diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 135795374..5201f0a51 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/eu/kanade/core/prefs/CheckboxState.kt b/app/src/main/java/eu/kanade/core/preference/CheckboxState.kt similarity index 75% rename from app/src/main/java/eu/kanade/core/prefs/CheckboxState.kt rename to app/src/main/java/eu/kanade/core/preference/CheckboxState.kt index c3b54ecab..fa98bcbda 100644 --- a/app/src/main/java/eu/kanade/core/prefs/CheckboxState.kt +++ b/app/src/main/java/eu/kanade/core/preference/CheckboxState.kt @@ -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 CheckboxState.TriState.asState(): ToggleableState { +fun CheckboxState.TriState.asToggleableState(): ToggleableState { return when (this) { is CheckboxState.TriState.Exclude -> ToggleableState.Indeterminate is CheckboxState.TriState.Include -> ToggleableState.On diff --git a/app/src/main/java/eu/kanade/core/prefs/PreferenceMutableState.kt b/app/src/main/java/eu/kanade/core/preference/PreferenceMutableState.kt similarity index 96% rename from app/src/main/java/eu/kanade/core/prefs/PreferenceMutableState.kt rename to app/src/main/java/eu/kanade/core/preference/PreferenceMutableState.kt index c33b24091..2c641ccc0 100644 --- a/app/src/main/java/eu/kanade/core/prefs/PreferenceMutableState.kt +++ b/app/src/main/java/eu/kanade/core/preference/PreferenceMutableState.kt @@ -1,4 +1,4 @@ -package eu.kanade.core.prefs +package eu.kanade.core.preference import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf diff --git a/app/src/main/java/eu/kanade/data/source/anime/AnimeSourceRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/source/anime/AnimeSourceRepositoryImpl.kt index 030c96053..6897066c6 100644 --- a/app/src/main/java/eu/kanade/data/source/anime/AnimeSourceRepositoryImpl.kt +++ b/app/src/main/java/eu/kanade/data/source/anime/AnimeSourceRepositoryImpl.kt @@ -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 } } diff --git a/app/src/main/java/eu/kanade/data/source/manga/MangaSourceRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/source/manga/MangaSourceRepositoryImpl.kt index ff4652513..9e41dabac 100644 --- a/app/src/main/java/eu/kanade/data/source/manga/MangaSourceRepositoryImpl.kt +++ b/app/src/main/java/eu/kanade/data/source/manga/MangaSourceRepositoryImpl.kt @@ -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 } } diff --git a/app/src/main/java/eu/kanade/domain/entries/manga/model/Manga.kt b/app/src/main/java/eu/kanade/domain/entries/manga/model/Manga.kt index d6fb4a013..df638d71e 100644 --- a/app/src/main/java/eu/kanade/domain/entries/manga/model/Manga.kt +++ b/app/src/main/java/eu/kanade/domain/entries/manga/model/Manga.kt @@ -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, +) diff --git a/app/src/main/java/eu/kanade/domain/source/anime/interactor/GetRemoteAnime.kt b/app/src/main/java/eu/kanade/domain/source/anime/interactor/GetRemoteAnime.kt index 19be78610..0c9e89646 100644 --- a/app/src/main/java/eu/kanade/domain/source/anime/interactor/GetRemoteAnime.kt +++ b/app/src/main/java/eu/kanade/domain/source/anime/interactor/GetRemoteAnime.kt @@ -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, diff --git a/app/src/main/java/eu/kanade/domain/source/anime/model/AnimeSourcePagingSourceType.kt b/app/src/main/java/eu/kanade/domain/source/anime/model/AnimeSourcePagingSourceType.kt deleted file mode 100644 index e3074b5d7..000000000 --- a/app/src/main/java/eu/kanade/domain/source/anime/model/AnimeSourcePagingSourceType.kt +++ /dev/null @@ -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 diff --git a/app/src/main/java/eu/kanade/domain/source/anime/repository/AnimeSourceRepository.kt b/app/src/main/java/eu/kanade/domain/source/anime/repository/AnimeSourceRepository.kt index 12a02a723..a9893031b 100644 --- a/app/src/main/java/eu/kanade/domain/source/anime/repository/AnimeSourceRepository.kt +++ b/app/src/main/java/eu/kanade/domain/source/anime/repository/AnimeSourceRepository.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/domain/source/manga/interactor/GetEnabledMangaSources.kt b/app/src/main/java/eu/kanade/domain/source/manga/interactor/GetEnabledMangaSources.kt index 1d0f42986..f6aa92c8f 100644 --- a/app/src/main/java/eu/kanade/domain/source/manga/interactor/GetEnabledMangaSources.kt +++ b/app/src/main/java/eu/kanade/domain/source/manga/interactor/GetEnabledMangaSources.kt @@ -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, diff --git a/app/src/main/java/eu/kanade/domain/source/manga/interactor/GetRemoteManga.kt b/app/src/main/java/eu/kanade/domain/source/manga/interactor/GetRemoteManga.kt index bd783d9da..20efcc962 100644 --- a/app/src/main/java/eu/kanade/domain/source/manga/interactor/GetRemoteManga.kt +++ b/app/src/main/java/eu/kanade/domain/source/manga/interactor/GetRemoteManga.kt @@ -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, diff --git a/app/src/main/java/eu/kanade/domain/source/manga/model/MangaSourcePagingSourceType.kt b/app/src/main/java/eu/kanade/domain/source/manga/model/MangaSourcePagingSourceType.kt deleted file mode 100644 index 3450acf66..000000000 --- a/app/src/main/java/eu/kanade/domain/source/manga/model/MangaSourcePagingSourceType.kt +++ /dev/null @@ -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 diff --git a/app/src/main/java/eu/kanade/domain/source/manga/repository/MangaSourceRepository.kt b/app/src/main/java/eu/kanade/domain/source/manga/repository/MangaSourceRepository.kt index 1887e1948..43715a89a 100644 --- a/app/src/main/java/eu/kanade/domain/source/manga/repository/MangaSourceRepository.kt +++ b/app/src/main/java/eu/kanade/domain/source/manga/repository/MangaSourceRepository.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceToolbar.kt index f08307534..b99cd8532 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/anime/components/BrowseAnimeSourceToolbar.kt @@ -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 }, diff --git a/app/src/main/java/eu/kanade/presentation/browse/manga/BrowseMangaSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/manga/BrowseMangaSourceScreen.kt index 8875dec1f..90c9d60ba 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/manga/BrowseMangaSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/manga/BrowseMangaSourceScreen.kt @@ -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( diff --git a/app/src/main/java/eu/kanade/presentation/browse/manga/MangaSourcesScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/manga/MangaSourcesScreen.kt index 7ae6c0340..8c4b5276e 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/manga/MangaSourcesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/manga/MangaSourcesScreen.kt @@ -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( diff --git a/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaIcons.kt b/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaIcons.kt index 38ec6999f..ec6cf3562 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaIcons.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaIcons.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaSourceToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaSourceToolbar.kt index 9f011fb06..f9fb4bca9 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaSourceToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/manga/components/BrowseMangaSourceToolbar.kt @@ -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 }, diff --git a/app/src/main/java/eu/kanade/presentation/category/ChangeCategoryDialog.kt b/app/src/main/java/eu/kanade/presentation/category/ChangeCategoryDialog.kt index 91b75cf4b..68f073f9e 100644 --- a/app/src/main/java/eu/kanade/presentation/category/ChangeCategoryDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/category/ChangeCategoryDialog.kt @@ -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) }, ) } diff --git a/app/src/main/java/eu/kanade/presentation/extensions/DiskUtil.kt b/app/src/main/java/eu/kanade/presentation/extensions/DiskUtil.kt new file mode 100644 index 000000000..f0581e099 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/extensions/DiskUtil.kt @@ -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() + } +} diff --git a/app/src/main/java/eu/kanade/presentation/library/DeleteLibraryEntryDialog.kt b/app/src/main/java/eu/kanade/presentation/library/DeleteLibraryEntryDialog.kt index cec3c14b3..5dbe97cc0 100644 --- a/app/src/main/java/eu/kanade/presentation/library/DeleteLibraryEntryDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/library/DeleteLibraryEntryDialog.kt @@ -67,9 +67,11 @@ fun DeleteLibraryEntryDialog( list.forEach { state -> val onCheck = { val index = list.indexOf(state) - val mutableList = list.toMutableList() - mutableList[index] = state.next() as CheckboxState.State - list = mutableList.toList() + if (index != -1) { + val mutableList = list.toMutableList() + mutableList[index] = state.next() as CheckboxState.State + list = mutableList.toList() + } } Row( diff --git a/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibraryContent.kt b/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibraryContent.kt index 9ef342454..02e607d7f 100644 --- a/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibraryContent.kt +++ b/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibraryContent.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibraryPager.kt b/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibraryPager.kt index 4cff6a41b..b4a3289a9 100644 --- a/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibraryPager.kt +++ b/app/src/main/java/eu/kanade/presentation/library/anime/AnimeLibraryPager.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibraryContent.kt b/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibraryContent.kt index 031691d79..4a49534b3 100644 --- a/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibraryContent.kt +++ b/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibraryContent.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibraryPager.kt b/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibraryPager.kt index fbb2b4f90..7b717c1df 100644 --- a/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibraryPager.kt +++ b/app/src/main/java/eu/kanade/presentation/library/manga/MangaLibraryPager.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupScreen.kt index c198cf0bb..2728abc4c 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupScreen.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt index a38580450..84c15f178 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt @@ -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 { AndroidLocalMangaSourceFileSystem(app) } + addSingletonFactory { AndroidLocalMangaCoverManager(app, get()) } + + addSingletonFactory { AndroidLocalAnimeSourceFileSystem(app) } + addSingletonFactory { AndroidLocalAnimeCoverManager(app, get()) } + addSingletonFactory { ExternalIntents() } // Asynchronously init expensive components for a faster cold start diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt index 42ef7fe34..e0a5ffe8e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt @@ -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 /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadManager.kt index c8c31469d..34cd5958e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadManager.kt @@ -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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloader.kt index 8d25d4993..2a03cdc06 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloader.kt @@ -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() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloadManager.kt index d8d712c00..8939b0c8a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloadManager.kt @@ -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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloader.kt index 38b56b7b2..bbbdcb214 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloader.kt @@ -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() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt index b8a2befab..c0536e431 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/anime/AnimeSourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/anime/AnimeSourceManager.kt index ebca9fe29..e8e6828bc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/anime/AnimeSourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/anime/AnimeSourceManager.kt @@ -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(mapOf(LocalAnimeSource.ID to LocalAnimeSource(context))) + val mutableMap = ConcurrentHashMap( + mapOf( + LocalAnimeSource.ID to LocalAnimeSource( + context, + Injekt.get(), + Injekt.get(), + ), + ), + ) extensions.forEach { extension -> extension.sources.forEach { mutableMap[it.id] = it diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/anime/LocalAnimeSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/anime/LocalAnimeSource.kt index be38dd1e1..ecce28f23 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/anime/LocalAnimeSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/anime/LocalAnimeSource.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/manga/LocalMangaSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/manga/LocalMangaSource.kt index d8053f38c..b442ab711 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/manga/LocalMangaSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/manga/LocalMangaSource.kt @@ -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 { - 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(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, 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(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? = null, - val status: Int? = null, - ) - - // Chapters - override suspend fun getChapterList(manga: SManga): List { - 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 { - 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 { - return getBaseDirectories(context) - // Get all the files inside all baseDir - .flatMap { it.listFiles().orEmpty().toList() } - } - - private fun getMangaDir(mangaUrl: String, baseDirsFile: Sequence): File? { - return baseDirsFile - // Get the first mangaDir or null - .firstOrNull { it.isDirectory && it.name == mangaUrl } - } - - private fun getMangaDirsFiles(mangaUrl: String, baseDirsFile: Sequence): Sequence { - 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? { - 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") diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/manga/MangaSourceExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/source/manga/MangaSourceExtensions.kt index 67204ffa1..3fde23c9c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/manga/MangaSourceExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/manga/MangaSourceExtensions.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/manga/MangaSourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/manga/MangaSourceManager.kt index 88ec4359e..52964dd0a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/manga/MangaSourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/manga/MangaSourceManager.kt @@ -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(mapOf(LocalMangaSource.ID to LocalMangaSource(context))) + val mutableMap = ConcurrentHashMap( + mapOf( + LocalMangaSource.ID to LocalMangaSource( + context, + Injekt.get(), + Injekt.get(), + ), + ), + ) extensions.forEach { extension -> extension.sources.forEach { mutableMap[it.id] = it diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt index 0a576aafa..75f13ac1c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/extension/details/SourcePreferencesScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/extension/details/SourcePreferencesScreen.kt index c1532c3fc..9265a7c3d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/extension/details/SourcePreferencesScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/extension/details/SourcePreferencesScreen.kt @@ -58,7 +58,7 @@ class SourcePreferencesScreen(val sourceId: Long) : Screen() { Scaffold( topBar = { TopAppBar( - title = { Text(text = Injekt.get().get(sourceId)!!.toString()) }, + title = { Text(text = Injekt.get().getOrStub(sourceId).toString()) }, navigationIcon = { IconButton(onClick = navigator::pop) { Icon( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/BrowseAnimeSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/BrowseAnimeSourceScreen.kt index 18a7253fc..0a165965d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/BrowseAnimeSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/BrowseAnimeSourceScreen.kt @@ -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) }, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/BrowseAnimeSourceScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/BrowseAnimeSourceScreenModel.kt index a074ffaa3..aff1a9a16 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/BrowseAnimeSourceScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/BrowseAnimeSourceScreenModel.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/SourceFilterAnimeDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/SourceFilterAnimeDialog.kt index 96e9a21db..6f328a41c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/SourceFilterAnimeDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/anime/source/browse/SourceFilterAnimeDialog.kt @@ -63,7 +63,10 @@ fun SourceFilterAnimeDialog( Spacer(modifier = Modifier.weight(1f)) - Button(onClick = onFilter) { + Button(onClick = { + onFilter() + onDismissRequest() + },) { Text(stringResource(R.string.action_filter)) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/extension/details/MangaSourcePreferencesScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/extension/details/MangaSourcePreferencesScreen.kt index 7ed66e58c..8f18aa538 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/extension/details/MangaSourcePreferencesScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/extension/details/MangaSourcePreferencesScreen.kt @@ -58,7 +58,7 @@ class MangaSourcePreferencesScreen(val sourceId: Long) : Screen() { Scaffold( topBar = { TopAppBar( - title = { Text(text = Injekt.get().get(sourceId)!!.toString()) }, + title = { Text(text = Injekt.get().getOrStub(sourceId).toString()) }, navigationIcon = { IconButton(onClick = navigator::pop) { Icon( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/migration/search/MangaSourceSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/migration/search/MangaSourceSearchScreen.kt index f60b7c94d..c97b1f833 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/migration/search/MangaSourceSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/migration/search/MangaSourceSearchScreen.kt @@ -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, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/BrowseMangaSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/BrowseMangaSourceScreen.kt index f9b7f9b49..e6ba8479b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/BrowseMangaSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/BrowseMangaSourceScreen.kt @@ -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) }, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/BrowseMangaSourceScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/BrowseMangaSourceScreenModel.kt index aecdd4a48..ade3aa8bd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/BrowseMangaSourceScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/BrowseMangaSourceScreenModel.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/SourceFilterMangaDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/SourceFilterMangaDialog.kt index d4bcc4688..567e899ac 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/SourceFilterMangaDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/manga/source/browse/SourceFilterMangaDialog.kt @@ -63,7 +63,10 @@ fun SourceFilterMangaDialog( Spacer(modifier = Modifier.weight(1f)) - Button(onClick = onFilter) { + Button(onClick = { + onFilter() + onDismissRequest() + },) { Text(stringResource(R.string.action_filter)) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadsTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadsTab.kt index 761e4e864..e90daf052 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadsTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadsTab.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeCoverScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeCoverScreenModel.kt index 3aa9d945d..681730098 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeCoverScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeCoverScreenModel.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreenModel.kt index b3273fd45..4faf35039 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/anime/AnimeScreenModel.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaCoverScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaCoverScreenModel.kt index 576674db8..ae67d3dfb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaCoverScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaCoverScreenModel.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaScreenModel.kt index ec75b935d..91927ad3f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/entries/manga/MangaScreenModel.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoriesTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoriesTab.kt index 18cef597d..1c639a13c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoriesTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoriesTab.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibraryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibraryScreenModel.kt index 5a40400f7..95a7ae7ff 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibraryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/anime/AnimeLibraryScreenModel.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibraryScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibraryScreenModel.kt index 1bb5d03f1..780efdc19 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibraryScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/manga/MangaLibraryScreenModel.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt index b0ea6e0ea..4be867231 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt index 93dd833e5..3ee376c39 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/player/PlayerViewModel.kt @@ -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 { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index c7bd6d8e0..2e4f4b9dc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -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 { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt index 49901f1a0..d04807550 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt @@ -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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt index cff8e38a6..1b25c273e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt index 58debe3b1..6cd804dda 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt index 543241b30..5022fb45e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt index 6cc0e4b41..801e03c4b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt index fda21b8a2..6ee030ce9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/anime/AnimeUpdatesScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/anime/AnimeUpdatesScreenModel.kt index 13fd12e00..b1b88be00 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/anime/AnimeUpdatesScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/anime/AnimeUpdatesScreenModel.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/manga/MangaUpdatesScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/manga/MangaUpdatesScreenModel.kt index 4a837dc04..cb545bce1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/manga/MangaUpdatesScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/manga/MangaUpdatesScreenModel.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/AnimeExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/AnimeExtensions.kt index 7db369a55..a1fdc1a7d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/AnimeExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/AnimeExtensions.kt @@ -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, 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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt index 97f9eb887..b1e6f5ab4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt @@ -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, 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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/preference/PreferenceExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/preference/PreferenceExtensions.kt index 35a8ae9c0..e12349630 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/preference/PreferenceExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/preference/PreferenceExtensions.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt index c2548e628..b8f590c57 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt @@ -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. */ diff --git a/core-metadata/.gitignore b/core-metadata/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/core-metadata/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core-metadata/build.gradle.kts b/core-metadata/build.gradle.kts new file mode 100644 index 000000000..51db46b91 --- /dev/null +++ b/core-metadata/build.gradle.kts @@ -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) +} diff --git a/core-metadata/consumer-rules.pro b/core-metadata/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/core-metadata/proguard-rules.pro b/core-metadata/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/core-metadata/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/core-metadata/src/main/AndroidManifest.xml b/core-metadata/src/main/AndroidManifest.xml new file mode 100644 index 000000000..568741e54 --- /dev/null +++ b/core-metadata/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/domain/entries/manga/model/ComicInfo.kt b/core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt similarity index 81% rename from app/src/main/java/eu/kanade/domain/entries/manga/model/ComicInfo.kt rename to core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt index acb99ca01..80c0e9d4c 100644 --- a/app/src/main/java/eu/kanade/domain/entries/manga/model/ComicInfo.kt +++ b/core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt @@ -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, ) { diff --git a/core-metadata/src/main/java/tachiyomi/core/metadata/tachiyomi/AnimeDetails.kt b/core-metadata/src/main/java/tachiyomi/core/metadata/tachiyomi/AnimeDetails.kt new file mode 100644 index 000000000..414d4873b --- /dev/null +++ b/core-metadata/src/main/java/tachiyomi/core/metadata/tachiyomi/AnimeDetails.kt @@ -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? = null, + val status: Int? = null, +) diff --git a/core-metadata/src/main/java/tachiyomi/core/metadata/tachiyomi/MangaDetails.kt b/core-metadata/src/main/java/tachiyomi/core/metadata/tachiyomi/MangaDetails.kt new file mode 100644 index 000000000..7768986e0 --- /dev/null +++ b/core-metadata/src/main/java/tachiyomi/core/metadata/tachiyomi/MangaDetails.kt @@ -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? = null, + val status: Int? = null, +) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 30a598517..97a275f4d 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -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) } diff --git a/core/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt b/core/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt index 9cc103589..efbdacb9d 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -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()) { diff --git a/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt b/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt new file mode 100644 index 000000000..2362b78c6 --- /dev/null +++ b/core/src/main/java/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt @@ -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) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/lang/Hash.kt b/core/src/main/java/eu/kanade/tachiyomi/util/lang/Hash.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/util/lang/Hash.kt rename to core/src/main/java/eu/kanade/tachiyomi/util/lang/Hash.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt b/core/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt rename to core/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt b/core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt similarity index 86% rename from app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt rename to core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt index 4a6322080..8e3141a27 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt @@ -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" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt b/core/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt similarity index 69% rename from app/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt rename to core/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt index 517ba818f..8838a199c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt @@ -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 { + fun getPagesFromDocument(document: Document): List { val pages = document.select("manifest > item") .filter { node -> "application/xhtml+xml" == node.attr("media-type") } .associateBy { it.attr("id") } diff --git a/core/src/main/java/eu/kanade/tachiyomi/util/storage/FFmpegUtils.kt b/core/src/main/java/eu/kanade/tachiyomi/util/storage/FFmpegUtils.kt new file mode 100644 index 000000000..1acde9075 --- /dev/null +++ b/core/src/main/java/eu/kanade/tachiyomi/util/storage/FFmpegUtils.kt @@ -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("\"", "\\\"") +} diff --git a/core/src/main/java/eu/kanade/tachiyomi/util/storage/FileExtensions.kt b/core/src/main/java/eu/kanade/tachiyomi/util/storage/FileExtensions.kt new file mode 100644 index 000000000..8ff1ae16a --- /dev/null +++ b/core/src/main/java/eu/kanade/tachiyomi/util/storage/FileExtensions.kt @@ -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() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt b/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt similarity index 99% rename from app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt rename to core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt index e4eebbce6..638f33726 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt +++ b/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt @@ -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) } diff --git a/app/src/main/java/eu/kanade/data/source/anime/AnimeSourcePagingSource.kt b/data/src/main/java/tachiyomi/data/source/anime/AnimeSourcePagingSource.kt similarity index 94% rename from app/src/main/java/eu/kanade/data/source/anime/AnimeSourcePagingSource.kt rename to data/src/main/java/tachiyomi/data/source/anime/AnimeSourcePagingSource.kt index cc6b581c4..30d21de96 100644 --- a/app/src/main/java/eu/kanade/data/source/anime/AnimeSourcePagingSource.kt +++ b/data/src/main/java/tachiyomi/data/source/anime/AnimeSourcePagingSource.kt @@ -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 + abstract class AnimeSourcePagingSource( protected val source: AnimeCatalogueSource, ) : AnimeSourcePagingSourceType() { diff --git a/app/src/main/java/eu/kanade/data/source/manga/MangaSourcePagingSource.kt b/data/src/main/java/tachiyomi/data/source/manga/MangaSourcePagingSource.kt similarity index 94% rename from app/src/main/java/eu/kanade/data/source/manga/MangaSourcePagingSource.kt rename to data/src/main/java/tachiyomi/data/source/manga/MangaSourcePagingSource.kt index 30aa7b87e..92dd72030 100644 --- a/app/src/main/java/eu/kanade/data/source/manga/MangaSourcePagingSource.kt +++ b/data/src/main/java/tachiyomi/data/source/manga/MangaSourcePagingSource.kt @@ -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 + abstract class SourcePagingSource( protected val source: CatalogueSource, ) : SourcePagingSourceType() { diff --git a/i18n/src/main/res/values-ar/strings.xml b/i18n/src/main/res/values-ar/strings.xml index bca1861cb..a9d733530 100644 --- a/i18n/src/main/res/values-ar/strings.xml +++ b/i18n/src/main/res/values-ar/strings.xml @@ -753,7 +753,7 @@ بحث… تم التخطي لأن السلسلة لا تتطلب تحديثات %s واجه خطأ غير متوقع. نقترح عليك أخد لقطة شاشة لهذه الرسالة، وتفريغ سجلات التعطل ، ثم مشاركتها في قناة الدعم الخاصة بنا على Discord. - أخ! + عفوًا! أعد تشغيل التطبيق لغة التطبيق، الإشعارات مظهر، التاريخ والوقت @@ -777,4 +777,13 @@ %1$s خطأ: %2$s *مطلوب نسخ إلى الحافظة + + الفصل التالي + الفصل التالي + %d فصول تالية + %d فصول تالية + %d فصل تالي + %d فصل تالي + + إخفاء الإدخالات الموجودة بالفعل في المكتبة \ No newline at end of file diff --git a/i18n/src/main/res/values-es/strings.xml b/i18n/src/main/res/values-es/strings.xml index b2fa7e9df..6942d21dd 100644 --- a/i18n/src/main/res/values-es/strings.xml +++ b/i18n/src/main/res/values-es/strings.xml @@ -329,7 +329,7 @@ Según ajustes del sistema Gestionar notificaciones Seguridad y privacidad - Requiere utilizar desbloqueo por biometría + Requiere el uso de desbloqueo por biometría Bloquear por inactividad Siempre Nunca diff --git a/i18n/src/main/res/values-fil/strings-aniyomi.xml b/i18n/src/main/res/values-fil/strings-aniyomi.xml index 4a44675c2..d89a5077e 100644 --- a/i18n/src/main/res/values-fil/strings-aniyomi.xml +++ b/i18n/src/main/res/values-fil/strings-aniyomi.xml @@ -19,7 +19,7 @@ Pagkatapos basahin, kusang burahin Burahin din ang may pananda Kategoryang di-kasama - I-update ang progress pagkabasa + I-update ang progress pagkatapos basahin Linisin ang cache ng kabanata Linisin ang cache ng kabanata pagkasara Linisin ang database diff --git a/i18n/src/main/res/values-fil/strings.xml b/i18n/src/main/res/values-fil/strings.xml index 27c6d7181..5c0c208d5 100644 --- a/i18n/src/main/res/values-fil/strings.xml +++ b/i18n/src/main/res/values-fil/strings.xml @@ -736,4 +736,10 @@ %dh %dm %ds + Kopyahin sa clipboard + Itago ang mga entry na nasa aklatan na + + Susunod na kabanata + Susunod na %d (mga) kabanata + \ No newline at end of file diff --git a/i18n/src/main/res/values-ko/strings.xml b/i18n/src/main/res/values-ko/strings.xml index 32215b408..fd8b8fc8d 100644 --- a/i18n/src/main/res/values-ko/strings.xml +++ b/i18n/src/main/res/values-ko/strings.xml @@ -2,7 +2,7 @@ 이름 만화 - 회차 + 범주 동기화 백업 및 복원 설정 @@ -65,7 +65,7 @@ 고급 정보 서재 정렬 - 소스 이동 + 데이터 이전 확장기능 확장기능 정보 전체 검색 @@ -147,7 +147,7 @@ 오류 보고서 전송 버그를 수정하는데 도움이 됩니다. 개인 정보는 전송되지 않습니다 %1$s 으로 로그인 - 유저네임 + 사용자명 비밀번호 로그인 로그인 성공 @@ -156,7 +156,7 @@ 더이상 결과 없음 없음 검색… 최신 - 연재중 + 연재 알 수 없음 서재에서 제거 서재에 추가되었습니다 @@ -214,7 +214,7 @@ 다운로드 중에 예기치 않은 오류가 발생하였습니다 다운로드한 챕터 연재가 끝남 - 항상 물어보기 + 기본 범주 현재 서재를 나중에 복구하는 데 사용 가능 네비게이션 페이지 전환 효과 표시 @@ -255,7 +255,7 @@ 종류 같은 이름을 가진 카테고리가 이미 존재합니다! 포함할 데이터를 선택하세요 - 이동 + 데이터 이전 복사 다운로드 실패. 다운로드 메뉴에서 다시 시도할 수 있습니다 이 행동을 하기 전 서재에 항목을 추가해주세요 @@ -300,7 +300,7 @@ 시크릿 모드 v%1$s 으로 업데이트 됨 업데이트 확인 - 오픈 소스 라이센스 + 오픈 소스 라이선스 디바이스 설정을 열 수 없습니다 배터리 최적화 끄기 MIUI 최적화가 꺼져 있을 경우 백업/복원 기능이 정상 작동하지 않을 수 있습니다. @@ -471,9 +471,9 @@ \"%1$s\"를 전체 검색합니다 로컬 저장소 사용법 핀 설정된 소스가 없습니다 - 잘못된 회차 포맷 + 잘못된 챕터형식 알 수 없는 상태 - 휴재중 + 휴재 상세정보 표시 상세정보 숨김 클립보드로 복사에 실패하였습니다 diff --git a/i18n/src/main/res/values-ne/strings.xml b/i18n/src/main/res/values-ne/strings.xml index 6da6d0f54..196531fbb 100644 --- a/i18n/src/main/res/values-ne/strings.xml +++ b/i18n/src/main/res/values-ne/strings.xml @@ -125,7 +125,7 @@ स्वचालित अपडेटहरु उपकरण प्रतिबन्धहरू साप्ताहिक प्रत्येक २ दिन - सँधै + दैनिक प्रत्येक १२ घण्टा प्रत्येक ६ घण्टा अफ @@ -146,8 +146,8 @@ एपहरू स्विच गर्दा \"सुरक्षित स्क्रिनले\" एप सामग्रीहरू लुकाउँछ र स्क्रिनसटहरू रोक्छ स्क्रिन सुरक्षित गर्नुहोस् - %1$s मिनट पछि - %1$s मिनट पछि + %1$s मिनेट पछि + %1$s मिनेट पछि कहिल्यै हैन सधैं @@ -183,13 +183,13 @@ पूर्वनिर्धारित ट्याब्लेट UI यन्त्रको सेटिङहरू खोल्न सकिएन - इन्कोग्निटो मोड + गुप्त मोड डाउनलोड गरिएको मात्र कुनै पनि बगहरू ठीक गर्न मद्दत गर्दछ। कुनै संवेदनशील डाटा पठाइने छैन क्रेश रिपोर्टहरू पठाउनुहोस् v%1$s मा अपडेट गरियो अपडेटका लागि चेक गर्नुहोस् - चित्र लोड गर्न सकिएन + छवि लोड गर्न सकिएन अध्यायहरू डाउनलोड गर्न सकिएन। तपाईं डाउनलोड कतारमा फेरि प्रयास गर्न सक्नुहुन्छ स्थापना रद्द गर्नुहोस् तेस्रो अन्तिम अध्याय @@ -453,8 +453,8 @@ द्वारा अर्डर गर्नुहोस् मिति %1$s मा लग इन गर्नुहोस् - पुस्तकालय का माङ्गा कभरहरू ताजा गर्नुहोस् - इन्कोग्निटो मोड असक्षम गर्नुहोस् + पुस्तकालय का कभरहरू ताजा गर्नुहोस् + गुप्त मोड असक्षम गर्नुहोस् अज्ञात एरर डाउनलोड गरिएका अध्यायहरू अध्याय फेला परेन @@ -540,7 +540,7 @@ के तपाईँले चयन गर्नुभएको अध्यायहरू हटाउन चाहनुहुन्छ\? अध्याय सेटिङ पूर्वनिर्धारित रूपमा सेट गर्नुहोस् - यो चित्र कभरको रूपमा राख्न चाहनुहुन्छ\? + यो छवि कभरको रूपमा राख्न चाहनुहुन्छ\? सबै नपढिएको कभर बचत भयो diff --git a/i18n/src/main/res/values-pl/strings.xml b/i18n/src/main/res/values-pl/strings.xml index 502260cf8..ce939a56a 100644 --- a/i18n/src/main/res/values-pl/strings.xml +++ b/i18n/src/main/res/values-pl/strings.xml @@ -313,7 +313,7 @@ Nigdy Zawsze Wymagaj odblokowania - Bezpieczeństwo + Bezpieczeństwo i prywatność Zarządzaj powiadomieniami Systemowy Włącz @@ -348,7 +348,7 @@ Kopia zapasowa jest w trakcie przywracania Kopia zapasowa jest już w trakcie tworzenia %02d min, %02d s - Pokazuj tylko przypięte źródła + Wyszukaj tylko w przypiętych źródłach Marginesy boczne Szary Ogranicza banding, ale może mieć wpływ na wydajność @@ -733,4 +733,12 @@ Informacje o procesach Błąd %1$s: %2$s *wymagane + Ukryj pozycje znajdujące się już w bibliotece + Kopiuj do schowka + + Następny rozdział + Następne %d rozdziały + Następne %d rozdziałów + Następne %d rozdziałów + \ No newline at end of file diff --git a/i18n/src/main/res/values-pt/strings.xml b/i18n/src/main/res/values-pt/strings.xml index cfba1cf90..a935e46bf 100644 --- a/i18n/src/main/res/values-pt/strings.xml +++ b/i18n/src/main/res/values-pt/strings.xml @@ -326,7 +326,7 @@ Ligado Seguir o do sistema Gerir notificações - Segurança + Segurança e privacidade. Requerer desbloqueio Bloquear quando inativo Sempre @@ -417,7 +417,7 @@ Licenças de código aberto Filtra todos os itens nasua biblioteca %02d min, %02d seg - Apenas incluir fontes fixadas + Pesquise apenas fontes fixadas na pesquisa global %1$s restante %1$s restantes @@ -793,4 +793,11 @@ Compilações do F-Droid não são suportadas oficialmente. \nToque para saber mais. Nenhum item encontrado nesta categoria + Copiar para a área de transferência + Ocultar entradas existentes na biblioteca + + Próximo capítulo + Próximos %d capítulos + Próximos %d capítulos + \ No newline at end of file diff --git a/i18n/src/main/res/values-sq/strings.xml b/i18n/src/main/res/values-sq/strings.xml index 631756efc..05a5ebd42 100644 --- a/i18n/src/main/res/values-sq/strings.xml +++ b/i18n/src/main/res/values-sq/strings.xml @@ -782,4 +782,8 @@ %do %dm Kategorija është bosh + Në dispozicion, por burimi nuk është i instaluar: %s + Kapërceni kapitujt e kopjuar + Fshih hyrjet tashmë në bibliotekë + Kopjo në kujtesën e fragmenteve \ No newline at end of file diff --git a/i18n/src/main/res/values-zh-rCN/strings.xml b/i18n/src/main/res/values-zh-rCN/strings.xml index e40a34209..9cfef0351 100644 --- a/i18n/src/main/res/values-zh-rCN/strings.xml +++ b/i18n/src/main/res/values-zh-rCN/strings.xml @@ -64,7 +64,7 @@ 正在加载… 已下载章节 应用不可用 - 通用 + 常规 阅读 下载 进度记录 diff --git a/i18n/src/main/res/values-zh-rTW/strings.xml b/i18n/src/main/res/values-zh-rTW/strings.xml index 013445620..7cea73b70 100644 --- a/i18n/src/main/res/values-zh-rTW/strings.xml +++ b/i18n/src/main/res/values-zh-rTW/strings.xml @@ -273,7 +273,7 @@ 倒數第三閱畢的章節 倒數第四閱畢的章節 倒數第五閱畢的章節 - 不看了 + 已拋棄 擱置中 找不到結果 請選擇檔案來源 @@ -506,7 +506,7 @@ 輕觸以檢視詳細資訊 不再支援此 Android 版本 - 無法複製至剪貼簿 + 無法複製到剪貼簿 即將還原備份中的資料。 \n \n隨後請安裝所有遺失的擴充套件並重新登入各歷程平台。 @@ -717,4 +717,9 @@ \n仍要新增嗎? %1$s 發生錯誤:%2$s *必填 + 隱藏已在書櫃的作品 + 複製到剪貼簿 + + 後續 %d 章 + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index f6e028f05..aa3440ac4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -45,3 +45,5 @@ include(":data") include(":domain") include(":presentation-widget") include(":presentation-core") +include(":source-local") +include(":core-metadata") diff --git a/source-local/.gitignore b/source-local/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/source-local/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/source-local/build.gradle.kts b/source-local/build.gradle.kts new file mode 100644 index 000000000..09b835b64 --- /dev/null +++ b/source-local/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + id("com.android.library") + kotlin("android") +} + +android { + namespace = "tachiyomi.source.local" + + defaultConfig { + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } +} + +dependencies { + + implementation(project(":source-api")) + implementation(project(":core")) + implementation(project(":core-metadata")) + + // Move ChapterRecognition to separate module? + implementation(project(":domain")) + + implementation(kotlinx.bundles.serialization) + + implementation(libs.unifile) + implementation(libs.junrar) + + // mpv-android + implementation(libs.aniyomi.mpv) + // FFmpeg-kit + implementation(libs.ffmpeg.kit) +} diff --git a/source-local/consumer-rules.pro b/source-local/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/source-local/proguard-rules.pro b/source-local/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/source-local/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/source-local/src/main/AndroidManifest.xml b/source-local/src/main/AndroidManifest.xml new file mode 100644 index 000000000..568741e54 --- /dev/null +++ b/source-local/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/source-local/src/main/java/tachiyomi/source/local/entries/anime/LocalAnimeSource.kt b/source-local/src/main/java/tachiyomi/source/local/entries/anime/LocalAnimeSource.kt new file mode 100644 index 000000000..4901425a7 --- /dev/null +++ b/source-local/src/main/java/tachiyomi/source/local/entries/anime/LocalAnimeSource.kt @@ -0,0 +1,235 @@ +package tachiyomi.source.local.entries.anime + +import android.content.Context +import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource +import eu.kanade.tachiyomi.animesource.model.AnimeFilterList +import eu.kanade.tachiyomi.animesource.model.AnimesPage +import eu.kanade.tachiyomi.animesource.model.SAnime +import eu.kanade.tachiyomi.animesource.model.SEpisode +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 kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import logcat.LogPriority +import rx.Observable +import tachiyomi.core.metadata.tachiyomi.AnimeDetails +import tachiyomi.core.util.lang.withIOContext +import tachiyomi.core.util.system.logcat +import tachiyomi.domain.items.episode.service.EpisodeRecognition +import tachiyomi.source.local.R +import tachiyomi.source.local.filter.anime.AnimeOrderBy +import tachiyomi.source.local.image.anime.LocalAnimeCoverManager +import tachiyomi.source.local.io.Archive +import tachiyomi.source.local.io.anime.LocalAnimeSourceFileSystem +import uy.kohesive.injekt.injectLazy +import java.io.File +import java.util.concurrent.TimeUnit + +class LocalAnimeSource( + private val context: Context, + private val fileSystem: LocalAnimeSourceFileSystem, + private val coverManager: LocalAnimeCoverManager, +) : AnimeCatalogueSource, UnmeteredSource { + + private val json: Json by injectLazy() + + private val POPULAR_FILTERS = AnimeFilterList(AnimeOrderBy.Popular(context)) + private val LATEST_FILTERS = AnimeFilterList(AnimeOrderBy.Latest(context)) + + override val name = context.getString(R.string.local_anime_source) + + override val id: Long = ID + + override val lang = "other" + + override fun toString() = name + + override val supportsLatest = true + + // Browse related + override fun fetchPopularAnime(page: Int) = fetchSearchAnime(page, "", POPULAR_FILTERS) + + override fun fetchLatestUpdates(page: Int) = fetchSearchAnime(page, "", LATEST_FILTERS) + + override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable { + val baseDirsFiles = fileSystem.getFilesInBaseDirectories() + val lastModifiedLimit by lazy { if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L } + + var animeDirs = baseDirsFiles + // Filter out files that are hidden and is not a folder + .filter { it.isDirectory && !it.name.startsWith('.') } + .distinctBy { it.name } + .filter { // Filter by query or last modified + if (lastModifiedLimit == 0L) { + it.name.contains(query, ignoreCase = true) + } else { + it.lastModified() >= lastModifiedLimit + } + } + + filters.forEach { filter -> + when (filter) { + is AnimeOrderBy.Popular -> { + animeDirs = if (filter.state!!.ascending) { + animeDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) + } else { + animeDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name }) + } + } + is AnimeOrderBy.Latest -> { + animeDirs = if (filter.state!!.ascending) { + animeDirs.sortedBy(File::lastModified) + } else { + animeDirs.sortedByDescending(File::lastModified) + } + } + + else -> { + /* Do nothing */ + } + } + } + + // Transform animeDirs to list of SAnime + val animes = animeDirs.map { animeDir -> + SAnime.create().apply { + title = animeDir.name + url = animeDir.name + + // Try to find the cover + coverManager.find(animeDir.name) + ?.takeIf(File::exists) + ?.let { thumbnail_url = it.absolutePath } + } + } + + // Fetch episodes of all the anime + animes.forEach { anime -> + runBlocking { + val episodes = getEpisodeList(anime) + if (episodes.isNotEmpty()) { + val episode = episodes.last() + // Copy the cover from the first episode found if not available + if (anime.thumbnail_url == null) { + try { + updateCoverFromVideo(episode, anime) + } catch (e: Exception) { + logcat(LogPriority.ERROR) { "Couldn't extract thumbnail from video." } + } + } + } + } + } + + return Observable.just(AnimesPage(animes.toList(), false)) + } + + // Anime details related + override suspend fun getAnimeDetails(anime: SAnime): SAnime = withIOContext { + coverManager.find(anime.url)?.let { + anime.thumbnail_url = it.absolutePath + } + + val animeDirFiles = fileSystem.getFilesInAnimeDirectory(anime.url).toList() + + animeDirFiles + .firstOrNull { it.extension == "json" } + ?.let { file -> + json.decodeFromStream(file.inputStream()).run { + title?.let { anime.title = it } + author?.let { anime.author = it } + artist?.let { anime.artist = it } + description?.let { anime.description = it } + genre?.let { anime.genre = it.joinToString() } + status?.let { anime.status = it } + } + } + + return@withIOContext anime + } + + // Episodes + override suspend fun getEpisodeList(anime: SAnime): List { + return fileSystem.getFilesInAnimeDirectory(anime.url) + // Only keep supported formats + .filter { it.isDirectory || Archive.isSupported(it) } + .map { episodeFile -> + SEpisode.create().apply { + url = episodeFile.absolutePath + name = if (episodeFile.isDirectory) { + episodeFile.name + } else { + episodeFile.nameWithoutExtension + } + date_upload = episodeFile.lastModified() + + episode_number = EpisodeRecognition.parseEpisodeNumber( + anime.title, + this.name, + this.episode_number, + ) + } + } + .sortedWith { e1, e2 -> + val e = e2.episode_number.compareTo(e1.episode_number) + if (e == 0) e2.name.compareToCaseInsensitiveNaturalOrder(e1.name) else e + } + .toList() + } + + // Filters + override fun getFilterList() = AnimeFilterList(AnimeOrderBy.Popular(context)) + + // Unused stuff + override suspend fun getVideoList(episode: SEpisode) = throw UnsupportedOperationException("Unused") + + private fun updateCoverFromVideo(episode: SEpisode, anime: SAnime) { + val baseDirsFiles = getBaseDirectoriesFiles(context) + val animeDir = getAnimeDir(anime.url, baseDirsFiles) ?: return + val coverPath = "${animeDir.absolutePath}/$DEFAULT_COVER_NAME" + + val episodeFilename = { episode.url.toFFmpegString(context) } + val ffProbe = com.arthenica.ffmpegkit.FFprobeKit.execute( + "-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 \"${episodeFilename()}\"", + ) + val duration = ffProbe.allLogsAsString.trim().toFloat() + val second = duration.toInt() / 2 + + val coverFilename = coverPath.toFFmpegString(context) + com.arthenica.ffmpegkit.FFmpegKit.execute("-ss $second -i \"${episodeFilename()}\" -frames 1 -q:v 2 \"$coverFilename\" -y") + + if (File(coverPath).exists()) { + anime.thumbnail_url = coverPath + } + } + + companion object { + const val ID = 0L + const val HELP_URL = "https://aniyomi.org/help/guides/local-anime/" + + private const val DEFAULT_COVER_NAME = "cover.jpg" + private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS) + + private fun getBaseDirectories(context: Context): Sequence { + val localFolder = context.getString(R.string.app_name) + File.separator + "localanime" + return DiskUtil.getExternalStorages(context) + .map { File(it.absolutePath, localFolder) } + .asSequence() + } + + private fun getBaseDirectoriesFiles(context: Context): Sequence { + return getBaseDirectories(context) + // Get all the files inside all baseDir + .flatMap { it.listFiles().orEmpty().toList() } + } + + private fun getAnimeDir(animeUrl: String, baseDirsFile: Sequence): File? { + return baseDirsFile + // Get the first animeDir or null + .firstOrNull { it.isDirectory && it.name == animeUrl } + } + } +} diff --git a/source-local/src/main/java/tachiyomi/source/local/entries/manga/LocalMangaSource.kt b/source-local/src/main/java/tachiyomi/source/local/entries/manga/LocalMangaSource.kt new file mode 100644 index 000000000..439d69d05 --- /dev/null +++ b/source-local/src/main/java/tachiyomi/source/local/entries/manga/LocalMangaSource.kt @@ -0,0 +1,372 @@ +package tachiyomi.source.local.entries.manga + +import android.content.Context +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.UnmeteredSource +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.EpubFile +import kotlinx.coroutines.runBlocking +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.metadata.comicinfo.COMIC_INFO_FILE +import tachiyomi.core.metadata.comicinfo.ComicInfo +import tachiyomi.core.metadata.comicinfo.copyFromComicInfo +import tachiyomi.core.metadata.tachiyomi.MangaDetails +import tachiyomi.core.util.lang.withIOContext +import tachiyomi.core.util.system.ImageUtil +import tachiyomi.core.util.system.logcat +import tachiyomi.domain.items.chapter.service.ChapterRecognition +import tachiyomi.source.local.R +import tachiyomi.source.local.filter.manga.MangaOrderBy +import tachiyomi.source.local.image.manga.LocalMangaCoverManager +import tachiyomi.source.local.io.Archive +import tachiyomi.source.local.io.Format +import tachiyomi.source.local.io.manga.LocalMangaSourceFileSystem +import tachiyomi.source.local.metadata.fillChapterMetadata +import tachiyomi.source.local.metadata.fillMangaMetadata +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 +import com.github.junrar.Archive as JunrarArchive + +class LocalMangaSource( + private val context: Context, + private val fileSystem: LocalMangaSourceFileSystem, + private val coverManager: LocalMangaCoverManager, +) : CatalogueSource, UnmeteredSource { + + private val json: Json by injectLazy() + private val xml: XML by injectLazy() + + private val POPULAR_FILTERS = FilterList(MangaOrderBy.Popular(context)) + private val LATEST_FILTERS = FilterList(MangaOrderBy.Latest(context)) + + 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 { + val baseDirsFiles = fileSystem.getFilesInBaseDirectories() + val lastModifiedLimit by lazy { if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L } + + var mangaDirs = baseDirsFiles + // Filter out files that are hidden and is not a folder + .filter { it.isDirectory && !it.name.startsWith('.') } + .distinctBy { it.name } + .filter { // Filter by query or last modified + if (lastModifiedLimit == 0L) { + it.name.contains(query, ignoreCase = true) + } else { + it.lastModified() >= lastModifiedLimit + } + } + + filters.forEach { filter -> + when (filter) { + is MangaOrderBy.Popular -> { + mangaDirs = if (filter.state!!.ascending) { + mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) + } else { + mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name }) + } + } + is MangaOrderBy.Latest -> { + 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 + coverManager.find(mangaDir.name) + ?.takeIf(File::exists) + ?.let { thumbnail_url = it.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 { + coverManager.find(manga.url)?.let { + manga.thumbnail_url = it.absolutePath + } + + // Augment manga details based on metadata files + try { + val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url).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(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(Archive::isSupported) + .toList() + + val mangaDir = fileSystem.getMangaDirectory(manga.url) + 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, folderPath: String?): File? { + for (chapter in chapterArchives) { + when (Format.valueOf(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 -> { + JunrarArchive(chapter).use { rar -> + 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(it) + } + + manga.copyFromComicInfo(comicInfo) + } + + // Chapters + override suspend fun getChapterList(manga: SManga): List { + return fileSystem.getFilesInMangaDirectory(manga.url) + // Only keep supported formats + .filter { it.isDirectory || Archive.isSupported(it) } + .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 = Format.valueOf(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(MangaOrderBy.Popular(context)) + + // Unused stuff + override suspend fun getPageList(chapter: SChapter) = throw UnsupportedOperationException("Unused") + + fun getFormat(chapter: SChapter): Format { + try { + return fileSystem.getBaseDirectories() + .map { directory -> File(directory, chapter.url) } + .find { chapterFile -> chapterFile.exists() } + ?.let(Format.Companion::valueOf) + ?: throw Exception(context.getString(R.string.chapter_not_found)) + } catch (e: Format.UnknownFormatException) { + throw Exception(context.getString(R.string.local_invalid_format)) + } catch (e: Exception) { + throw e + } + } + + 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 { coverManager.update(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 { coverManager.update(manga, zip.getInputStream(it)) } + } + } + is Format.Rar -> { + JunrarArchive(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 { coverManager.update(manga, archive.getInputStream(it)) } + } + } + is Format.Epub -> { + EpubFile(format.file).use { epub -> + val entry = epub.getImagesFromPages() + .firstOrNull() + ?.let { epub.getEntry(it) } + + entry?.let { coverManager.update(manga, epub.getInputStream(it)) } + } + } + } + } catch (e: Throwable) { + logcat(LogPriority.ERROR, e) { "Error updating cover for ${manga.title}" } + null + } + } + + companion object { + const val ID = 0L + const val HELP_URL = "https://aniyomi.org/help/guides/local-manga/" + + private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS) + } +} diff --git a/source-local/src/main/java/tachiyomi/source/local/filter/anime/AnimeOrderBy.kt b/source-local/src/main/java/tachiyomi/source/local/filter/anime/AnimeOrderBy.kt new file mode 100644 index 000000000..9c65eee98 --- /dev/null +++ b/source-local/src/main/java/tachiyomi/source/local/filter/anime/AnimeOrderBy.kt @@ -0,0 +1,14 @@ +package tachiyomi.source.local.filter.anime + +import android.content.Context +import eu.kanade.tachiyomi.animesource.model.AnimeFilter +import tachiyomi.source.local.R + +sealed class AnimeOrderBy(context: Context, selection: Selection) : AnimeFilter.Sort( + context.getString(R.string.local_filter_order_by), + arrayOf(context.getString(R.string.title), context.getString(R.string.date)), + selection, +) { + class Popular(context: Context) : AnimeOrderBy(context, Selection(0, true)) + class Latest(context: Context) : AnimeOrderBy(context, Selection(1, false)) +} diff --git a/source-local/src/main/java/tachiyomi/source/local/filter/manga/MangaOrderBy.kt b/source-local/src/main/java/tachiyomi/source/local/filter/manga/MangaOrderBy.kt new file mode 100644 index 000000000..e5f9feef3 --- /dev/null +++ b/source-local/src/main/java/tachiyomi/source/local/filter/manga/MangaOrderBy.kt @@ -0,0 +1,14 @@ +package tachiyomi.source.local.filter.manga + +import android.content.Context +import eu.kanade.tachiyomi.source.model.Filter +import tachiyomi.source.local.R + +sealed class MangaOrderBy(context: Context, selection: Selection) : Filter.Sort( + context.getString(R.string.local_filter_order_by), + arrayOf(context.getString(R.string.title), context.getString(R.string.date)), + selection, +) { + class Popular(context: Context) : MangaOrderBy(context, Selection(0, true)) + class Latest(context: Context) : MangaOrderBy(context, Selection(1, false)) +} diff --git a/source-local/src/main/java/tachiyomi/source/local/image/anime/AndroidLocalAnimeCoverManager.kt b/source-local/src/main/java/tachiyomi/source/local/image/anime/AndroidLocalAnimeCoverManager.kt new file mode 100644 index 000000000..fc0b8b339 --- /dev/null +++ b/source-local/src/main/java/tachiyomi/source/local/image/anime/AndroidLocalAnimeCoverManager.kt @@ -0,0 +1,55 @@ +package tachiyomi.source.local.image.anime + +import android.content.Context +import com.hippo.unifile.UniFile +import eu.kanade.tachiyomi.animesource.model.SAnime +import eu.kanade.tachiyomi.util.storage.DiskUtil +import tachiyomi.core.util.system.ImageUtil +import tachiyomi.source.local.io.anime.LocalAnimeSourceFileSystem +import java.io.File +import java.io.InputStream + +private const val DEFAULT_COVER_NAME = "cover.jpg" + +class AndroidLocalAnimeCoverManager( + private val context: Context, + private val fileSystem: LocalAnimeSourceFileSystem, +) : LocalAnimeCoverManager { + + override fun find(animeUrl: String): File? { + return fileSystem.getFilesInAnimeDirectory(animeUrl) + // 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() } + } + } + + override fun update(anime: SAnime, inputStream: InputStream): File? { + val directory = fileSystem.getAnimeDirectory(anime.url) + if (directory == null) { + inputStream.close() + return null + } + + var targetFile = find(anime.url) + if (targetFile == null) { + targetFile = File(directory.absolutePath, DEFAULT_COVER_NAME) + targetFile.createNewFile() + } + + // It might not exist at this point + targetFile.parentFile?.mkdirs() + inputStream.use { input -> + targetFile.outputStream().use { output -> + input.copyTo(output) + } + } + + DiskUtil.createNoMediaFile(UniFile.fromFile(directory), context) + + anime.thumbnail_url = targetFile.absolutePath + return targetFile + } +} diff --git a/source-local/src/main/java/tachiyomi/source/local/image/anime/LocalAnimeCoverManager.kt b/source-local/src/main/java/tachiyomi/source/local/image/anime/LocalAnimeCoverManager.kt new file mode 100644 index 000000000..659f236bf --- /dev/null +++ b/source-local/src/main/java/tachiyomi/source/local/image/anime/LocalAnimeCoverManager.kt @@ -0,0 +1,12 @@ +package tachiyomi.source.local.image.anime + +import eu.kanade.tachiyomi.animesource.model.SAnime +import java.io.File +import java.io.InputStream + +interface LocalAnimeCoverManager { + + fun find(animeUrl: String): File? + + fun update(anime: SAnime, inputStream: InputStream): File? +} diff --git a/source-local/src/main/java/tachiyomi/source/local/image/manga/AndroidLocalMangaCoverManager.kt b/source-local/src/main/java/tachiyomi/source/local/image/manga/AndroidLocalMangaCoverManager.kt new file mode 100644 index 000000000..aa6786f2a --- /dev/null +++ b/source-local/src/main/java/tachiyomi/source/local/image/manga/AndroidLocalMangaCoverManager.kt @@ -0,0 +1,55 @@ +package tachiyomi.source.local.image.manga + +import android.content.Context +import com.hippo.unifile.UniFile +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.util.storage.DiskUtil +import tachiyomi.core.util.system.ImageUtil +import tachiyomi.source.local.io.manga.LocalMangaSourceFileSystem +import java.io.File +import java.io.InputStream + +private const val DEFAULT_COVER_NAME = "cover.jpg" + +class AndroidLocalMangaCoverManager( + private val context: Context, + private val fileSystem: LocalMangaSourceFileSystem, +) : LocalMangaCoverManager { + + override fun find(mangaUrl: String): File? { + return fileSystem.getFilesInMangaDirectory(mangaUrl) + // 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() } + } + } + + override fun update(manga: SManga, inputStream: InputStream): File? { + val directory = fileSystem.getMangaDirectory(manga.url) + if (directory == null) { + inputStream.close() + return null + } + + var targetFile = find(manga.url) + if (targetFile == null) { + targetFile = File(directory.absolutePath, DEFAULT_COVER_NAME) + targetFile.createNewFile() + } + + // It might not exist at this point + targetFile.parentFile?.mkdirs() + inputStream.use { input -> + targetFile.outputStream().use { output -> + input.copyTo(output) + } + } + + DiskUtil.createNoMediaFile(UniFile.fromFile(directory), context) + + manga.thumbnail_url = targetFile.absolutePath + return targetFile + } +} diff --git a/source-local/src/main/java/tachiyomi/source/local/image/manga/LocalMangaCoverManager.kt b/source-local/src/main/java/tachiyomi/source/local/image/manga/LocalMangaCoverManager.kt new file mode 100644 index 000000000..b11cdc43a --- /dev/null +++ b/source-local/src/main/java/tachiyomi/source/local/image/manga/LocalMangaCoverManager.kt @@ -0,0 +1,12 @@ +package tachiyomi.source.local.image.manga + +import eu.kanade.tachiyomi.source.model.SManga +import java.io.File +import java.io.InputStream + +interface LocalMangaCoverManager { + + fun find(mangaUrl: String): File? + + fun update(manga: SManga, inputStream: InputStream): File? +} diff --git a/source-local/src/main/java/tachiyomi/source/local/io/Archive.kt b/source-local/src/main/java/tachiyomi/source/local/io/Archive.kt new file mode 100644 index 000000000..b28ee60b5 --- /dev/null +++ b/source-local/src/main/java/tachiyomi/source/local/io/Archive.kt @@ -0,0 +1,12 @@ +package tachiyomi.source.local.io + +import java.io.File + +object Archive { + + private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub") + + fun isSupported(file: File): Boolean = with(file) { + return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES + } +} diff --git a/source-local/src/main/java/tachiyomi/source/local/io/Format.kt b/source-local/src/main/java/tachiyomi/source/local/io/Format.kt new file mode 100644 index 000000000..a73534f21 --- /dev/null +++ b/source-local/src/main/java/tachiyomi/source/local/io/Format.kt @@ -0,0 +1,25 @@ +package tachiyomi.source.local.io + +import java.io.File + +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() + + class UnknownFormatException : Exception() + + companion object { + + fun valueOf(file: File) = with(file) { + when { + isDirectory -> Directory(this) + extension.equals("zip", true) || extension.equals("cbz", true) -> Zip(this) + extension.equals("rar", true) || extension.equals("cbr", true) -> Rar(this) + extension.equals("epub", true) -> Epub(this) + else -> throw UnknownFormatException() + } + } + } +} diff --git a/source-local/src/main/java/tachiyomi/source/local/io/anime/AndroidLocalAnimeSourceFileSystem.kt b/source-local/src/main/java/tachiyomi/source/local/io/anime/AndroidLocalAnimeSourceFileSystem.kt new file mode 100644 index 000000000..87561a904 --- /dev/null +++ b/source-local/src/main/java/tachiyomi/source/local/io/anime/AndroidLocalAnimeSourceFileSystem.kt @@ -0,0 +1,39 @@ +package tachiyomi.source.local.io.anime + +import android.content.Context +import eu.kanade.tachiyomi.util.storage.DiskUtil +import tachiyomi.source.local.R +import java.io.File + +class AndroidLocalAnimeSourceFileSystem( + private val context: Context, +) : LocalAnimeSourceFileSystem { + + private val baseFolderLocation = "${context.getString(R.string.app_name)}${File.separator}local" + + override fun getBaseDirectories(): Sequence { + return DiskUtil.getExternalStorages(context) + .map { File(it.absolutePath, baseFolderLocation) } + .asSequence() + } + + override fun getFilesInBaseDirectories(): Sequence { + return getBaseDirectories() + // Get all the files inside all baseDir + .flatMap { it.listFiles().orEmpty().toList() } + } + + override fun getAnimeDirectory(name: String): File? { + return getFilesInBaseDirectories() + // Get the first animeDir or null + .firstOrNull { it.isDirectory && it.name == name } + } + + override fun getFilesInAnimeDirectory(name: String): Sequence { + return getFilesInBaseDirectories() + // Filter out ones that are not related to the anime and is not a directory + .filter { it.isDirectory && it.name == name } + // Get all the files inside the filtered folders + .flatMap { it.listFiles().orEmpty().toList() } + } +} diff --git a/source-local/src/main/java/tachiyomi/source/local/io/anime/LocalAnimeSourceFileSystem.kt b/source-local/src/main/java/tachiyomi/source/local/io/anime/LocalAnimeSourceFileSystem.kt new file mode 100644 index 000000000..778706ef5 --- /dev/null +++ b/source-local/src/main/java/tachiyomi/source/local/io/anime/LocalAnimeSourceFileSystem.kt @@ -0,0 +1,14 @@ +package tachiyomi.source.local.io.anime + +import java.io.File + +interface LocalAnimeSourceFileSystem { + + fun getBaseDirectories(): Sequence + + fun getFilesInBaseDirectories(): Sequence + + fun getAnimeDirectory(name: String): File? + + fun getFilesInAnimeDirectory(name: String): Sequence +} diff --git a/source-local/src/main/java/tachiyomi/source/local/io/manga/AndroidLocalMangaSourceFileSystem.kt b/source-local/src/main/java/tachiyomi/source/local/io/manga/AndroidLocalMangaSourceFileSystem.kt new file mode 100644 index 000000000..d25b53d0c --- /dev/null +++ b/source-local/src/main/java/tachiyomi/source/local/io/manga/AndroidLocalMangaSourceFileSystem.kt @@ -0,0 +1,39 @@ +package tachiyomi.source.local.io.manga + +import android.content.Context +import eu.kanade.tachiyomi.util.storage.DiskUtil +import tachiyomi.source.local.R +import java.io.File + +class AndroidLocalMangaSourceFileSystem( + private val context: Context, +) : LocalMangaSourceFileSystem { + + private val baseFolderLocation = "${context.getString(R.string.app_name)}${File.separator}local" + + override fun getBaseDirectories(): Sequence { + return DiskUtil.getExternalStorages(context) + .map { File(it.absolutePath, baseFolderLocation) } + .asSequence() + } + + override fun getFilesInBaseDirectories(): Sequence { + return getBaseDirectories() + // Get all the files inside all baseDir + .flatMap { it.listFiles().orEmpty().toList() } + } + + override fun getMangaDirectory(name: String): File? { + return getFilesInBaseDirectories() + // Get the first mangaDir or null + .firstOrNull { it.isDirectory && it.name == name } + } + + override fun getFilesInMangaDirectory(name: String): Sequence { + return getFilesInBaseDirectories() + // Filter out ones that are not related to the manga and is not a directory + .filter { it.isDirectory && it.name == name } + // Get all the files inside the filtered folders + .flatMap { it.listFiles().orEmpty().toList() } + } +} diff --git a/source-local/src/main/java/tachiyomi/source/local/io/manga/LocalMangaSourceFileSystem.kt b/source-local/src/main/java/tachiyomi/source/local/io/manga/LocalMangaSourceFileSystem.kt new file mode 100644 index 000000000..8a022370f --- /dev/null +++ b/source-local/src/main/java/tachiyomi/source/local/io/manga/LocalMangaSourceFileSystem.kt @@ -0,0 +1,14 @@ +package tachiyomi.source.local.io.manga + +import java.io.File + +interface LocalMangaSourceFileSystem { + + fun getBaseDirectories(): Sequence + + fun getFilesInBaseDirectories(): Sequence + + fun getMangaDirectory(name: String): File? + + fun getFilesInMangaDirectory(name: String): Sequence +} diff --git a/source-local/src/main/java/tachiyomi/source/local/metadata/EpubFile.kt b/source-local/src/main/java/tachiyomi/source/local/metadata/EpubFile.kt new file mode 100644 index 000000000..d9ce323d5 --- /dev/null +++ b/source-local/src/main/java/tachiyomi/source/local/metadata/EpubFile.kt @@ -0,0 +1,60 @@ +package tachiyomi.source.local.metadata + +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.util.storage.EpubFile +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Locale + +/** + * Fills manga metadata using this epub file's metadata. + */ +fun EpubFile.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 EpubFile.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 + } + } +}