mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-21 20:27:06 +03:00
parent
cd98fe13f7
commit
54ec5ac72c
123 changed files with 1500 additions and 727 deletions
|
@ -141,7 +141,9 @@ android {
|
|||
dependencies {
|
||||
implementation(project(":i18n"))
|
||||
implementation(project(":core"))
|
||||
implementation(project(":core-metadata"))
|
||||
implementation(project(":source-api"))
|
||||
implementation(project(":source-local"))
|
||||
implementation(project(":data"))
|
||||
implementation(project(":domain"))
|
||||
implementation(project(":presentation-core"))
|
||||
|
@ -201,7 +203,7 @@ dependencies {
|
|||
// TLS 1.3 support for Android < 10
|
||||
implementation(libs.conscrypt.android)
|
||||
|
||||
// Data serialization (JSON, protobuf)
|
||||
// Data serialization (JSON, protobuf, xml)
|
||||
implementation(kotlinx.bundles.serialization)
|
||||
|
||||
// HTML parser
|
||||
|
@ -225,9 +227,6 @@ dependencies {
|
|||
}
|
||||
implementation(libs.image.decoder)
|
||||
|
||||
// Sort
|
||||
implementation(libs.natural.comparator)
|
||||
|
||||
// UI libraries
|
||||
implementation(libs.material)
|
||||
implementation(libs.flexible.adapter.core)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package eu.kanade.core.prefs
|
||||
package eu.kanade.core.preference
|
||||
|
||||
import androidx.compose.ui.state.ToggleableState
|
||||
import tachiyomi.core.preference.CheckboxState
|
||||
|
||||
fun <T> CheckboxState.TriState<T>.asState(): ToggleableState {
|
||||
fun <T> CheckboxState.TriState<T>.asToggleableState(): ToggleableState {
|
||||
return when (this) {
|
||||
is CheckboxState.TriState.Exclude -> ToggleableState.Indeterminate
|
||||
is CheckboxState.TriState.Include -> ToggleableState.On
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.core.prefs
|
||||
package eu.kanade.core.preference
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
|
@ -1,16 +1,19 @@
|
|||
package eu.kanade.data.source.anime
|
||||
|
||||
import eu.kanade.domain.source.anime.model.AnimeSourcePagingSourceType
|
||||
import eu.kanade.domain.source.anime.repository.AnimeSourceRepository
|
||||
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.source.anime.AnimeSourceManager
|
||||
import eu.kanade.tachiyomi.source.anime.LocalAnimeSource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import tachiyomi.data.handlers.anime.AnimeDatabaseHandler
|
||||
import tachiyomi.data.source.anime.AnimeSourceLatestPagingSource
|
||||
import tachiyomi.data.source.anime.AnimeSourcePagingSourceType
|
||||
import tachiyomi.data.source.anime.AnimeSourcePopularPagingSource
|
||||
import tachiyomi.data.source.anime.AnimeSourceSearchPagingSource
|
||||
import tachiyomi.domain.source.anime.model.AnimeSource
|
||||
import tachiyomi.domain.source.anime.model.AnimeSourceWithCount
|
||||
import tachiyomi.source.local.entries.anime.LocalAnimeSource
|
||||
|
||||
class AnimeSourceRepositoryImpl(
|
||||
private val sourceManager: AnimeSourceManager,
|
||||
|
@ -35,9 +38,7 @@ class AnimeSourceRepositoryImpl(
|
|||
sourceIdsWithCount
|
||||
.filterNot { it.source == LocalAnimeSource.ID }
|
||||
.map { (sourceId, count) ->
|
||||
val source = sourceManager.getOrStub(sourceId).run {
|
||||
animeSourceMapper(this)
|
||||
}
|
||||
val source = animeSourceMapper(sourceManager.getOrStub(sourceId))
|
||||
source to count
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
package eu.kanade.data.source.manga
|
||||
|
||||
import eu.kanade.domain.source.manga.model.SourcePagingSourceType
|
||||
import eu.kanade.domain.source.manga.repository.MangaSourceRepository
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
|
||||
import eu.kanade.tachiyomi.source.manga.MangaSourceManager
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import tachiyomi.data.handlers.manga.MangaDatabaseHandler
|
||||
import tachiyomi.data.source.manga.SourceLatestPagingSource
|
||||
import tachiyomi.data.source.manga.SourcePagingSourceType
|
||||
import tachiyomi.data.source.manga.SourcePopularPagingSource
|
||||
import tachiyomi.data.source.manga.SourceSearchPagingSource
|
||||
import tachiyomi.domain.source.manga.model.MangaSourceWithCount
|
||||
import tachiyomi.domain.source.manga.model.Source
|
||||
import tachiyomi.source.local.entries.manga.LocalMangaSource
|
||||
|
||||
class MangaSourceRepositoryImpl(
|
||||
private val sourceManager: MangaSourceManager,
|
||||
|
@ -35,9 +38,7 @@ class MangaSourceRepositoryImpl(
|
|||
sourceIdsWithCount
|
||||
.filterNot { it.source == LocalMangaSource.ID }
|
||||
.map { (sourceId, count) ->
|
||||
val source = sourceManager.getOrStub(sourceId).run {
|
||||
mangaSourceMapper(this)
|
||||
}
|
||||
val source = mangaSourceMapper(sourceManager.getOrStub(sourceId))
|
||||
source to count
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,15 @@ package eu.kanade.domain.entries.manga.model
|
|||
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.tachiyomi.data.cache.MangaCoverCache
|
||||
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import tachiyomi.core.metadata.comicinfo.ComicInfo
|
||||
import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus
|
||||
import tachiyomi.domain.entries.TriStateFilter
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.items.chapter.model.Chapter
|
||||
import tachiyomi.source.local.entries.manga.LocalMangaSource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
|
@ -91,3 +94,25 @@ fun Manga.isLocal(): Boolean = source == LocalMangaSource.ID
|
|||
fun Manga.hasCustomCover(coverCache: MangaCoverCache = Injekt.get()): Boolean {
|
||||
return coverCache.getCustomCoverFile(id).exists()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ComicInfo instance based on the manga and chapter metadata.
|
||||
*/
|
||||
fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String) = ComicInfo(
|
||||
title = ComicInfo.Title(chapter.name),
|
||||
series = ComicInfo.Series(manga.title),
|
||||
web = ComicInfo.Web(chapterUrl),
|
||||
summary = manga.description?.let { ComicInfo.Summary(it) },
|
||||
writer = manga.author?.let { ComicInfo.Writer(it) },
|
||||
penciller = manga.artist?.let { ComicInfo.Penciller(it) },
|
||||
translator = chapter.scanlator?.let { ComicInfo.Translator(it) },
|
||||
genre = manga.genre?.let { ComicInfo.Genre(it.joinToString()) },
|
||||
publishingStatus = ComicInfo.PublishingStatusTachiyomi(
|
||||
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
|
||||
),
|
||||
inker = null,
|
||||
colorist = null,
|
||||
letterer = null,
|
||||
coverArtist = null,
|
||||
tags = null,
|
||||
)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package eu.kanade.domain.source.anime.interactor
|
||||
|
||||
import eu.kanade.domain.source.anime.model.AnimeSourcePagingSourceType
|
||||
import eu.kanade.domain.source.anime.repository.AnimeSourceRepository
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import tachiyomi.data.source.anime.AnimeSourcePagingSourceType
|
||||
|
||||
class GetRemoteAnime(
|
||||
private val repository: AnimeSourceRepository,
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
package eu.kanade.domain.source.anime.model
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
|
||||
typealias AnimeSourcePagingSourceType = PagingSource<Long, SAnime>
|
|
@ -1,8 +1,8 @@
|
|||
package eu.kanade.domain.source.anime.repository
|
||||
|
||||
import eu.kanade.domain.source.anime.model.AnimeSourcePagingSourceType
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import tachiyomi.data.source.anime.AnimeSourcePagingSourceType
|
||||
import tachiyomi.domain.source.anime.model.AnimeSource
|
||||
import tachiyomi.domain.source.anime.model.AnimeSourceWithCount
|
||||
|
||||
|
|
|
@ -2,13 +2,13 @@ package eu.kanade.domain.source.manga.interactor
|
|||
|
||||
import eu.kanade.domain.source.manga.repository.MangaSourceRepository
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import tachiyomi.domain.source.manga.model.Pin
|
||||
import tachiyomi.domain.source.manga.model.Pins
|
||||
import tachiyomi.domain.source.manga.model.Source
|
||||
import tachiyomi.source.local.entries.manga.LocalMangaSource
|
||||
|
||||
class GetEnabledMangaSources(
|
||||
private val repository: MangaSourceRepository,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package eu.kanade.domain.source.manga.interactor
|
||||
|
||||
import eu.kanade.domain.source.manga.model.SourcePagingSourceType
|
||||
import eu.kanade.domain.source.manga.repository.MangaSourceRepository
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import tachiyomi.data.source.manga.SourcePagingSourceType
|
||||
|
||||
class GetRemoteManga(
|
||||
private val repository: MangaSourceRepository,
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
package eu.kanade.domain.source.manga.model
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
|
||||
typealias SourcePagingSourceType = PagingSource<Long, SManga>
|
|
@ -1,8 +1,8 @@
|
|||
package eu.kanade.domain.source.manga.repository
|
||||
|
||||
import eu.kanade.domain.source.manga.model.SourcePagingSourceType
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import tachiyomi.data.source.manga.SourcePagingSourceType
|
||||
import tachiyomi.domain.source.manga.model.MangaSourceWithCount
|
||||
import tachiyomi.domain.source.manga.model.Source
|
||||
|
||||
|
|
|
@ -3,8 +3,6 @@ package eu.kanade.presentation.browse.anime.components
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ViewList
|
||||
import androidx.compose.material.icons.filled.ViewModule
|
||||
import androidx.compose.material.icons.outlined.Help
|
||||
import androidx.compose.material.icons.outlined.Public
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
|
@ -21,6 +19,7 @@ import eu.kanade.presentation.components.RadioMenuItem
|
|||
import eu.kanade.presentation.components.SearchToolbar
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.source.anime.LocalAnimeSource
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
|
||||
|
@ -34,12 +33,16 @@ fun BrowseAnimeSourceToolbar(
|
|||
navigateUp: () -> Unit,
|
||||
onWebViewClick: () -> Unit,
|
||||
onHelpClick: () -> Unit,
|
||||
onSettingsClick: () -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||
) {
|
||||
// Avoid capturing unstable source in actions lambda
|
||||
val title = source?.name
|
||||
val isLocalSource = source is LocalAnimeSource
|
||||
val isConfigurableSource = source is ConfigurableAnimeSource
|
||||
|
||||
var selectingDisplayMode by remember { mutableStateOf(false) }
|
||||
|
||||
SearchToolbar(
|
||||
navigateUp = navigateUp,
|
||||
|
@ -49,29 +52,31 @@ fun BrowseAnimeSourceToolbar(
|
|||
onSearch = onSearch,
|
||||
onClickCloseSearch = navigateUp,
|
||||
actions = {
|
||||
var selectingDisplayMode by remember { mutableStateOf(false) }
|
||||
AppBarActions(
|
||||
actions = listOf(
|
||||
actions = listOfNotNull(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_display_mode),
|
||||
icon = if (displayMode == LibraryDisplayMode.List) Icons.Filled.ViewList else Icons.Filled.ViewModule,
|
||||
onClick = { selectingDisplayMode = true },
|
||||
),
|
||||
if (isLocalSource) {
|
||||
AppBar.Action(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(R.string.label_help),
|
||||
icon = Icons.Outlined.Help,
|
||||
onClick = onHelpClick,
|
||||
)
|
||||
} else {
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_web_view),
|
||||
icon = Icons.Outlined.Public,
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(R.string.action_open_in_web_view),
|
||||
onClick = onWebViewClick,
|
||||
)
|
||||
},
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(R.string.action_settings),
|
||||
onClick = onSettingsClick,
|
||||
).takeIf { isConfigurableSource },
|
||||
),
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
expanded = selectingDisplayMode,
|
||||
onDismissRequest = { selectingDisplayMode = false },
|
||||
|
|
|
@ -22,7 +22,6 @@ import eu.kanade.presentation.browse.manga.components.BrowseMangaSourceList
|
|||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.MangaSource
|
||||
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
|
||||
import eu.kanade.tachiyomi.source.manga.MangaSourceManager
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
|
@ -32,6 +31,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
|||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.screens.EmptyScreenAction
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import tachiyomi.source.local.entries.manga.LocalMangaSource
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceContent(
|
||||
|
|
|
@ -23,7 +23,6 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.browse.manga.components.BaseMangaSourceItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.source.MangaSourcesState
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.source.browse.BrowseMangaSourceScreenModel.Listing
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
|
@ -36,6 +35,7 @@ import tachiyomi.presentation.core.screens.EmptyScreen
|
|||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import tachiyomi.presentation.core.theme.header
|
||||
import tachiyomi.presentation.core.util.plus
|
||||
import tachiyomi.source.local.entries.manga.LocalMangaSource
|
||||
|
||||
@Composable
|
||||
fun MangaSourcesScreen(
|
||||
|
|
|
@ -31,9 +31,9 @@ import eu.kanade.domain.source.manga.model.icon
|
|||
import eu.kanade.presentation.util.rememberResourceBitmapPainter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.domain.source.manga.model.Source
|
||||
import tachiyomi.source.local.entries.manga.LocalMangaSource
|
||||
|
||||
private val defaultModifier = Modifier
|
||||
.height(40.dp)
|
||||
|
|
|
@ -3,8 +3,6 @@ package eu.kanade.presentation.browse.manga.components
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ViewList
|
||||
import androidx.compose.material.icons.filled.ViewModule
|
||||
import androidx.compose.material.icons.outlined.Help
|
||||
import androidx.compose.material.icons.outlined.Public
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
|
@ -20,9 +18,10 @@ import eu.kanade.presentation.components.DropdownMenu
|
|||
import eu.kanade.presentation.components.RadioMenuItem
|
||||
import eu.kanade.presentation.components.SearchToolbar
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.MangaSource
|
||||
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
import tachiyomi.source.local.entries.manga.LocalMangaSource
|
||||
|
||||
@Composable
|
||||
fun BrowseMangaSourceToolbar(
|
||||
|
@ -34,12 +33,16 @@ fun BrowseMangaSourceToolbar(
|
|||
navigateUp: () -> Unit,
|
||||
onWebViewClick: () -> Unit,
|
||||
onHelpClick: () -> Unit,
|
||||
onSettingsClick: () -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||
) {
|
||||
// Avoid capturing unstable source in actions lambda
|
||||
val title = source?.name
|
||||
val isLocalSource = source is LocalMangaSource
|
||||
val isConfigurableSource = source is ConfigurableSource
|
||||
|
||||
var selectingDisplayMode by remember { mutableStateOf(false) }
|
||||
|
||||
SearchToolbar(
|
||||
navigateUp = navigateUp,
|
||||
|
@ -49,29 +52,31 @@ fun BrowseMangaSourceToolbar(
|
|||
onSearch = onSearch,
|
||||
onClickCloseSearch = navigateUp,
|
||||
actions = {
|
||||
var selectingDisplayMode by remember { mutableStateOf(false) }
|
||||
AppBarActions(
|
||||
actions = listOf(
|
||||
actions = listOfNotNull(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_display_mode),
|
||||
icon = if (displayMode == LibraryDisplayMode.List) Icons.Filled.ViewList else Icons.Filled.ViewModule,
|
||||
onClick = { selectingDisplayMode = true },
|
||||
),
|
||||
if (isLocalSource) {
|
||||
AppBar.Action(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(R.string.label_help),
|
||||
icon = Icons.Outlined.Help,
|
||||
onClick = onHelpClick,
|
||||
)
|
||||
} else {
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_web_view),
|
||||
icon = Icons.Outlined.Public,
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(R.string.action_open_in_web_view),
|
||||
onClick = onWebViewClick,
|
||||
)
|
||||
},
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(R.string.action_settings),
|
||||
onClick = onSettingsClick,
|
||||
).takeIf { isConfigurableSource },
|
||||
),
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
expanded = selectingDisplayMode,
|
||||
onDismissRequest = { selectingDisplayMode = false },
|
||||
|
|
|
@ -21,7 +21,7 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.core.prefs.asState
|
||||
import eu.kanade.core.preference.asToggleableState
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.core.preference.CheckboxState
|
||||
import tachiyomi.domain.category.model.Category
|
||||
|
@ -110,7 +110,7 @@ fun ChangeCategoryDialog(
|
|||
when (checkbox) {
|
||||
is CheckboxState.TriState -> {
|
||||
TriStateCheckbox(
|
||||
state = checkbox.asState(),
|
||||
state = checkbox.asToggleableState(),
|
||||
onClick = { onChange(checkbox) },
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package eu.kanade.presentation.extensions
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
|
||||
/**
|
||||
* Launches request for [Manifest.permission.WRITE_EXTERNAL_STORAGE] permission
|
||||
*/
|
||||
@Composable
|
||||
fun DiskUtil.RequestStoragePermission() {
|
||||
val permissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
LaunchedEffect(Unit) {
|
||||
permissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
|
@ -67,10 +67,12 @@ fun DeleteLibraryEntryDialog(
|
|||
list.forEach { state ->
|
||||
val onCheck = {
|
||||
val index = list.indexOf(state)
|
||||
if (index != -1) {
|
||||
val mutableList = list.toMutableList()
|
||||
mutableList[index] = state.next() as CheckboxState.State<Int>
|
||||
list = mutableList.toList()
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
|
|
@ -14,7 +14,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import eu.kanade.core.prefs.PreferenceMutableState
|
||||
import eu.kanade.core.preference.PreferenceMutableState
|
||||
import eu.kanade.presentation.library.LibraryTabs
|
||||
import eu.kanade.tachiyomi.ui.library.anime.AnimeLibraryItem
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
|
@ -10,7 +10,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import eu.kanade.core.prefs.PreferenceMutableState
|
||||
import eu.kanade.core.preference.PreferenceMutableState
|
||||
import eu.kanade.presentation.library.manga.LibraryPagerEmptyScreen
|
||||
import eu.kanade.tachiyomi.ui.library.anime.AnimeLibraryItem
|
||||
import tachiyomi.domain.library.anime.LibraryAnime
|
||||
|
|
|
@ -14,7 +14,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import eu.kanade.core.prefs.PreferenceMutableState
|
||||
import eu.kanade.core.preference.PreferenceMutableState
|
||||
import eu.kanade.presentation.library.LibraryTabs
|
||||
import eu.kanade.tachiyomi.ui.library.manga.MangaLibraryItem
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
|
@ -16,7 +16,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.core.prefs.PreferenceMutableState
|
||||
import eu.kanade.core.preference.PreferenceMutableState
|
||||
import eu.kanade.presentation.animelib.components.GlobalSearchItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.library.manga.MangaLibraryItem
|
||||
|
|
|
@ -37,6 +37,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.core.net.toUri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.domain.backup.service.BackupPreferences
|
||||
import eu.kanade.presentation.extensions.RequestStoragePermission
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.util.collectAsState
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
|
|
@ -62,6 +62,14 @@ import tachiyomi.data.handlers.manga.MangaDatabaseHandler
|
|||
import tachiyomi.data.listOfStringsAdapter
|
||||
import tachiyomi.data.updateStrategyAdapter
|
||||
import tachiyomi.mi.data.AnimeDatabase
|
||||
import tachiyomi.source.local.image.anime.AndroidLocalAnimeCoverManager
|
||||
import tachiyomi.source.local.image.anime.LocalAnimeCoverManager
|
||||
import tachiyomi.source.local.image.manga.AndroidLocalMangaCoverManager
|
||||
import tachiyomi.source.local.image.manga.LocalMangaCoverManager
|
||||
import tachiyomi.source.local.io.anime.AndroidLocalAnimeSourceFileSystem
|
||||
import tachiyomi.source.local.io.anime.LocalAnimeSourceFileSystem
|
||||
import tachiyomi.source.local.io.manga.AndroidLocalMangaSourceFileSystem
|
||||
import tachiyomi.source.local.io.manga.LocalMangaSourceFileSystem
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
import uy.kohesive.injekt.api.addSingleton
|
||||
|
@ -198,6 +206,12 @@ class AppModule(val app: Application) : InjektModule {
|
|||
|
||||
addSingletonFactory { ImageSaver(app) }
|
||||
|
||||
addSingletonFactory<LocalMangaSourceFileSystem> { AndroidLocalMangaSourceFileSystem(app) }
|
||||
addSingletonFactory<LocalMangaCoverManager> { AndroidLocalMangaCoverManager(app, get()) }
|
||||
|
||||
addSingletonFactory<LocalAnimeSourceFileSystem> { AndroidLocalAnimeSourceFileSystem(app) }
|
||||
addSingletonFactory<LocalAnimeCoverManager> { AndroidLocalAnimeCoverManager(app, get()) }
|
||||
|
||||
addSingletonFactory { ExternalIntents() }
|
||||
|
||||
// Asynchronously init expensive components for a faster cold start
|
||||
|
|
|
@ -9,8 +9,8 @@ import coil.decode.ImageDecoderDecoder
|
|||
import coil.decode.ImageSource
|
||||
import coil.fetch.SourceResult
|
||||
import coil.request.Options
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import okio.BufferedSource
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import tachiyomi.decoder.ImageDecoder
|
||||
|
||||
/**
|
||||
|
|
|
@ -68,7 +68,7 @@ class AnimeDownloadManager(
|
|||
*/
|
||||
fun pauseDownloads() {
|
||||
downloader.pause()
|
||||
AnimeDownloadService.stop(context)
|
||||
downloader.stop()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -76,7 +76,7 @@ class AnimeDownloadManager(
|
|||
*/
|
||||
fun clearQueue() {
|
||||
downloader.clearQueue()
|
||||
AnimeDownloadService.stop(context)
|
||||
downloader.stop()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -117,8 +117,8 @@ class AnimeDownloadManager(
|
|||
val wasRunning = downloader.isRunning
|
||||
|
||||
if (downloads.isEmpty()) {
|
||||
AnimeDownloadService.stop(context)
|
||||
queue.clear()
|
||||
downloader.clearQueue()
|
||||
downloader.stop()
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -277,7 +277,6 @@ class AnimeDownloadManager(
|
|||
|
||||
if (wasRunning) {
|
||||
if (queue.isEmpty()) {
|
||||
AnimeDownloadService.stop(context)
|
||||
downloader.stop()
|
||||
} else if (queue.isNotEmpty()) {
|
||||
downloader.start()
|
||||
|
|
|
@ -23,7 +23,6 @@ import eu.kanade.tachiyomi.animesource.online.fetchUrlFromVideo
|
|||
import eu.kanade.tachiyomi.data.cache.EpisodeCache
|
||||
import eu.kanade.tachiyomi.data.download.anime.model.AnimeDownload
|
||||
import eu.kanade.tachiyomi.data.download.anime.model.AnimeDownloadQueue
|
||||
import eu.kanade.tachiyomi.data.download.manga.MangaDownloader.Companion.WARNING_NOTIF_TIMEOUT_MS
|
||||
import eu.kanade.tachiyomi.data.library.anime.AnimeLibraryUpdateNotifier
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationHandler
|
||||
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||
|
@ -31,7 +30,6 @@ import eu.kanade.tachiyomi.source.anime.AnimeSourceManager
|
|||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||
import eu.kanade.tachiyomi.util.storage.toFFmpegString
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import kotlinx.coroutines.async
|
||||
import logcat.LogPriority
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
|
@ -42,6 +40,7 @@ import rx.schedulers.Schedulers
|
|||
import tachiyomi.core.util.lang.launchIO
|
||||
import tachiyomi.core.util.lang.launchNow
|
||||
import tachiyomi.core.util.lang.withUIContext
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.domain.items.episode.model.Episode
|
||||
|
@ -179,6 +178,11 @@ class AnimeDownloader(
|
|||
}
|
||||
|
||||
isPaused = false
|
||||
|
||||
// Prevent recursion when DownloadService.onDestroy() calls downloader.stop()
|
||||
if (AnimeDownloadService.isRunning.value) {
|
||||
AnimeDownloadService.stop(context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -232,11 +236,11 @@ class AnimeDownloader(
|
|||
completeAnimeDownload(completedDownload)
|
||||
},
|
||||
{ error ->
|
||||
AnimeDownloadService.stop(context)
|
||||
logcat(LogPriority.ERROR, error)
|
||||
queue.state.value.forEach {
|
||||
notifier.onError(it, error.message, it.episode.name, it.anime.title)
|
||||
}
|
||||
stop()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -749,7 +753,7 @@ class AnimeDownloader(
|
|||
queue.remove(download)
|
||||
}
|
||||
if (areAllAnimeDownloadsFinished()) {
|
||||
AnimeDownloadService.stop(context)
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ class MangaDownloadManager(
|
|||
*/
|
||||
fun pauseDownloads() {
|
||||
downloader.pause()
|
||||
MangaDownloadService.stop(context)
|
||||
downloader.stop()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -75,7 +75,7 @@ class MangaDownloadManager(
|
|||
*/
|
||||
fun clearQueue() {
|
||||
downloader.clearQueue()
|
||||
MangaDownloadService.stop(context)
|
||||
downloader.stop()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -115,8 +115,8 @@ class MangaDownloadManager(
|
|||
val wasRunning = downloader.isRunning
|
||||
|
||||
if (downloads.isEmpty()) {
|
||||
MangaDownloadService.stop(context)
|
||||
queue.clear()
|
||||
downloader.clearQueue()
|
||||
downloader.stop()
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -274,7 +274,6 @@ class MangaDownloadManager(
|
|||
|
||||
if (wasRunning) {
|
||||
if (queue.isEmpty()) {
|
||||
MangaDownloadService.stop(context)
|
||||
downloader.stop()
|
||||
} else if (queue.isNotEmpty()) {
|
||||
downloader.start()
|
||||
|
|
|
@ -4,8 +4,6 @@ import android.content.Context
|
|||
import com.hippo.unifile.UniFile
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.domain.download.service.DownloadPreferences
|
||||
import eu.kanade.domain.entries.manga.model.COMIC_INFO_FILE
|
||||
import eu.kanade.domain.entries.manga.model.ComicInfo
|
||||
import eu.kanade.domain.entries.manga.model.getComicInfo
|
||||
import eu.kanade.domain.items.chapter.model.toSChapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
@ -21,7 +19,6 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
|||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE
|
||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
|
@ -40,11 +37,14 @@ import rx.Observable
|
|||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
|
||||
import tachiyomi.core.metadata.comicinfo.ComicInfo
|
||||
import tachiyomi.core.util.lang.awaitSingle
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import tachiyomi.core.util.lang.launchNow
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.lang.withUIContext
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.items.chapter.model.Chapter
|
||||
|
@ -167,6 +167,11 @@ class MangaDownloader(
|
|||
}
|
||||
|
||||
isPaused = false
|
||||
|
||||
// Prevent recursion when DownloadService.onDestroy() calls downloader.stop()
|
||||
if (MangaDownloadService.isRunning.value) {
|
||||
MangaDownloadService.stop(context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -217,9 +222,9 @@ class MangaDownloader(
|
|||
completeDownload(it)
|
||||
},
|
||||
{ error ->
|
||||
MangaDownloadService.stop(context)
|
||||
logcat(LogPriority.ERROR, error)
|
||||
notifier.onError(error.message)
|
||||
stop()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -634,7 +639,7 @@ class MangaDownloader(
|
|||
queue.remove(download)
|
||||
}
|
||||
if (areAllDownloadsFinished()) {
|
||||
MangaDownloadService.stop(context)
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,9 +15,9 @@ import eu.kanade.tachiyomi.R
|
|||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import logcat.LogPriority
|
||||
import okio.IOException
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
|
|
@ -21,6 +21,9 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.runBlocking
|
||||
import tachiyomi.domain.source.anime.model.AnimeSourceData
|
||||
import tachiyomi.domain.source.anime.repository.AnimeSourceDataRepository
|
||||
import tachiyomi.source.local.entries.anime.LocalAnimeSource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
|
@ -44,7 +47,15 @@ class AnimeSourceManager(
|
|||
scope.launch {
|
||||
extensionManager.installedExtensionsFlow
|
||||
.collectLatest { extensions ->
|
||||
val mutableMap = ConcurrentHashMap<Long, AnimeSource>(mapOf(LocalAnimeSource.ID to LocalAnimeSource(context)))
|
||||
val mutableMap = ConcurrentHashMap<Long, AnimeSource>(
|
||||
mapOf(
|
||||
LocalAnimeSource.ID to LocalAnimeSource(
|
||||
context,
|
||||
Injekt.get(),
|
||||
Injekt.get(),
|
||||
),
|
||||
),
|
||||
)
|
||||
extensions.forEach { extension ->
|
||||
extension.sources.forEach {
|
||||
mutableMap[it.id] = it
|
||||
|
|
|
@ -15,7 +15,6 @@ import eu.kanade.tachiyomi.source.UnmeteredSource
|
|||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.toFFmpegString
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
@ -23,6 +22,7 @@ import kotlinx.serialization.json.decodeFromStream
|
|||
import logcat.LogPriority
|
||||
import rx.Observable
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.items.episode.service.EpisodeRecognition
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
|
|
@ -1,462 +1,3 @@
|
|||
package eu.kanade.tachiyomi.source.manga
|
||||
|
||||
import android.content.Context
|
||||
import com.github.junrar.Archive
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.domain.entries.manga.model.COMIC_INFO_FILE
|
||||
import eu.kanade.domain.entries.manga.model.ComicInfo
|
||||
import eu.kanade.domain.entries.manga.model.copyFromComicInfo
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import logcat.LogPriority
|
||||
import nl.adaptivity.xmlutil.AndroidXmlReader
|
||||
import nl.adaptivity.xmlutil.serialization.XML
|
||||
import rx.Observable
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.items.chapter.service.ChapterRecognition
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
class LocalMangaSource(
|
||||
private val context: Context,
|
||||
) : CatalogueSource, UnmeteredSource {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
private val xml: XML by injectLazy()
|
||||
|
||||
override val name: String = context.getString(R.string.local_manga_source)
|
||||
|
||||
override val id: Long = ID
|
||||
|
||||
override val lang: String = "other"
|
||||
|
||||
override fun toString() = name
|
||||
|
||||
override val supportsLatest: Boolean = true
|
||||
|
||||
// Browse related
|
||||
override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
|
||||
|
||||
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
val baseDirsFiles = getBaseDirectoriesFiles(context)
|
||||
|
||||
var mangaDirs = baseDirsFiles
|
||||
// Filter out files that are hidden and is not a folder
|
||||
.filter { it.isDirectory && !it.name.startsWith('.') }
|
||||
.distinctBy { it.name }
|
||||
|
||||
val lastModifiedLimit = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
||||
// Filter by query or last modified
|
||||
mangaDirs = mangaDirs.filter {
|
||||
if (lastModifiedLimit == 0L) {
|
||||
it.name.contains(query, ignoreCase = true)
|
||||
} else {
|
||||
it.lastModified() >= lastModifiedLimit
|
||||
}
|
||||
}
|
||||
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is OrderBy -> {
|
||||
when (filter.state!!.index) {
|
||||
0 -> {
|
||||
mangaDirs = if (filter.state!!.ascending) {
|
||||
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
} else {
|
||||
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
}
|
||||
}
|
||||
1 -> {
|
||||
mangaDirs = if (filter.state!!.ascending) {
|
||||
mangaDirs.sortedBy(File::lastModified)
|
||||
} else {
|
||||
mangaDirs.sortedByDescending(File::lastModified)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
/* Do nothing */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transform mangaDirs to list of SManga
|
||||
val mangas = mangaDirs.map { mangaDir ->
|
||||
SManga.create().apply {
|
||||
title = mangaDir.name
|
||||
url = mangaDir.name
|
||||
|
||||
// Try to find the cover
|
||||
val cover = getCoverFile(mangaDir.name, baseDirsFiles)
|
||||
if (cover != null && cover.exists()) {
|
||||
thumbnail_url = cover.absolutePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch chapters of all the manga
|
||||
mangas.forEach { manga ->
|
||||
runBlocking {
|
||||
val chapters = getChapterList(manga)
|
||||
if (chapters.isNotEmpty()) {
|
||||
val chapter = chapters.last()
|
||||
val format = getFormat(chapter)
|
||||
|
||||
if (format is Format.Epub) {
|
||||
EpubFile(format.file).use { epub ->
|
||||
epub.fillMangaMetadata(manga)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the cover from the first chapter found if not available
|
||||
if (manga.thumbnail_url == null) {
|
||||
updateCover(chapter, manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Observable.just(MangasPage(mangas.toList(), false))
|
||||
}
|
||||
|
||||
// Manga details related
|
||||
override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext {
|
||||
val baseDirsFile = getBaseDirectoriesFiles(context)
|
||||
|
||||
getCoverFile(manga.url, baseDirsFile)?.let {
|
||||
manga.thumbnail_url = it.absolutePath
|
||||
}
|
||||
|
||||
// Augment manga details based on metadata files
|
||||
try {
|
||||
val mangaDirFiles = getMangaDirsFiles(manga.url, baseDirsFile).toList()
|
||||
|
||||
val comicInfoFile = mangaDirFiles
|
||||
.firstOrNull { it.name == COMIC_INFO_FILE }
|
||||
val noXmlFile = mangaDirFiles
|
||||
.firstOrNull { it.name == ".noxml" }
|
||||
val legacyJsonDetailsFile = mangaDirFiles
|
||||
.firstOrNull { it.extension == "json" }
|
||||
|
||||
when {
|
||||
// Top level ComicInfo.xml
|
||||
comicInfoFile != null -> {
|
||||
noXmlFile?.delete()
|
||||
setMangaDetailsFromComicInfoFile(comicInfoFile.inputStream(), manga)
|
||||
}
|
||||
|
||||
// TODO: automatically convert these to ComicInfo.xml
|
||||
legacyJsonDetailsFile != null -> {
|
||||
json.decodeFromStream<MangaDetails>(legacyJsonDetailsFile.inputStream()).run {
|
||||
title?.let { manga.title = it }
|
||||
author?.let { manga.author = it }
|
||||
artist?.let { manga.artist = it }
|
||||
description?.let { manga.description = it }
|
||||
genre?.let { manga.genre = it.joinToString() }
|
||||
status?.let { manga.status = it }
|
||||
}
|
||||
}
|
||||
|
||||
// Copy ComicInfo.xml from chapter archive to top level if found
|
||||
noXmlFile == null -> {
|
||||
val chapterArchives = mangaDirFiles
|
||||
.filter { isSupportedArchiveFile(it.extension) }
|
||||
.toList()
|
||||
|
||||
val mangaDir = getMangaDir(manga.url, baseDirsFile)
|
||||
val folderPath = mangaDir?.absolutePath
|
||||
|
||||
val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)
|
||||
if (copiedFile != null) {
|
||||
setMangaDetailsFromComicInfoFile(copiedFile.inputStream(), manga)
|
||||
} else {
|
||||
// Avoid re-scanning
|
||||
File("$folderPath/.noxml").createNewFile()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.ERROR, e) { "Error setting manga details from local metadata for ${manga.title}" }
|
||||
}
|
||||
|
||||
return@withIOContext manga
|
||||
}
|
||||
|
||||
private fun copyComicInfoFileFromArchive(chapterArchives: List<File>, folderPath: String?): File? {
|
||||
for (chapter in chapterArchives) {
|
||||
when (getFormat(chapter)) {
|
||||
is Format.Zip -> {
|
||||
ZipFile(chapter).use { zip: ZipFile ->
|
||||
zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile ->
|
||||
zip.getInputStream(comicInfoFile).buffered().use { stream ->
|
||||
return copyComicInfoFile(stream, folderPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is Format.Rar -> {
|
||||
Archive(chapter).use { rar: Archive ->
|
||||
rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile ->
|
||||
rar.getInputStream(comicInfoFile).buffered().use { stream ->
|
||||
return copyComicInfoFile(stream, folderPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun copyComicInfoFile(comicInfoFileStream: InputStream, folderPath: String?): File {
|
||||
return File("$folderPath/$COMIC_INFO_FILE").apply {
|
||||
outputStream().use { outputStream ->
|
||||
comicInfoFileStream.use { it.copyTo(outputStream) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setMangaDetailsFromComicInfoFile(stream: InputStream, manga: SManga) {
|
||||
val comicInfo = AndroidXmlReader(stream, StandardCharsets.UTF_8.name()).use {
|
||||
xml.decodeFromReader<ComicInfo>(it)
|
||||
}
|
||||
|
||||
manga.copyFromComicInfo(comicInfo)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class MangaDetails(
|
||||
val title: String? = null,
|
||||
val author: String? = null,
|
||||
val artist: String? = null,
|
||||
val description: String? = null,
|
||||
val genre: List<String>? = null,
|
||||
val status: Int? = null,
|
||||
)
|
||||
|
||||
// Chapters
|
||||
override suspend fun getChapterList(manga: SManga): List<SChapter> {
|
||||
val baseDirsFile = getBaseDirectoriesFiles(context)
|
||||
return getMangaDirsFiles(manga.url, baseDirsFile)
|
||||
// Only keep supported formats
|
||||
.filter { it.isDirectory || isSupportedArchiveFile(it.extension) }
|
||||
.map { chapterFile ->
|
||||
SChapter.create().apply {
|
||||
url = "${manga.url}/${chapterFile.name}"
|
||||
name = if (chapterFile.isDirectory) {
|
||||
chapterFile.name
|
||||
} else {
|
||||
chapterFile.nameWithoutExtension
|
||||
}
|
||||
date_upload = chapterFile.lastModified()
|
||||
chapter_number = ChapterRecognition.parseChapterNumber(manga.title, this.name, this.chapter_number)
|
||||
|
||||
val format = getFormat(chapterFile)
|
||||
if (format is Format.Epub) {
|
||||
EpubFile(format.file).use { epub ->
|
||||
epub.fillChapterMetadata(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sortedWith { c1, c2 ->
|
||||
val c = c2.chapter_number.compareTo(c1.chapter_number)
|
||||
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
// Filters
|
||||
override fun getFilterList() = FilterList(OrderBy(context))
|
||||
|
||||
private val POPULAR_FILTERS = FilterList(OrderBy(context))
|
||||
private val LATEST_FILTERS = FilterList(OrderBy(context).apply { state = Filter.Sort.Selection(1, false) })
|
||||
|
||||
private class OrderBy(context: Context) : Filter.Sort(
|
||||
context.getString(R.string.local_filter_order_by),
|
||||
arrayOf(context.getString(R.string.title), context.getString(R.string.date)),
|
||||
Selection(0, true),
|
||||
)
|
||||
|
||||
// Unused stuff
|
||||
override suspend fun getPageList(chapter: SChapter) = throw UnsupportedOperationException("Unused")
|
||||
|
||||
// Miscellaneous
|
||||
private fun isSupportedArchiveFile(extension: String): Boolean {
|
||||
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
|
||||
}
|
||||
|
||||
fun getFormat(chapter: SChapter): Format {
|
||||
val baseDirs = getBaseDirectories(context)
|
||||
|
||||
for (dir in baseDirs) {
|
||||
val chapFile = File(dir, chapter.url)
|
||||
if (!chapFile.exists()) continue
|
||||
|
||||
return getFormat(chapFile)
|
||||
}
|
||||
throw Exception(context.getString(R.string.chapter_not_found))
|
||||
}
|
||||
|
||||
private fun getFormat(file: File) = with(file) {
|
||||
when {
|
||||
isDirectory -> Format.Directory(this)
|
||||
extension.equals("zip", true) || extension.equals("cbz", true) -> Format.Zip(this)
|
||||
extension.equals("rar", true) || extension.equals("cbr", true) -> Format.Rar(this)
|
||||
extension.equals("epub", true) -> Format.Epub(this)
|
||||
else -> throw Exception(context.getString(R.string.local_invalid_format))
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCover(chapter: SChapter, manga: SManga): File? {
|
||||
return try {
|
||||
when (val format = getFormat(chapter)) {
|
||||
is Format.Directory -> {
|
||||
val entry = format.file.listFiles()
|
||||
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||
|
||||
entry?.let { updateCover(context, manga, it.inputStream()) }
|
||||
}
|
||||
is Format.Zip -> {
|
||||
ZipFile(format.file).use { zip ->
|
||||
val entry = zip.entries().toList()
|
||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||
|
||||
entry?.let { updateCover(context, manga, zip.getInputStream(it)) }
|
||||
}
|
||||
}
|
||||
is Format.Rar -> {
|
||||
Archive(format.file).use { archive ->
|
||||
val entry = archive.fileHeaders
|
||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
||||
|
||||
entry?.let { updateCover(context, manga, archive.getInputStream(it)) }
|
||||
}
|
||||
}
|
||||
is Format.Epub -> {
|
||||
EpubFile(format.file).use { epub ->
|
||||
val entry = epub.getImagesFromPages()
|
||||
.firstOrNull()
|
||||
?.let { epub.getEntry(it) }
|
||||
|
||||
entry?.let { updateCover(context, manga, epub.getInputStream(it)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.ERROR, e) { "Error updating cover for ${manga.title}" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Format {
|
||||
data class Directory(val file: File) : Format()
|
||||
data class Zip(val file: File) : Format()
|
||||
data class Rar(val file: File) : Format()
|
||||
data class Epub(val file: File) : Format()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ID = 0L
|
||||
const val HELP_URL = "https://aniyomi.org/help/guides/local-manga/"
|
||||
|
||||
private const val DEFAULT_COVER_NAME = "cover.jpg"
|
||||
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||
|
||||
private fun getBaseDirectories(context: Context): Sequence<File> {
|
||||
val localFolder = context.getString(R.string.app_name) + File.separator + "local"
|
||||
return DiskUtil.getExternalStorages(context)
|
||||
.map { File(it.absolutePath, localFolder) }
|
||||
.asSequence()
|
||||
}
|
||||
|
||||
private fun getBaseDirectoriesFiles(context: Context): Sequence<File> {
|
||||
return getBaseDirectories(context)
|
||||
// Get all the files inside all baseDir
|
||||
.flatMap { it.listFiles().orEmpty().toList() }
|
||||
}
|
||||
|
||||
private fun getMangaDir(mangaUrl: String, baseDirsFile: Sequence<File>): File? {
|
||||
return baseDirsFile
|
||||
// Get the first mangaDir or null
|
||||
.firstOrNull { it.isDirectory && it.name == mangaUrl }
|
||||
}
|
||||
|
||||
private fun getMangaDirsFiles(mangaUrl: String, baseDirsFile: Sequence<File>): Sequence<File> {
|
||||
return baseDirsFile
|
||||
// Filter out ones that are not related to the manga and is not a directory
|
||||
.filter { it.isDirectory && it.name == mangaUrl }
|
||||
// Get all the files inside the filtered folders
|
||||
.flatMap { it.listFiles().orEmpty().toList() }
|
||||
}
|
||||
|
||||
private fun getCoverFile(mangaUrl: String, baseDirsFile: Sequence<File>): File? {
|
||||
return getMangaDirsFiles(mangaUrl, baseDirsFile)
|
||||
// Get all file whose names start with 'cover'
|
||||
.filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
|
||||
// Get the first actual image
|
||||
.firstOrNull {
|
||||
ImageUtil.isImage(it.name) { it.inputStream() }
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCover(context: Context, manga: SManga, inputStream: InputStream): File? {
|
||||
val baseDirsFiles = getBaseDirectoriesFiles(context)
|
||||
|
||||
val mangaDir = getMangaDir(manga.url, baseDirsFiles)
|
||||
if (mangaDir == null) {
|
||||
inputStream.close()
|
||||
return null
|
||||
}
|
||||
|
||||
var coverFile = getCoverFile(manga.url, baseDirsFiles)
|
||||
if (coverFile == null) {
|
||||
coverFile = File(mangaDir.absolutePath, DEFAULT_COVER_NAME)
|
||||
coverFile.createNewFile()
|
||||
}
|
||||
|
||||
// It might not exist at this point
|
||||
coverFile.parentFile?.mkdirs()
|
||||
inputStream.use { input ->
|
||||
coverFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
DiskUtil.createNoMediaFile(UniFile.fromFile(mangaDir), context)
|
||||
|
||||
manga.thumbnail_url = coverFile.absolutePath
|
||||
return coverFile
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
|
||||
|
|
|
@ -5,6 +5,7 @@ import eu.kanade.domain.source.service.SourcePreferences
|
|||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import eu.kanade.tachiyomi.source.MangaSource
|
||||
import tachiyomi.domain.source.manga.model.MangaSourceData
|
||||
import tachiyomi.source.local.entries.manga.LocalMangaSource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
|
|
|
@ -22,6 +22,9 @@ import kotlinx.coroutines.runBlocking
|
|||
import rx.Observable
|
||||
import tachiyomi.domain.source.manga.model.MangaSourceData
|
||||
import tachiyomi.domain.source.manga.repository.MangaSourceDataRepository
|
||||
import tachiyomi.source.local.entries.manga.LocalMangaSource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
|
@ -45,7 +48,15 @@ class MangaSourceManager(
|
|||
scope.launch {
|
||||
extensionManager.installedExtensionsFlow
|
||||
.collectLatest { extensions ->
|
||||
val mutableMap = ConcurrentHashMap<Long, MangaSource>(mapOf(LocalMangaSource.ID to LocalMangaSource(context)))
|
||||
val mutableMap = ConcurrentHashMap<Long, MangaSource>(
|
||||
mapOf(
|
||||
LocalMangaSource.ID to LocalMangaSource(
|
||||
context,
|
||||
Injekt.get(),
|
||||
Injekt.get(),
|
||||
),
|
||||
),
|
||||
)
|
||||
extensions.forEach { extension ->
|
||||
extension.sources.forEach {
|
||||
mutableMap[it.id] = it
|
||||
|
|
|
@ -14,6 +14,7 @@ import cafe.adriel.voyager.navigator.Navigator
|
|||
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
||||
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||
import eu.kanade.presentation.components.TabbedScreen
|
||||
import eu.kanade.presentation.extensions.RequestStoragePermission
|
||||
import eu.kanade.presentation.util.Tab
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.browse.anime.extension.AnimeExtensionsScreenModel
|
||||
|
|
|
@ -58,7 +58,7 @@ class SourcePreferencesScreen(val sourceId: Long) : Screen() {
|
|||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(text = Injekt.get<AnimeSourceManager>().get(sourceId)!!.toString()) },
|
||||
title = { Text(text = Injekt.get<AnimeSourceManager>().getOrStub(sourceId).toString()) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigator::pop) {
|
||||
Icon(
|
||||
|
|
|
@ -49,6 +49,7 @@ import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
|||
import eu.kanade.tachiyomi.core.Constants
|
||||
import eu.kanade.tachiyomi.source.anime.AnimeSourceManager
|
||||
import eu.kanade.tachiyomi.source.anime.LocalAnimeSource
|
||||
import eu.kanade.tachiyomi.ui.browse.anime.extension.details.SourcePreferencesScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.anime.source.browse.BrowseAnimeSourceScreenModel.Listing
|
||||
import eu.kanade.tachiyomi.ui.category.CategoriesTab
|
||||
import eu.kanade.tachiyomi.ui.entries.anime.AnimeScreen
|
||||
|
@ -124,6 +125,7 @@ data class BrowseAnimeSourceScreen(
|
|||
navigateUp = navigateUp,
|
||||
onWebViewClick = onWebViewClick,
|
||||
onHelpClick = onHelpClick,
|
||||
onSettingsClick = { navigator.push(SourcePreferencesScreen(sourceId)) },
|
||||
onSearch = { screenModel.search(it) },
|
||||
)
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import androidx.paging.filter
|
|||
import androidx.paging.map
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import eu.kanade.core.prefs.asState
|
||||
import eu.kanade.core.preference.asState
|
||||
import eu.kanade.domain.entries.anime.interactor.UpdateAnime
|
||||
import eu.kanade.domain.entries.anime.model.copyFrom
|
||||
import eu.kanade.domain.entries.anime.model.toDomainAnime
|
||||
|
|
|
@ -63,7 +63,10 @@ fun SourceFilterAnimeDialog(
|
|||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Button(onClick = onFilter) {
|
||||
Button(onClick = {
|
||||
onFilter()
|
||||
onDismissRequest()
|
||||
},) {
|
||||
Text(stringResource(R.string.action_filter))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ class MangaSourcePreferencesScreen(val sourceId: Long) : Screen() {
|
|||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(text = Injekt.get<MangaSourceManager>().get(sourceId)!!.toString()) },
|
||||
title = { Text(text = Injekt.get<MangaSourceManager>().getOrStub(sourceId).toString()) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = navigator::pop) {
|
||||
Icon(
|
||||
|
|
|
@ -24,7 +24,6 @@ import eu.kanade.presentation.components.SearchToolbar
|
|||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.core.Constants
|
||||
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.source.browse.BrowseMangaSourceScreenModel
|
||||
import eu.kanade.tachiyomi.ui.entries.manga.MangaScreen
|
||||
|
@ -34,6 +33,7 @@ import kotlinx.coroutines.launch
|
|||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.source.local.entries.manga.LocalMangaSource
|
||||
|
||||
data class MangaSourceSearchScreen(
|
||||
private val oldManga: Manga,
|
||||
|
|
|
@ -46,9 +46,9 @@ import eu.kanade.presentation.util.Screen
|
|||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.core.Constants
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
|
||||
import eu.kanade.tachiyomi.source.manga.MangaSourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.extension.details.MangaSourcePreferencesScreen
|
||||
import eu.kanade.tachiyomi.ui.browse.manga.source.browse.BrowseMangaSourceScreenModel.Listing
|
||||
import eu.kanade.tachiyomi.ui.category.CategoriesTab
|
||||
import eu.kanade.tachiyomi.ui.entries.manga.MangaScreen
|
||||
|
@ -60,6 +60,7 @@ import tachiyomi.core.util.lang.launchIO
|
|||
import tachiyomi.presentation.core.components.material.Divider
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.source.local.entries.manga.LocalMangaSource
|
||||
|
||||
data class BrowseMangaSourceScreen(
|
||||
private val sourceId: Long,
|
||||
|
@ -124,6 +125,7 @@ data class BrowseMangaSourceScreen(
|
|||
navigateUp = navigateUp,
|
||||
onWebViewClick = onWebViewClick,
|
||||
onHelpClick = onHelpClick,
|
||||
onSettingsClick = { navigator.push(MangaSourcePreferencesScreen(sourceId)) },
|
||||
onSearch = { screenModel.search(it) },
|
||||
)
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import androidx.paging.filter
|
|||
import androidx.paging.map
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import eu.kanade.core.prefs.asState
|
||||
import eu.kanade.core.preference.asState
|
||||
import eu.kanade.domain.entries.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.entries.manga.model.copyFrom
|
||||
import eu.kanade.domain.entries.manga.model.toDomainManga
|
||||
|
|
|
@ -63,7 +63,10 @@ fun SourceFilterMangaDialog(
|
|||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Button(onClick = onFilter) {
|
||||
Button(onClick = {
|
||||
onFilter()
|
||||
onDismissRequest()
|
||||
},) {
|
||||
Text(stringResource(R.string.action_filter))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import androidx.compose.ui.res.stringResource
|
|||
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
||||
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||
import eu.kanade.presentation.components.TabbedScreen
|
||||
import eu.kanade.presentation.extensions.RequestStoragePermission
|
||||
import eu.kanade.presentation.util.Tab
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.download.anime.animeDownloadTab
|
||||
|
|
|
@ -121,7 +121,7 @@ class AnimeCoverScreenModel(
|
|||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
context.contentResolver.openInputStream(data)?.use {
|
||||
try {
|
||||
anime.editCover(context, it, updateAnime, coverCache)
|
||||
anime.editCover(Injekt.get(), it, updateAnime, coverCache)
|
||||
notifyCoverUpdated(context)
|
||||
} catch (e: Exception) {
|
||||
notifyFailedCoverUpdate(context, e)
|
||||
|
|
|
@ -8,7 +8,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import eu.kanade.core.prefs.asState
|
||||
import eu.kanade.core.preference.asState
|
||||
import eu.kanade.core.util.addOrRemove
|
||||
import eu.kanade.domain.download.service.DownloadPreferences
|
||||
import eu.kanade.domain.entries.anime.interactor.UpdateAnime
|
||||
|
|
|
@ -121,7 +121,7 @@ class MangaCoverScreenModel(
|
|||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
context.contentResolver.openInputStream(data)?.use {
|
||||
try {
|
||||
manga.editCover(context, it, updateManga, coverCache)
|
||||
manga.editCover(Injekt.get(), it, updateManga, coverCache)
|
||||
notifyCoverUpdated(context)
|
||||
} catch (e: Exception) {
|
||||
notifyFailedCoverUpdate(context, e)
|
||||
|
|
|
@ -8,7 +8,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import eu.kanade.core.prefs.asState
|
||||
import eu.kanade.core.preference.asState
|
||||
import eu.kanade.core.util.addOrRemove
|
||||
import eu.kanade.domain.download.service.DownloadPreferences
|
||||
import eu.kanade.domain.entries.manga.interactor.UpdateManga
|
||||
|
|
|
@ -14,6 +14,7 @@ import cafe.adriel.voyager.navigator.Navigator
|
|||
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
||||
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||
import eu.kanade.presentation.components.TabbedScreen
|
||||
import eu.kanade.presentation.extensions.RequestStoragePermission
|
||||
import eu.kanade.presentation.util.Tab
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.history.anime.AnimeHistoryScreenModel
|
||||
|
|
|
@ -7,8 +7,8 @@ import androidx.compose.ui.util.fastAny
|
|||
import androidx.compose.ui.util.fastMap
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import eu.kanade.core.prefs.PreferenceMutableState
|
||||
import eu.kanade.core.prefs.asState
|
||||
import eu.kanade.core.preference.PreferenceMutableState
|
||||
import eu.kanade.core.preference.asState
|
||||
import eu.kanade.core.util.fastDistinctBy
|
||||
import eu.kanade.core.util.fastFilter
|
||||
import eu.kanade.core.util.fastFilterNot
|
||||
|
|
|
@ -7,8 +7,8 @@ import androidx.compose.ui.util.fastAny
|
|||
import androidx.compose.ui.util.fastMap
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import eu.kanade.core.prefs.PreferenceMutableState
|
||||
import eu.kanade.core.prefs.asState
|
||||
import eu.kanade.core.preference.PreferenceMutableState
|
||||
import eu.kanade.core.preference.asState
|
||||
import eu.kanade.core.util.fastDistinctBy
|
||||
import eu.kanade.core.util.fastFilter
|
||||
import eu.kanade.core.util.fastFilterNot
|
||||
|
|
|
@ -17,7 +17,7 @@ import cafe.adriel.voyager.navigator.Navigator
|
|||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
||||
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||
import eu.kanade.core.prefs.asState
|
||||
import eu.kanade.core.preference.asState
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
import eu.kanade.presentation.more.MoreScreen
|
||||
|
|
|
@ -509,7 +509,7 @@ class PlayerViewModel(
|
|||
|
||||
viewModelScope.launchNonCancellable {
|
||||
val result = try {
|
||||
anime.editCover(context, imageStream)
|
||||
anime.editCover(Injekt.get(), imageStream)
|
||||
if (anime.isLocal() || anime.favorite) {
|
||||
SetAsCoverResult.Success
|
||||
} else {
|
||||
|
|
|
@ -774,7 +774,7 @@ class ReaderViewModel(
|
|||
|
||||
viewModelScope.launchNonCancellable {
|
||||
val result = try {
|
||||
manga.editCover(context, stream())
|
||||
manga.editCover(Injekt.get(), stream())
|
||||
if (manga.isLocal() || manga.favorite) {
|
||||
SetAsCoverResult.Success
|
||||
} else {
|
||||
|
|
|
@ -6,7 +6,6 @@ import eu.kanade.tachiyomi.R
|
|||
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadProvider
|
||||
import eu.kanade.tachiyomi.source.MangaSource
|
||||
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
|
||||
import eu.kanade.tachiyomi.source.manga.MangaSourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
|
@ -14,6 +13,8 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
|||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.source.local.entries.manga.LocalMangaSource
|
||||
import tachiyomi.source.local.io.Format
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
|
@ -83,14 +84,14 @@ class ChapterLoader(
|
|||
source is HttpSource -> HttpPageLoader(chapter, source)
|
||||
source is LocalMangaSource -> source.getFormat(chapter.chapter).let { format ->
|
||||
when (format) {
|
||||
is LocalMangaSource.Format.Directory -> DirectoryPageLoader(format.file)
|
||||
is LocalMangaSource.Format.Zip -> ZipPageLoader(format.file)
|
||||
is LocalMangaSource.Format.Rar -> try {
|
||||
is Format.Directory -> DirectoryPageLoader(format.file)
|
||||
is Format.Zip -> ZipPageLoader(format.file)
|
||||
is Format.Rar -> try {
|
||||
RarPageLoader(format.file)
|
||||
} catch (e: UnsupportedRarV5Exception) {
|
||||
error(context.getString(R.string.loader_rar5_error))
|
||||
}
|
||||
is LocalMangaSource.Format.Epub -> EpubPageLoader(format.file)
|
||||
is Format.Epub -> EpubPageLoader(format.file)
|
||||
}
|
||||
}
|
||||
source is MangaSourceManager.StubMangaSource -> throw source.getSourceNotInstalledException()
|
||||
|
|
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.reader.loader
|
|||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import com.github.junrar.rarfile.FileHeader
|
|||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.PipedInputStream
|
||||
|
|
|
@ -4,7 +4,7 @@ import android.os.Build
|
|||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import java.io.File
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.zip.ZipFile
|
||||
|
|
|
@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
|||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
|
||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.MainScope
|
||||
|
@ -23,6 +22,7 @@ import kotlinx.coroutines.supervisorScope
|
|||
import tachiyomi.core.util.lang.launchIO
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.lang.withUIContext
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
|
|
|
@ -18,7 +18,6 @@ import eu.kanade.tachiyomi.ui.reader.model.StencilPage
|
|||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
|
||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.MainScope
|
||||
|
@ -29,6 +28,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine
|
|||
import tachiyomi.core.util.lang.launchIO
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.lang.withUIContext
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.InputStream
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import eu.kanade.core.prefs.asState
|
||||
import eu.kanade.core.preference.asState
|
||||
import eu.kanade.core.util.addOrRemove
|
||||
import eu.kanade.core.util.insertSeparators
|
||||
import eu.kanade.domain.download.service.DownloadPreferences
|
||||
|
|
|
@ -8,7 +8,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import eu.kanade.core.prefs.asState
|
||||
import eu.kanade.core.preference.asState
|
||||
import eu.kanade.core.util.addOrRemove
|
||||
import eu.kanade.core.util.insertSeparators
|
||||
import eu.kanade.domain.items.chapter.interactor.SetReadStatus
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package eu.kanade.tachiyomi.util
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.domain.download.service.DownloadPreferences
|
||||
import eu.kanade.domain.entries.anime.interactor.UpdateAnime
|
||||
import eu.kanade.domain.entries.anime.model.hasCustomCover
|
||||
|
@ -8,8 +7,8 @@ import eu.kanade.domain.entries.anime.model.isLocal
|
|||
import eu.kanade.domain.entries.anime.model.toSAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
|
||||
import eu.kanade.tachiyomi.source.anime.LocalAnimeSource
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.source.local.image.anime.LocalAnimeCoverManager
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.InputStream
|
||||
|
@ -77,13 +76,13 @@ fun Anime.shouldDownloadNewEpisodes(dbCategories: List<Long>, preferences: Downl
|
|||
}
|
||||
|
||||
suspend fun Anime.editCover(
|
||||
context: Context,
|
||||
coverManager: LocalAnimeCoverManager,
|
||||
stream: InputStream,
|
||||
updateAnime: UpdateAnime = Injekt.get(),
|
||||
coverCache: AnimeCoverCache = Injekt.get(),
|
||||
) {
|
||||
if (isLocal()) {
|
||||
LocalAnimeSource.updateCover(context, toSAnime(), stream)
|
||||
coverManager.update(toSAnime(), stream)
|
||||
updateAnime.awaitUpdateCoverLastModified(id)
|
||||
} else if (favorite) {
|
||||
coverCache.setCustomCoverToCache(this, stream)
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
package eu.kanade.tachiyomi.util
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.domain.download.service.DownloadPreferences
|
||||
import eu.kanade.domain.entries.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.entries.manga.model.hasCustomCover
|
||||
import eu.kanade.domain.entries.manga.model.isLocal
|
||||
import eu.kanade.domain.entries.manga.model.toSManga
|
||||
import eu.kanade.tachiyomi.data.cache.MangaCoverCache
|
||||
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.source.local.image.manga.LocalMangaCoverManager
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.InputStream
|
||||
|
@ -77,13 +76,13 @@ fun Manga.shouldDownloadNewChapters(dbCategories: List<Long>, preferences: Downl
|
|||
}
|
||||
|
||||
suspend fun Manga.editCover(
|
||||
context: Context,
|
||||
coverManager: LocalMangaCoverManager,
|
||||
stream: InputStream,
|
||||
updateManga: UpdateManga = Injekt.get(),
|
||||
coverCache: MangaCoverCache = Injekt.get(),
|
||||
) {
|
||||
if (isLocal()) {
|
||||
LocalMangaSource.updateCover(context, toSManga(), stream)
|
||||
coverManager.update(toSManga(), stream)
|
||||
updateManga.awaitUpdateCoverLastModified(id)
|
||||
} else if (favorite) {
|
||||
coverCache.setCustomCoverToCache(this, stream)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package eu.kanade.tachiyomi.util.preference
|
||||
|
||||
import android.widget.CompoundButton
|
||||
import eu.kanade.core.prefs.PreferenceMutableState
|
||||
import eu.kanade.core.preference.PreferenceMutableState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
|
|
@ -46,7 +46,6 @@ import tachiyomi.core.util.system.logcat
|
|||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
|
@ -113,9 +112,6 @@ fun Context.hasPermission(permission: String) = ContextCompat.checkSelfPermissio
|
|||
}
|
||||
}
|
||||
|
||||
val getDisplayMaxHeightInPx: Int
|
||||
get() = Resources.getSystem().displayMetrics.let { max(it.heightPixels, it.widthPixels) }
|
||||
|
||||
/**
|
||||
* Converts to px and takes into account LTR/RTL layout.
|
||||
*/
|
||||
|
|
1
core-metadata/.gitignore
vendored
Normal file
1
core-metadata/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
21
core-metadata/build.gradle.kts
Normal file
21
core-metadata/build.gradle.kts
Normal file
|
@ -0,0 +1,21 @@
|
|||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
kotlin("plugin.serialization")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "tachiyomi.core.metadata"
|
||||
|
||||
defaultConfig {
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":source-api"))
|
||||
|
||||
implementation(kotlinx.bundles.serialization)
|
||||
}
|
0
core-metadata/consumer-rules.pro
Normal file
0
core-metadata/consumer-rules.pro
Normal file
21
core-metadata/proguard-rules.pro
vendored
Normal file
21
core-metadata/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
2
core-metadata/src/main/AndroidManifest.xml
Normal file
2
core-metadata/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
|
@ -1,37 +1,13 @@
|
|||
package eu.kanade.domain.entries.manga.model
|
||||
package tachiyomi.core.metadata.comicinfo
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.Serializable
|
||||
import nl.adaptivity.xmlutil.serialization.XmlElement
|
||||
import nl.adaptivity.xmlutil.serialization.XmlSerialName
|
||||
import nl.adaptivity.xmlutil.serialization.XmlValue
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.items.chapter.model.Chapter
|
||||
|
||||
const val COMIC_INFO_FILE = "ComicInfo.xml"
|
||||
|
||||
/**
|
||||
* Creates a ComicInfo instance based on the manga and chapter metadata.
|
||||
*/
|
||||
fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String) = ComicInfo(
|
||||
title = ComicInfo.Title(chapter.name),
|
||||
series = ComicInfo.Series(manga.title),
|
||||
web = ComicInfo.Web(chapterUrl),
|
||||
summary = manga.description?.let { ComicInfo.Summary(it) },
|
||||
writer = manga.author?.let { ComicInfo.Writer(it) },
|
||||
penciller = manga.artist?.let { ComicInfo.Penciller(it) },
|
||||
translator = chapter.scanlator?.let { ComicInfo.Translator(it) },
|
||||
genre = manga.genre?.let { ComicInfo.Genre(it.joinToString()) },
|
||||
publishingStatus = ComicInfo.PublishingStatusTachiyomi(
|
||||
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
|
||||
),
|
||||
inker = null,
|
||||
colorist = null,
|
||||
letterer = null,
|
||||
coverArtist = null,
|
||||
tags = null,
|
||||
)
|
||||
|
||||
fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
|
||||
comicInfo.series?.let { title = it.value }
|
||||
comicInfo.writer?.let { author = it.value }
|
||||
|
@ -149,7 +125,7 @@ data class ComicInfo(
|
|||
data class PublishingStatusTachiyomi(@XmlValue(true) val value: String = "")
|
||||
}
|
||||
|
||||
private enum class ComicInfoPublishingStatus(
|
||||
enum class ComicInfoPublishingStatus(
|
||||
val comicInfoValue: String,
|
||||
val sMangaModelValue: Int,
|
||||
) {
|
|
@ -0,0 +1,13 @@
|
|||
package tachiyomi.core.metadata.tachiyomi
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class AnimeDetails(
|
||||
val title: String? = null,
|
||||
val author: String? = null,
|
||||
val artist: String? = null,
|
||||
val description: String? = null,
|
||||
val genre: List<String>? = null,
|
||||
val status: Int? = null,
|
||||
)
|
|
@ -0,0 +1,13 @@
|
|||
package tachiyomi.core.metadata.tachiyomi
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class MangaDetails(
|
||||
val title: String? = null,
|
||||
val author: String? = null,
|
||||
val artist: String? = null,
|
||||
val description: String? = null,
|
||||
val genre: List<String>? = null,
|
||||
val status: Int? = null,
|
||||
)
|
|
@ -27,12 +27,24 @@ dependencies {
|
|||
api(libs.okhttp.dnsoverhttps)
|
||||
api(libs.okio)
|
||||
|
||||
implementation(libs.image.decoder)
|
||||
|
||||
implementation(libs.unifile)
|
||||
|
||||
api(kotlinx.coroutines.core)
|
||||
api(kotlinx.serialization.json)
|
||||
api(kotlinx.serialization.json.okio)
|
||||
|
||||
api(libs.preferencektx)
|
||||
|
||||
implementation(libs.jsoup)
|
||||
|
||||
// Sort
|
||||
implementation(libs.natural.comparator)
|
||||
|
||||
// JavaScript engine
|
||||
implementation(libs.bundles.js.engine)
|
||||
|
||||
// FFmpeg-kit
|
||||
implementation(libs.ffmpeg.kit)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.network
|
|||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
||||
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
|
||||
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
|
@ -33,6 +34,7 @@ class NetworkHelper(
|
|||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.callTimeout(2, TimeUnit.MINUTES)
|
||||
.addInterceptor(UncaughtExceptionInterceptor())
|
||||
.addInterceptor(userAgentInterceptor)
|
||||
|
||||
if (preferences.verboseLogging().get()) {
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Catches any uncaught exceptions from later in the chain and rethrows as a non-fatal
|
||||
* IOException to avoid catastrophic failure.
|
||||
*
|
||||
* This should be the first interceptor in the client.
|
||||
*
|
||||
* See https://square.github.io/okhttp/4.x/okhttp/okhttp3/-interceptor/
|
||||
*/
|
||||
class UncaughtExceptionInterceptor : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
return try {
|
||||
chain.proceed(chain.request())
|
||||
} catch (e: Exception) {
|
||||
throw IOException(e)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +1,11 @@
|
|||
package eu.kanade.tachiyomi.util.storage
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.media.MediaScannerConnection
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.os.StatFs
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.util.lang.Hash
|
||||
import java.io.File
|
||||
|
@ -117,16 +113,5 @@ object DiskUtil {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches request for [Manifest.permission.WRITE_EXTERNAL_STORAGE] permission
|
||||
*/
|
||||
@Composable
|
||||
fun RequestStoragePermission() {
|
||||
val permissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
LaunchedEffect(Unit) {
|
||||
permissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
|
||||
const val NOMEDIA_FILE = ".nomedia"
|
||||
}
|
|
@ -1,15 +1,10 @@
|
|||
package eu.kanade.tachiyomi.util.storage
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
|
@ -49,58 +44,6 @@ class EpubFile(file: File) : Closeable {
|
|||
return zip.getEntry(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills manga metadata using this epub file's metadata.
|
||||
*/
|
||||
fun fillMangaMetadata(manga: SManga) {
|
||||
val ref = getPackageHref()
|
||||
val doc = getPackageDocument(ref)
|
||||
|
||||
val creator = doc.getElementsByTag("dc:creator").first()
|
||||
val description = doc.getElementsByTag("dc:description").first()
|
||||
|
||||
manga.author = creator?.text()
|
||||
manga.description = description?.text()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills chapter metadata using this epub file's metadata.
|
||||
*/
|
||||
fun fillChapterMetadata(chapter: SChapter) {
|
||||
val ref = getPackageHref()
|
||||
val doc = getPackageDocument(ref)
|
||||
|
||||
val title = doc.getElementsByTag("dc:title").first()
|
||||
val publisher = doc.getElementsByTag("dc:publisher").first()
|
||||
val creator = doc.getElementsByTag("dc:creator").first()
|
||||
var date = doc.getElementsByTag("dc:date").first()
|
||||
if (date == null) {
|
||||
date = doc.select("meta[property=dcterms:modified]").first()
|
||||
}
|
||||
|
||||
if (title != null) {
|
||||
chapter.name = title.text()
|
||||
}
|
||||
|
||||
if (publisher != null) {
|
||||
chapter.scanlator = publisher.text()
|
||||
} else if (creator != null) {
|
||||
chapter.scanlator = creator.text()
|
||||
}
|
||||
|
||||
if (date != null) {
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault())
|
||||
try {
|
||||
val parsedDate = dateFormat.parse(date.text())
|
||||
if (parsedDate != null) {
|
||||
chapter.date_upload = parsedDate.time
|
||||
}
|
||||
} catch (e: ParseException) {
|
||||
// Empty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path of all the images found in the epub file.
|
||||
*/
|
||||
|
@ -114,7 +57,7 @@ class EpubFile(file: File) : Closeable {
|
|||
/**
|
||||
* Returns the path to the package document.
|
||||
*/
|
||||
private fun getPackageHref(): String {
|
||||
fun getPackageHref(): String {
|
||||
val meta = zip.getEntry(resolveZipPath("META-INF", "container.xml"))
|
||||
if (meta != null) {
|
||||
val metaDoc = zip.getInputStream(meta).use { Jsoup.parse(it, null, "") }
|
||||
|
@ -129,7 +72,7 @@ class EpubFile(file: File) : Closeable {
|
|||
/**
|
||||
* Returns the package document where all the files are listed.
|
||||
*/
|
||||
private fun getPackageDocument(ref: String): Document {
|
||||
fun getPackageDocument(ref: String): Document {
|
||||
val entry = zip.getEntry(ref)
|
||||
return zip.getInputStream(entry).use { Jsoup.parse(it, null, "") }
|
||||
}
|
||||
|
@ -137,7 +80,7 @@ class EpubFile(file: File) : Closeable {
|
|||
/**
|
||||
* Returns all the pages from the epub.
|
||||
*/
|
||||
private fun getPagesFromDocument(document: Document): List<String> {
|
||||
fun getPagesFromDocument(document: Document): List<String> {
|
||||
val pages = document.select("manifest > item")
|
||||
.filter { node -> "application/xhtml+xml" == node.attr("media-type") }
|
||||
.associateBy { it.attr("id") }
|
|
@ -0,0 +1,19 @@
|
|||
package eu.kanade.tachiyomi.util.storage
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import com.arthenica.ffmpegkit.FFmpegKitConfig
|
||||
import java.io.File
|
||||
|
||||
fun String.toFFmpegString(context: Context): String {
|
||||
return File(this).getUriCompat(context).toFFmpegString(context)
|
||||
}
|
||||
|
||||
fun Uri.toFFmpegString(context: Context): String {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && this.scheme == "content") {
|
||||
FFmpegKitConfig.getSafParameter(context, this, "rw")
|
||||
} else {
|
||||
this.path!!
|
||||
}.replace("\"", "\\\"")
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package eu.kanade.tachiyomi.util.storage
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toUri
|
||||
import java.io.File
|
||||
|
||||
val Context.cacheImageDir: File
|
||||
get() = File(cacheDir, "shared_image")
|
||||
|
||||
/**
|
||||
* Returns the uri of a file
|
||||
*
|
||||
* @param context context of application
|
||||
*/
|
||||
fun File.getUriCompat(context: Context): Uri {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
FileProvider.getUriForFile(context, "xyz.jmir.tachiyomi.mi.debug" + ".provider", this)
|
||||
} else {
|
||||
this.toUri()
|
||||
}
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
package eu.kanade.tachiyomi.util.system
|
||||
package tachiyomi.core.util.system
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.BitmapRegionDecoder
|
||||
|
@ -22,7 +23,6 @@ import androidx.core.graphics.green
|
|||
import androidx.core.graphics.red
|
||||
import com.hippo.unifile.UniFile
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.decoder.Format
|
||||
import tachiyomi.decoder.ImageDecoder
|
||||
import java.io.BufferedInputStream
|
||||
|
@ -31,6 +31,7 @@ import java.io.ByteArrayOutputStream
|
|||
import java.io.InputStream
|
||||
import java.net.URLConnection
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
object ImageUtil {
|
||||
|
@ -587,3 +588,6 @@ object ImageUtil {
|
|||
"image/jxl" to "jxl",
|
||||
)
|
||||
}
|
||||
|
||||
val getDisplayMaxHeightInPx: Int
|
||||
get() = Resources.getSystem().displayMetrics.let { max(it.heightPixels, it.widthPixels) }
|
|
@ -1,7 +1,7 @@
|
|||
package eu.kanade.data.source.anime
|
||||
package tachiyomi.data.source.anime
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import eu.kanade.domain.source.anime.model.AnimeSourcePagingSourceType
|
||||
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||
|
@ -10,6 +10,8 @@ import tachiyomi.core.util.lang.awaitSingle
|
|||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.domain.items.episode.model.NoEpisodesException
|
||||
|
||||
typealias AnimeSourcePagingSourceType = PagingSource<Long, SAnime>
|
||||
|
||||
abstract class AnimeSourcePagingSource(
|
||||
protected val source: AnimeCatalogueSource,
|
||||
) : AnimeSourcePagingSourceType() {
|
|
@ -1,7 +1,7 @@
|
|||
package eu.kanade.data.source.manga
|
||||
package tachiyomi.data.source.manga
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import eu.kanade.domain.source.manga.model.SourcePagingSourceType
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
|
@ -10,6 +10,8 @@ import tachiyomi.core.util.lang.awaitSingle
|
|||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.domain.items.chapter.model.NoChaptersException
|
||||
|
||||
typealias SourcePagingSourceType = PagingSource<Long, SManga>
|
||||
|
||||
abstract class SourcePagingSource(
|
||||
protected val source: CatalogueSource,
|
||||
) : SourcePagingSourceType() {
|
|
@ -753,7 +753,7 @@
|
|||
<string name="action_search_hint">بحث…</string>
|
||||
<string name="skipped_reason_not_always_update">تم التخطي لأن السلسلة لا تتطلب تحديثات</string>
|
||||
<string name="crash_screen_description">%s واجه خطأ غير متوقع. نقترح عليك أخد لقطة شاشة لهذه الرسالة، وتفريغ سجلات التعطل ، ثم مشاركتها في قناة الدعم الخاصة بنا على Discord.</string>
|
||||
<string name="crash_screen_title">أخ!</string>
|
||||
<string name="crash_screen_title">عفوًا!</string>
|
||||
<string name="crash_screen_restart_application">أعد تشغيل التطبيق</string>
|
||||
<string name="pref_general_summary">لغة التطبيق، الإشعارات</string>
|
||||
<string name="pref_appearance_summary">مظهر، التاريخ والوقت</string>
|
||||
|
@ -777,4 +777,13 @@
|
|||
<string name="track_error">%1$s خطأ: %2$s</string>
|
||||
<string name="information_required_plain">*مطلوب</string>
|
||||
<string name="action_copy_to_clipboard">نسخ إلى الحافظة</string>
|
||||
<plurals name="download_amount">
|
||||
<item quantity="zero">الفصل التالي</item>
|
||||
<item quantity="one">الفصل التالي</item>
|
||||
<item quantity="two">%d فصول تالية</item>
|
||||
<item quantity="few">%d فصول تالية</item>
|
||||
<item quantity="many">%d فصل تالي</item>
|
||||
<item quantity="other">%d فصل تالي</item>
|
||||
</plurals>
|
||||
<string name="pref_hide_in_library_items">إخفاء الإدخالات الموجودة بالفعل في المكتبة</string>
|
||||
</resources>
|
|
@ -329,7 +329,7 @@
|
|||
<string name="theme_system">Según ajustes del sistema</string>
|
||||
<string name="pref_manage_notifications">Gestionar notificaciones</string>
|
||||
<string name="pref_category_security">Seguridad y privacidad</string>
|
||||
<string name="lock_with_biometrics">Requiere utilizar desbloqueo por biometría</string>
|
||||
<string name="lock_with_biometrics">Requiere el uso de desbloqueo por biometría</string>
|
||||
<string name="lock_when_idle">Bloquear por inactividad</string>
|
||||
<string name="lock_always">Siempre</string>
|
||||
<string name="lock_never">Nunca</string>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<string name="pref_remove_after_read">Pagkatapos basahin, kusang burahin</string>
|
||||
<string name="pref_remove_bookmarked_chapters">Burahin din ang may pananda</string>
|
||||
<string name="pref_remove_exclude_categories_manga">Kategoryang di-kasama</string>
|
||||
<string name="pref_auto_update_manga_sync">I-update ang progress pagkabasa</string>
|
||||
<string name="pref_auto_update_manga_sync">I-update ang progress pagkatapos basahin</string>
|
||||
<string name="pref_clear_chapter_cache">Linisin ang cache ng kabanata</string>
|
||||
<string name="pref_auto_clear_chapter_cache">Linisin ang cache ng kabanata pagkasara</string>
|
||||
<string name="pref_clear_manga_database">Linisin ang database</string>
|
||||
|
|
|
@ -736,4 +736,10 @@
|
|||
<string name="hour_short">%dh</string>
|
||||
<string name="minute_short">%dm</string>
|
||||
<string name="seconds_short">%ds</string>
|
||||
<string name="action_copy_to_clipboard">Kopyahin sa clipboard</string>
|
||||
<string name="pref_hide_in_library_items">Itago ang mga entry na nasa aklatan na</string>
|
||||
<plurals name="download_amount">
|
||||
<item quantity="one">Susunod na kabanata</item>
|
||||
<item quantity="other">Susunod na %d (mga) kabanata</item>
|
||||
</plurals>
|
||||
</resources>
|
|
@ -2,7 +2,7 @@
|
|||
<resources>
|
||||
<string name="name">이름</string>
|
||||
<string name="manga">만화</string>
|
||||
<string name="chapters">회차</string>
|
||||
<string name="chapters">범주</string>
|
||||
<string name="track">동기화</string>
|
||||
<string name="label_backup">백업 및 복원</string>
|
||||
<string name="action_settings">설정</string>
|
||||
|
@ -65,7 +65,7 @@
|
|||
<string name="pref_category_advanced">고급</string>
|
||||
<string name="pref_category_about">정보</string>
|
||||
<string name="pref_library_columns">서재 정렬</string>
|
||||
<string name="label_migration">소스 이동</string>
|
||||
<string name="label_migration">데이터 이전</string>
|
||||
<string name="label_extensions">확장기능</string>
|
||||
<string name="label_extension_info">확장기능 정보</string>
|
||||
<string name="action_global_search">전체 검색</string>
|
||||
|
@ -147,7 +147,7 @@
|
|||
<string name="pref_enable_acra">오류 보고서 전송</string>
|
||||
<string name="pref_acra_summary">버그를 수정하는데 도움이 됩니다. 개인 정보는 전송되지 않습니다</string>
|
||||
<string name="login_title">%1$s 으로 로그인</string>
|
||||
<string name="username">유저네임</string>
|
||||
<string name="username">사용자명</string>
|
||||
<string name="password">비밀번호</string>
|
||||
<string name="login">로그인</string>
|
||||
<string name="login_success">로그인 성공</string>
|
||||
|
@ -156,7 +156,7 @@
|
|||
<string name="no_more_results">더이상 결과 없음</string>
|
||||
<string name="action_global_search_hint">없음 검색…</string>
|
||||
<string name="latest">최신</string>
|
||||
<string name="ongoing">연재중</string>
|
||||
<string name="ongoing">연재</string>
|
||||
<string name="unknown">알 수 없음</string>
|
||||
<string name="remove_from_library">서재에서 제거</string>
|
||||
<string name="manga_added_library">서재에 추가되었습니다</string>
|
||||
|
@ -214,7 +214,7 @@
|
|||
<string name="download_notifier_unknown_error">다운로드 중에 예기치 않은 오류가 발생하였습니다</string>
|
||||
<string name="action_display_download_badge">다운로드한 챕터</string>
|
||||
<string name="pref_update_only_non_completed">연재가 끝남</string>
|
||||
<string name="default_category_summary">항상 물어보기</string>
|
||||
<string name="default_category">기본 범주</string>
|
||||
<string name="pref_create_backup_summ">현재 서재를 나중에 복구하는 데 사용 가능</string>
|
||||
<string name="pref_reader_navigation">네비게이션</string>
|
||||
<string name="pref_page_transitions">페이지 전환 효과 표시</string>
|
||||
|
@ -255,7 +255,7 @@
|
|||
<string name="track_type">종류</string>
|
||||
<string name="error_category_exists">같은 이름을 가진 카테고리가 이미 존재합니다!</string>
|
||||
<string name="migration_dialog_what_to_include">포함할 데이터를 선택하세요</string>
|
||||
<string name="migrate">이동</string>
|
||||
<string name="migrate">데이터 이전</string>
|
||||
<string name="copy">복사</string>
|
||||
<string name="download_queue_error">다운로드 실패. 다운로드 메뉴에서 다시 시도할 수 있습니다</string>
|
||||
<string name="notification_first_add_to_library">이 행동을 하기 전 서재에 항목을 추가해주세요</string>
|
||||
|
@ -300,7 +300,7 @@
|
|||
<string name="pref_incognito_mode">시크릿 모드</string>
|
||||
<string name="updated_version">v%1$s 으로 업데이트 됨</string>
|
||||
<string name="check_for_updates">업데이트 확인</string>
|
||||
<string name="licenses">오픈 소스 라이센스</string>
|
||||
<string name="licenses">오픈 소스 라이선스</string>
|
||||
<string name="battery_optimization_setting_activity_not_found">디바이스 설정을 열 수 없습니다</string>
|
||||
<string name="pref_disable_battery_optimization">배터리 최적화 끄기</string>
|
||||
<string name="restore_miui_warning">MIUI 최적화가 꺼져 있을 경우 백업/복원 기능이 정상 작동하지 않을 수 있습니다.</string>
|
||||
|
@ -471,9 +471,9 @@
|
|||
<string name="action_global_search_query">\"%1$s\"를 전체 검색합니다</string>
|
||||
<string name="local_source_help_guide">로컬 저장소 사용법</string>
|
||||
<string name="no_pinned_sources">핀 설정된 소스가 없습니다</string>
|
||||
<string name="local_invalid_format">잘못된 회차 포맷</string>
|
||||
<string name="local_invalid_format">잘못된 챕터형식</string>
|
||||
<string name="unknown_status">알 수 없는 상태</string>
|
||||
<string name="on_hiatus">휴재중</string>
|
||||
<string name="on_hiatus">휴재</string>
|
||||
<string name="manga_info_expand">상세정보 표시</string>
|
||||
<string name="manga_info_collapse">상세정보 숨김</string>
|
||||
<string name="clipboard_copy_error">클립보드로 복사에 실패하였습니다</string>
|
||||
|
|
|
@ -125,7 +125,7 @@
|
|||
<string name="pref_library_update_restriction">स्वचालित अपडेटहरु उपकरण प्रतिबन्धहरू</string>
|
||||
<string name="update_weekly">साप्ताहिक</string>
|
||||
<string name="update_48hour">प्रत्येक २ दिन</string>
|
||||
<string name="update_24hour">सँधै</string>
|
||||
<string name="update_24hour">दैनिक</string>
|
||||
<string name="update_12hour">प्रत्येक १२ घण्टा</string>
|
||||
<string name="update_6hour">प्रत्येक ६ घण्टा</string>
|
||||
<string name="update_never">अफ</string>
|
||||
|
@ -146,8 +146,8 @@
|
|||
<string name="secure_screen_summary">एपहरू स्विच गर्दा \"सुरक्षित स्क्रिनले\" एप सामग्रीहरू लुकाउँछ र स्क्रिनसटहरू रोक्छ</string>
|
||||
<string name="secure_screen">स्क्रिन सुरक्षित गर्नुहोस्</string>
|
||||
<plurals name="lock_after_mins">
|
||||
<item quantity="one">%1$s मिनट पछि</item>
|
||||
<item quantity="other">%1$s मिनट पछि</item>
|
||||
<item quantity="one">%1$s मिनेट पछि</item>
|
||||
<item quantity="other">%1$s मिनेट पछि</item>
|
||||
</plurals>
|
||||
<string name="lock_never">कहिल्यै हैन</string>
|
||||
<string name="lock_always">सधैं</string>
|
||||
|
@ -183,13 +183,13 @@
|
|||
<string name="label_default">पूर्वनिर्धारित</string>
|
||||
<string name="pref_tablet_ui_mode">ट्याब्लेट UI</string>
|
||||
<string name="battery_optimization_setting_activity_not_found">यन्त्रको सेटिङहरू खोल्न सकिएन</string>
|
||||
<string name="pref_incognito_mode">इन्कोग्निटो मोड</string>
|
||||
<string name="pref_incognito_mode">गुप्त मोड</string>
|
||||
<string name="label_downloaded_only">डाउनलोड गरिएको मात्र</string>
|
||||
<string name="pref_acra_summary">कुनै पनि बगहरू ठीक गर्न मद्दत गर्दछ। कुनै संवेदनशील डाटा पठाइने छैन</string>
|
||||
<string name="pref_enable_acra">क्रेश रिपोर्टहरू पठाउनुहोस्</string>
|
||||
<string name="updated_version">v%1$s मा अपडेट गरियो</string>
|
||||
<string name="check_for_updates">अपडेटका लागि चेक गर्नुहोस्</string>
|
||||
<string name="decode_image_error">चित्र लोड गर्न सकिएन</string>
|
||||
<string name="decode_image_error">छवि लोड गर्न सकिएन</string>
|
||||
<string name="download_queue_error">अध्यायहरू डाउनलोड गर्न सकिएन। तपाईं डाउनलोड कतारमा फेरि प्रयास गर्न सक्नुहुन्छ</string>
|
||||
<string name="ext_uninstall">स्थापना रद्द गर्नुहोस्</string>
|
||||
<string name="third_to_last">तेस्रो अन्तिम अध्याय</string>
|
||||
|
@ -453,8 +453,8 @@
|
|||
<string name="local_filter_order_by">द्वारा अर्डर गर्नुहोस्</string>
|
||||
<string name="date">मिति</string>
|
||||
<string name="login_title">%1$s मा लग इन गर्नुहोस्</string>
|
||||
<string name="pref_refresh_library_covers">पुस्तकालय का माङ्गा कभरहरू ताजा गर्नुहोस्</string>
|
||||
<string name="notification_incognito_text">इन्कोग्निटो मोड असक्षम गर्नुहोस्</string>
|
||||
<string name="pref_refresh_library_covers">पुस्तकालय का कभरहरू ताजा गर्नुहोस्</string>
|
||||
<string name="notification_incognito_text">गुप्त मोड असक्षम गर्नुहोस्</string>
|
||||
<string name="unknown_error">अज्ञात एरर</string>
|
||||
<string name="downloaded_chapters">डाउनलोड गरिएका अध्यायहरू</string>
|
||||
<string name="chapter_not_found">अध्याय फेला परेन</string>
|
||||
|
@ -540,7 +540,7 @@
|
|||
<string name="confirm_delete_chapters">के तपाईँले चयन गर्नुभएको अध्यायहरू हटाउन चाहनुहुन्छ\?</string>
|
||||
<string name="chapter_settings">अध्याय सेटिङ</string>
|
||||
<string name="set_chapter_settings_as_default">पूर्वनिर्धारित रूपमा सेट गर्नुहोस्</string>
|
||||
<string name="confirm_set_image_as_cover">यो चित्र कभरको रूपमा राख्न चाहनुहुन्छ\?</string>
|
||||
<string name="confirm_set_image_as_cover">यो छवि कभरको रूपमा राख्न चाहनुहुन्छ\?</string>
|
||||
<string name="download_all">सबै</string>
|
||||
<string name="download_unread">नपढिएको</string>
|
||||
<string name="cover_saved">कभर बचत भयो</string>
|
||||
|
|
|
@ -313,7 +313,7 @@
|
|||
<string name="lock_never">Nigdy</string>
|
||||
<string name="lock_always">Zawsze</string>
|
||||
<string name="lock_with_biometrics">Wymagaj odblokowania</string>
|
||||
<string name="pref_category_security">Bezpieczeństwo</string>
|
||||
<string name="pref_category_security">Bezpieczeństwo i prywatność</string>
|
||||
<string name="pref_manage_notifications">Zarządzaj powiadomieniami</string>
|
||||
<string name="theme_system">Systemowy</string>
|
||||
<string name="theme_dark">Włącz</string>
|
||||
|
@ -348,7 +348,7 @@
|
|||
<string name="restore_in_progress">Kopia zapasowa jest w trakcie przywracania</string>
|
||||
<string name="backup_in_progress">Kopia zapasowa jest już w trakcie tworzenia</string>
|
||||
<string name="restore_duration">%02d min, %02d s</string>
|
||||
<string name="pref_search_pinned_sources_only">Pokazuj tylko przypięte źródła</string>
|
||||
<string name="pref_search_pinned_sources_only">Wyszukaj tylko w przypiętych źródłach</string>
|
||||
<string name="pref_webtoon_side_padding">Marginesy boczne</string>
|
||||
<string name="gray_background">Szary</string>
|
||||
<string name="pref_true_color_summary">Ogranicza banding, ale może mieć wpływ na wydajność</string>
|
||||
|
@ -733,4 +733,12 @@
|
|||
<string name="pref_worker_info">Informacje o procesach</string>
|
||||
<string name="track_error">Błąd %1$s: %2$s</string>
|
||||
<string name="information_required_plain">*wymagane</string>
|
||||
<string name="pref_hide_in_library_items">Ukryj pozycje znajdujące się już w bibliotece</string>
|
||||
<string name="action_copy_to_clipboard">Kopiuj do schowka</string>
|
||||
<plurals name="download_amount">
|
||||
<item quantity="one">Następny rozdział</item>
|
||||
<item quantity="few">Następne %d rozdziały</item>
|
||||
<item quantity="many">Następne %d rozdziałów</item>
|
||||
<item quantity="other">Następne %d rozdziałów</item>
|
||||
</plurals>
|
||||
</resources>
|
|
@ -326,7 +326,7 @@
|
|||
<string name="theme_dark">Ligado</string>
|
||||
<string name="theme_system">Seguir o do sistema</string>
|
||||
<string name="pref_manage_notifications">Gerir notificações</string>
|
||||
<string name="pref_category_security">Segurança</string>
|
||||
<string name="pref_category_security">Segurança e privacidade.</string>
|
||||
<string name="lock_with_biometrics">Requerer desbloqueio</string>
|
||||
<string name="lock_when_idle">Bloquear quando inativo</string>
|
||||
<string name="lock_always">Sempre</string>
|
||||
|
@ -417,7 +417,7 @@
|
|||
<string name="licenses">Licenças de código aberto</string>
|
||||
<string name="downloaded_only_summary">Filtra todos os itens nasua biblioteca</string>
|
||||
<string name="restore_duration">%02d min, %02d seg</string>
|
||||
<string name="pref_search_pinned_sources_only">Apenas incluir fontes fixadas</string>
|
||||
<string name="pref_search_pinned_sources_only">Pesquise apenas fontes fixadas na pesquisa global</string>
|
||||
<plurals name="download_queue_summary">
|
||||
<item quantity="one">%1$s restante</item>
|
||||
<item quantity="many">%1$s restantes</item>
|
||||
|
@ -793,4 +793,11 @@
|
|||
<string name="fdroid_warning">Compilações do F-Droid não são suportadas oficialmente.
|
||||
\nToque para saber mais.</string>
|
||||
<string name="information_no_entries_found">Nenhum item encontrado nesta categoria</string>
|
||||
<string name="action_copy_to_clipboard">Copiar para a área de transferência</string>
|
||||
<string name="pref_hide_in_library_items">Ocultar entradas existentes na biblioteca</string>
|
||||
<plurals name="download_amount">
|
||||
<item quantity="one">Próximo capítulo</item>
|
||||
<item quantity="many">Próximos %d capítulos</item>
|
||||
<item quantity="other">Próximos %d capítulos</item>
|
||||
</plurals>
|
||||
</resources>
|
|
@ -782,4 +782,8 @@
|
|||
<string name="hour_short">%do</string>
|
||||
<string name="minute_short">%dm</string>
|
||||
<string name="information_no_manga_category">Kategorija është bosh</string>
|
||||
<string name="enhanced_services_not_installed">Në dispozicion, por burimi nuk është i instaluar: %s</string>
|
||||
<string name="pref_skip_dupe_chapters">Kapërceni kapitujt e kopjuar</string>
|
||||
<string name="pref_hide_in_library_items">Fshih hyrjet tashmë në bibliotekë</string>
|
||||
<string name="action_copy_to_clipboard">Kopjo në kujtesën e fragmenteve</string>
|
||||
</resources>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue