From f27dc19b378f2f38ed2885f40d93f6d8817ef584 Mon Sep 17 00:00:00 2001 From: Andreas Date: Sun, 26 Feb 2023 22:16:49 +0100 Subject: [PATCH] Move Local Source to separate module (#9152) * Move Local Source to separate module * Review changes --- app/build.gradle.kts | 7 +- .../data/source/SourceRepositoryImpl.kt | 2 +- .../eu/kanade/domain/manga/model/Manga.kt | 25 +- .../source/interactor/GetEnabledSources.kt | 2 +- .../presentation/browse/BrowseSourceScreen.kt | 2 +- .../presentation/browse/SourcesScreen.kt | 2 +- .../browse/components/BrowseIcons.kt | 2 +- .../browse/components/BrowseSourceToolbar.kt | 2 +- .../presentation/extensions/DiskUtil.kt | 18 ++ .../settings/screen/SettingsBackupScreen.kt | 1 + .../java/eu/kanade/tachiyomi/AppModule.kt | 7 + .../data/coil/TachiyomiImageDecoder.kt | 2 +- .../tachiyomi/data/download/Downloader.kt | 2 +- .../kanade/tachiyomi/data/saver/ImageSaver.kt | 2 +- .../tachiyomi/source/SourceExtensions.kt | 1 + .../kanade/tachiyomi/source/SourceManager.kt | 13 +- .../kanade/tachiyomi/ui/browse/BrowseTab.kt | 1 + .../migration/search/SourceSearchScreen.kt | 2 +- .../source/browse/BrowseSourceScreen.kt | 2 +- .../ui/manga/MangaCoverScreenModel.kt | 2 +- .../tachiyomi/ui/reader/ReaderViewModel.kt | 2 +- .../ui/reader/loader/ChapterLoader.kt | 11 +- .../ui/reader/loader/DirectoryPageLoader.kt | 2 +- .../ui/reader/loader/RarPageLoader.kt | 2 +- .../ui/reader/loader/ZipPageLoader.kt | 2 +- .../ui/reader/viewer/pager/PagerPageHolder.kt | 2 +- .../viewer/webtoon/WebtoonPageHolder.kt | 2 +- .../kanade/tachiyomi/util/MangaExtensions.kt | 7 +- .../util/system/ContextExtensions.kt | 4 - core-metadata/.gitignore | 1 + core-metadata/build.gradle.kts | 21 ++ core-metadata/consumer-rules.pro | 0 core-metadata/proguard-rules.pro | 21 ++ core-metadata/src/main/AndroidManifest.xml | 2 + .../core/metadata/comicinfo}/ComicInfo.kt | 26 +- .../core/metadata/tachiyomi/MangaDetails.kt | 13 + core/build.gradle.kts | 9 + .../eu/kanade/tachiyomi/util/lang/Hash.kt | 0 .../tachiyomi/util/lang/StringExtensions.kt | 0 .../kanade/tachiyomi/util/storage/DiskUtil.kt | 15 -- .../kanade/tachiyomi/util/storage/EpubFile.kt | 63 +---- .../tachiyomi/core}/util/system/ImageUtil.kt | 8 +- settings.gradle.kts | 2 + source-local/.gitignore | 1 + source-local/build.gradle.kts | 29 +++ source-local/consumer-rules.pro | 0 source-local/proguard-rules.pro | 21 ++ source-local/src/main/AndroidManifest.xml | 2 + .../tachiyomi/source/local}/LocalSource.kt | 241 +++++------------- .../tachiyomi/source/local/filter/OrderBy.kt | 14 + .../local/image/AndroidLocalCoverManager.kt | 55 ++++ .../source/local/image/LocalCoverManager.kt | 12 + .../local/io/AndroidLocalSourceFileSystem.kt | 39 +++ .../java/tachiyomi/source/local/io/Archive.kt | 12 + .../java/tachiyomi/source/local/io/Format.kt | 25 ++ .../source/local/io/LocalSourceFileSystem.kt | 14 + .../source/local/metadata/EpubFile.kt | 60 +++++ 57 files changed, 523 insertions(+), 314 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/extensions/DiskUtil.kt create mode 100644 core-metadata/.gitignore create mode 100644 core-metadata/build.gradle.kts create mode 100644 core-metadata/consumer-rules.pro create mode 100644 core-metadata/proguard-rules.pro create mode 100644 core-metadata/src/main/AndroidManifest.xml rename {app/src/main/java/eu/kanade/domain/manga/model => core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo}/ComicInfo.kt (82%) create mode 100644 core-metadata/src/main/java/tachiyomi/core/metadata/tachiyomi/MangaDetails.kt rename {app => core}/src/main/java/eu/kanade/tachiyomi/util/lang/Hash.kt (100%) rename {app => core}/src/main/java/eu/kanade/tachiyomi/util/lang/StringExtensions.kt (100%) rename {app => core}/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt (86%) rename {app => core}/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt (69%) rename {app/src/main/java/eu/kanade/tachiyomi => core/src/main/java/tachiyomi/core}/util/system/ImageUtil.kt (99%) create mode 100644 source-local/.gitignore create mode 100644 source-local/build.gradle.kts create mode 100644 source-local/consumer-rules.pro create mode 100644 source-local/proguard-rules.pro create mode 100644 source-local/src/main/AndroidManifest.xml rename {app/src/main/java/eu/kanade/tachiyomi/source => source-local/src/main/java/tachiyomi/source/local}/LocalSource.kt (58%) create mode 100644 source-local/src/main/java/tachiyomi/source/local/filter/OrderBy.kt create mode 100644 source-local/src/main/java/tachiyomi/source/local/image/AndroidLocalCoverManager.kt create mode 100644 source-local/src/main/java/tachiyomi/source/local/image/LocalCoverManager.kt create mode 100644 source-local/src/main/java/tachiyomi/source/local/io/AndroidLocalSourceFileSystem.kt create mode 100644 source-local/src/main/java/tachiyomi/source/local/io/Archive.kt create mode 100644 source-local/src/main/java/tachiyomi/source/local/io/Format.kt create mode 100644 source-local/src/main/java/tachiyomi/source/local/io/LocalSourceFileSystem.kt create mode 100644 source-local/src/main/java/tachiyomi/source/local/metadata/EpubFile.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 64787068f8..22fce094cb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -140,7 +140,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")) @@ -200,7 +202,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 @@ -224,9 +226,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/data/source/SourceRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt index 9f30c29133..9ac162e1ba 100644 --- a/app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt +++ b/app/src/main/java/eu/kanade/data/source/SourceRepositoryImpl.kt @@ -3,7 +3,6 @@ package eu.kanade.data.source import eu.kanade.domain.source.model.SourcePagingSourceType import eu.kanade.domain.source.repository.SourceRepository import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.FilterList import kotlinx.coroutines.flow.Flow @@ -11,6 +10,7 @@ import kotlinx.coroutines.flow.map import tachiyomi.data.DatabaseHandler import tachiyomi.domain.source.model.Source import tachiyomi.domain.source.model.SourceWithCount +import tachiyomi.source.local.LocalSource class SourceRepositoryImpl( private val sourceManager: SourceManager, diff --git a/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt b/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt index a12076e77f..5c7c1e22dc 100644 --- a/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt +++ b/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt @@ -2,12 +2,13 @@ package eu.kanade.domain.manga.model import eu.kanade.domain.base.BasePreferences import eu.kanade.tachiyomi.data.cache.CoverCache -import eu.kanade.tachiyomi.source.LocalSource 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.domain.chapter.model.Chapter import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.TriStateFilter +import tachiyomi.source.local.LocalSource import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -91,3 +92,25 @@ fun Manga.isLocal(): Boolean = source == LocalSource.ID fun Manga.hasCustomCover(coverCache: CoverCache = 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/interactor/GetEnabledSources.kt b/app/src/main/java/eu/kanade/domain/source/interactor/GetEnabledSources.kt index a957523846..a6fc330af9 100644 --- a/app/src/main/java/eu/kanade/domain/source/interactor/GetEnabledSources.kt +++ b/app/src/main/java/eu/kanade/domain/source/interactor/GetEnabledSources.kt @@ -2,13 +2,13 @@ package eu.kanade.domain.source.interactor import eu.kanade.domain.source.repository.SourceRepository import eu.kanade.domain.source.service.SourcePreferences -import eu.kanade.tachiyomi.source.LocalSource import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import tachiyomi.domain.source.model.Pin import tachiyomi.domain.source.model.Pins import tachiyomi.domain.source.model.Source +import tachiyomi.source.local.LocalSource class GetEnabledSources( private val repository: SourceRepository, diff --git a/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt index 1c9bdacddd..a27bf3d096 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/BrowseSourceScreen.kt @@ -22,7 +22,6 @@ import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid import eu.kanade.presentation.browse.components.BrowseSourceList import eu.kanade.presentation.components.AppBar import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import kotlinx.coroutines.flow.StateFlow @@ -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.LocalSource @Composable fun BrowseSourceContent( diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt index e89d506202..5c0dd1042a 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import eu.kanade.presentation.browse.components.BaseSourceItem import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.ui.browse.source.SourcesState import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.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.LocalSource @Composable fun SourcesScreen( diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt index 862ecb93e2..035656f60a 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt @@ -31,9 +31,9 @@ import eu.kanade.domain.source.model.icon import eu.kanade.presentation.util.rememberResourceBitmapPainter import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.extension.model.Extension -import eu.kanade.tachiyomi.source.LocalSource import tachiyomi.core.util.lang.withIOContext import tachiyomi.domain.source.model.Source +import tachiyomi.source.local.LocalSource private val defaultModifier = Modifier .height(40.dp) diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt index 508cccbef4..ccea144a28 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt @@ -19,9 +19,9 @@ 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.LocalSource import eu.kanade.tachiyomi.source.Source import tachiyomi.domain.library.model.LibraryDisplayMode +import tachiyomi.source.local.LocalSource @Composable fun BrowseSourceToolbar( 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 0000000000..f0581e0999 --- /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/more/settings/screen/SettingsBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupScreen.kt index be159f9d20..8f0913ae87 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 9cb8b5b067..f4cee88626 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt @@ -48,6 +48,10 @@ import tachiyomi.data.Mangas import tachiyomi.data.dateAdapter import tachiyomi.data.listOfStringsAdapter import tachiyomi.data.updateStrategyAdapter +import tachiyomi.source.local.image.AndroidLocalCoverManager +import tachiyomi.source.local.image.LocalCoverManager +import tachiyomi.source.local.io.AndroidLocalSourceFileSystem +import tachiyomi.source.local.io.LocalSourceFileSystem import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektRegistrar import uy.kohesive.injekt.api.addSingleton @@ -133,6 +137,9 @@ class AppModule(val app: Application) : InjektModule { addSingletonFactory { ImageSaver(app) } + addSingletonFactory { AndroidLocalSourceFileSystem(app) } + addSingletonFactory { AndroidLocalCoverManager(app, get()) } + // Asynchronously init expensive components for a faster cold start ContextCompat.getMainExecutor(app).execute { get() 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 42ef7fe34f..e0a5ffe8e7 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/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index 061927054f..f62c38e552 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -21,7 +21,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 @@ -45,6 +44,7 @@ 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.chapter.model.Chapter import tachiyomi.domain.manga.model.Manga 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 b8a2befab2..c0536e431a 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/SourceExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt index 28d5d32921..e94c1d895e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt @@ -4,6 +4,7 @@ import android.graphics.drawable.Drawable import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.extension.ExtensionManager import tachiyomi.domain.source.model.SourceData +import tachiyomi.source.local.LocalSource import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt index 7fb1a96492..81b4bde693 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt @@ -20,6 +20,9 @@ import kotlinx.coroutines.runBlocking import rx.Observable import tachiyomi.domain.source.model.SourceData import tachiyomi.domain.source.repository.SourceDataRepository +import tachiyomi.source.local.LocalSource +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.util.concurrent.ConcurrentHashMap @@ -43,7 +46,15 @@ class SourceManager( scope.launch { extensionManager.installedExtensionsFlow .collectLatest { extensions -> - val mutableMap = ConcurrentHashMap(mapOf(LocalSource.ID to LocalSource(context))) + val mutableMap = ConcurrentHashMap( + mapOf( + LocalSource.ID to LocalSource( + 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 58ee5a0471..d0753ce108 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.extension.ExtensionsScreenModel diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt index d4eb08653f..8c8b949c55 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt @@ -23,7 +23,6 @@ import eu.kanade.presentation.browse.BrowseSourceContent import eu.kanade.presentation.components.SearchToolbar import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel import eu.kanade.tachiyomi.ui.home.HomeScreen @@ -34,6 +33,7 @@ import tachiyomi.core.Constants import tachiyomi.domain.manga.model.Manga import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton import tachiyomi.presentation.core.components.material.Scaffold +import tachiyomi.source.local.LocalSource data class SourceSearchScreen( private val oldManga: Manga, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt index e8a01a8f1d..69acc6f01c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt @@ -45,7 +45,6 @@ import eu.kanade.presentation.util.AssistContentScreen import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen @@ -61,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.LocalSource data class BrowseSourceScreen( private val sourceId: Long, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaCoverScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaCoverScreenModel.kt index c7e8661d22..c8060e02bf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaCoverScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/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/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index e0261c6f5c..05868e6cb7 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 4a863d862a..c3e8441c98 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 @@ -5,7 +5,6 @@ import com.github.junrar.exception.UnsupportedRarV5Exception import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadProvider -import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.online.HttpSource @@ -13,6 +12,8 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.system.logcat import tachiyomi.domain.manga.model.Manga +import tachiyomi.source.local.LocalSource +import tachiyomi.source.local.io.Format /** * Loader used to retrieve the [PageLoader] for a given chapter. @@ -80,14 +81,14 @@ class ChapterLoader( source is HttpSource -> HttpPageLoader(chapter, source) source is LocalSource -> source.getFormat(chapter.chapter).let { format -> when (format) { - is LocalSource.Format.Directory -> DirectoryPageLoader(format.file) - is LocalSource.Format.Zip -> ZipPageLoader(format.file) - is LocalSource.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 LocalSource.Format.Epub -> EpubPageLoader(format.file) + is Format.Epub -> EpubPageLoader(format.file) } } source is SourceManager.StubSource -> 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 cff8e38a61..1b25c273e9 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 58debe3b1b..6cd804dda6 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 543241b304..5022fb45e4 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 6cc0e4b416..801e03c4bb 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 fda21b8a24..6ee030ce96 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/util/MangaExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt index 0d73d2bbe5..67cd8ec75e 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.manga.interactor.UpdateManga import eu.kanade.domain.manga.model.hasCustomCover import eu.kanade.domain.manga.model.isLocal import eu.kanade.domain.manga.model.toSManga import eu.kanade.tachiyomi.data.cache.CoverCache -import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.model.SManga import tachiyomi.domain.manga.model.Manga +import tachiyomi.source.local.image.LocalCoverManager 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: LocalCoverManager, stream: InputStream, updateManga: UpdateManga = Injekt.get(), coverCache: CoverCache = Injekt.get(), ) { if (isLocal()) { - LocalSource.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/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt index c51605cb62..318b9175dc 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 0000000000..42afabfd2a --- /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 0000000000..51db46b912 --- /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 0000000000..e69de29bb2 diff --git a/core-metadata/proguard-rules.pro b/core-metadata/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /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 0000000000..568741e54f --- /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/manga/model/ComicInfo.kt b/core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt similarity index 82% rename from app/src/main/java/eu/kanade/domain/manga/model/ComicInfo.kt rename to core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt index 95987f530c..8181a83802 100644 --- a/app/src/main/java/eu/kanade/domain/manga/model/ComicInfo.kt +++ b/core-metadata/src/main/java/tachiyomi/core/metadata/comicinfo/ComicInfo.kt @@ -5,33 +5,9 @@ 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.chapter.model.Chapter -import tachiyomi.domain.manga.model.Manga 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/MangaDetails.kt b/core-metadata/src/main/java/tachiyomi/core/metadata/tachiyomi/MangaDetails.kt new file mode 100644 index 0000000000..7768986e08 --- /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 30a5985173..548d65563f 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -27,12 +27,21 @@ 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) } 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 4a6322080a..8e3141a277 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 517ba818f7..8838a199c8 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/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 e4eebbce6b..638f33726c 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/settings.gradle.kts b/settings.gradle.kts index c711be250b..4db52ce318 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 0000000000..42afabfd2a --- /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 0000000000..96a974721f --- /dev/null +++ b/source-local/build.gradle.kts @@ -0,0 +1,29 @@ +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) +} diff --git a/source-local/consumer-rules.pro b/source-local/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/source-local/proguard-rules.pro b/source-local/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /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 0000000000..568741e54f --- /dev/null +++ b/source-local/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt b/source-local/src/main/java/tachiyomi/source/local/LocalSource.kt similarity index 58% rename from app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt rename to source-local/src/main/java/tachiyomi/source/local/LocalSource.kt index f4a7bb9c38..b95c66daf2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt +++ b/source-local/src/main/java/tachiyomi/source/local/LocalSource.kt @@ -1,32 +1,36 @@ -package eu.kanade.tachiyomi.source +package tachiyomi.source.local import android.content.Context -import com.github.junrar.Archive -import com.hippo.unifile.UniFile import eu.kanade.domain.manga.model.COMIC_INFO_FILE import eu.kanade.domain.manga.model.ComicInfo import eu.kanade.domain.manga.model.copyFromComicInfo -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.source.model.Filter +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.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.metadata.tachiyomi.MangaDetails import tachiyomi.core.util.lang.withIOContext +import tachiyomi.core.util.system.ImageUtil import tachiyomi.core.util.system.logcat import tachiyomi.domain.chapter.service.ChapterRecognition +import tachiyomi.source.local.filter.OrderBy +import tachiyomi.source.local.image.LocalCoverManager +import tachiyomi.source.local.io.Archive +import tachiyomi.source.local.io.Format +import tachiyomi.source.local.io.LocalSourceFileSystem +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 @@ -34,14 +38,20 @@ 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 LocalSource( private val context: Context, + private val fileSystem: LocalSourceFileSystem, + private val coverManager: LocalCoverManager, ) : CatalogueSource, UnmeteredSource { private val json: Json by injectLazy() private val xml: XML by injectLazy() + private val POPULAR_FILTERS = FilterList(OrderBy.Popular(context)) + private val LATEST_FILTERS = FilterList(OrderBy.Latest(context)) + override val name: String = context.getString(R.string.local_source) override val id: Long = ID @@ -58,41 +68,34 @@ class LocalSource( override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS) override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { - val baseDirsFiles = getBaseDirectoriesFiles(context) - + 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 } - - 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 + .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 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) - } - } + is OrderBy.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 OrderBy.Latest -> { + mangaDirs = if (filter.state!!.ascending) { + mangaDirs.sortedBy(File::lastModified) + } else { + mangaDirs.sortedByDescending(File::lastModified) } } @@ -109,10 +112,9 @@ class LocalSource( url = mangaDir.name // Try to find the cover - val cover = getCoverFile(mangaDir.name, baseDirsFiles) - if (cover != null && cover.exists()) { - thumbnail_url = cover.absolutePath - } + coverManager.find(mangaDir.name) + ?.takeIf(File::exists) + ?.let { thumbnail_url = it.absolutePath } } } @@ -143,15 +145,13 @@ class LocalSource( // Manga details related override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext { - val baseDirsFile = getBaseDirectoriesFiles(context) - - getCoverFile(manga.url, baseDirsFile)?.let { + coverManager.find(manga.url)?.let { manga.thumbnail_url = it.absolutePath } // Augment manga details based on metadata files try { - val mangaDirFiles = getMangaDirsFiles(manga.url, baseDirsFile).toList() + val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url).toList() val comicInfoFile = mangaDirFiles .firstOrNull { it.name == COMIC_INFO_FILE } @@ -182,10 +182,10 @@ class LocalSource( // Copy ComicInfo.xml from chapter archive to top level if found noXmlFile == null -> { val chapterArchives = mangaDirFiles - .filter { isSupportedArchiveFile(it.extension) } + .filter(Archive::isSupported) .toList() - val mangaDir = getMangaDir(manga.url, baseDirsFile) + val mangaDir = fileSystem.getMangaDirectory(manga.url) val folderPath = mangaDir?.absolutePath val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath) @@ -206,7 +206,7 @@ class LocalSource( private fun copyComicInfoFileFromArchive(chapterArchives: List, folderPath: String?): File? { for (chapter in chapterArchives) { - when (getFormat(chapter)) { + when (Format.valueOf(chapter)) { is Format.Zip -> { ZipFile(chapter).use { zip: ZipFile -> zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile -> @@ -217,7 +217,7 @@ class LocalSource( } } is Format.Rar -> { - Archive(chapter).use { rar: Archive -> + JunrarArchive(chapter).use { rar -> rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile -> rar.getInputStream(comicInfoFile).buffered().use { stream -> return copyComicInfoFile(stream, folderPath) @@ -247,22 +247,11 @@ class LocalSource( 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) + return fileSystem.getFilesInMangaDirectory(manga.url) // Only keep supported formats - .filter { it.isDirectory || isSupportedArchiveFile(it.extension) } + .filter { it.isDirectory || Archive.isSupported(it) } .map { chapterFile -> SChapter.create().apply { url = "${manga.url}/${chapterFile.name}" @@ -274,7 +263,7 @@ class LocalSource( date_upload = chapterFile.lastModified() chapter_number = ChapterRecognition.parseChapterNumber(manga.title, this.name, this.chapter_number) - val format = getFormat(chapterFile) + val format = Format.valueOf(chapterFile) if (format is Format.Epub) { EpubFile(format.file).use { epub -> epub.fillChapterMetadata(this) @@ -290,44 +279,22 @@ class LocalSource( } // 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), - ) + override fun getFilterList() = FilterList(OrderBy.Popular(context)) // 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)) + 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 } } @@ -339,7 +306,7 @@ class LocalSource( ?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } ?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } } - entry?.let { updateCover(context, manga, it.inputStream()) } + entry?.let { coverManager.update(manga, it.inputStream()) } } is Format.Zip -> { ZipFile(format.file).use { zip -> @@ -347,16 +314,16 @@ class LocalSource( .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)) } + entry?.let { coverManager.update(manga, zip.getInputStream(it)) } } } is Format.Rar -> { - Archive(format.file).use { archive -> + 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 { updateCover(context, manga, archive.getInputStream(it)) } + entry?.let { coverManager.update(manga, archive.getInputStream(it)) } } } is Format.Epub -> { @@ -365,7 +332,7 @@ class LocalSource( .firstOrNull() ?.let { epub.getEntry(it) } - entry?.let { updateCover(context, manga, epub.getInputStream(it)) } + entry?.let { coverManager.update(manga, epub.getInputStream(it)) } } } } @@ -375,86 +342,10 @@ class LocalSource( } } - 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://tachiyomi.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/source-local/src/main/java/tachiyomi/source/local/filter/OrderBy.kt b/source-local/src/main/java/tachiyomi/source/local/filter/OrderBy.kt new file mode 100644 index 0000000000..39e91cc6f1 --- /dev/null +++ b/source-local/src/main/java/tachiyomi/source/local/filter/OrderBy.kt @@ -0,0 +1,14 @@ +package tachiyomi.source.local.filter + +import android.content.Context +import eu.kanade.tachiyomi.source.model.Filter +import tachiyomi.source.local.R + +sealed class OrderBy(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) : OrderBy(context, Selection(0, true)) + class Latest(context: Context) : OrderBy(context, Selection(1, false)) +} diff --git a/source-local/src/main/java/tachiyomi/source/local/image/AndroidLocalCoverManager.kt b/source-local/src/main/java/tachiyomi/source/local/image/AndroidLocalCoverManager.kt new file mode 100644 index 0000000000..b1161010eb --- /dev/null +++ b/source-local/src/main/java/tachiyomi/source/local/image/AndroidLocalCoverManager.kt @@ -0,0 +1,55 @@ +package tachiyomi.source.local.image + +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.LocalSourceFileSystem +import java.io.File +import java.io.InputStream + +private const val DEFAULT_COVER_NAME = "cover.jpg" + +class AndroidLocalCoverManager( + private val context: Context, + private val fileSystem: LocalSourceFileSystem, +) : LocalCoverManager { + + 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/LocalCoverManager.kt b/source-local/src/main/java/tachiyomi/source/local/image/LocalCoverManager.kt new file mode 100644 index 0000000000..c8ce8c5457 --- /dev/null +++ b/source-local/src/main/java/tachiyomi/source/local/image/LocalCoverManager.kt @@ -0,0 +1,12 @@ +package tachiyomi.source.local.image + +import eu.kanade.tachiyomi.source.model.SManga +import java.io.File +import java.io.InputStream + +interface LocalCoverManager { + + fun find(mangaUrl: String): File? + + fun update(manga: SManga, inputStream: InputStream): File? +} diff --git a/source-local/src/main/java/tachiyomi/source/local/io/AndroidLocalSourceFileSystem.kt b/source-local/src/main/java/tachiyomi/source/local/io/AndroidLocalSourceFileSystem.kt new file mode 100644 index 0000000000..fa100a0b5f --- /dev/null +++ b/source-local/src/main/java/tachiyomi/source/local/io/AndroidLocalSourceFileSystem.kt @@ -0,0 +1,39 @@ +package tachiyomi.source.local.io + +import android.content.Context +import eu.kanade.tachiyomi.util.storage.DiskUtil +import tachiyomi.source.local.R +import java.io.File + +class AndroidLocalSourceFileSystem( + private val context: Context, +) : LocalSourceFileSystem { + + 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/Archive.kt b/source-local/src/main/java/tachiyomi/source/local/io/Archive.kt new file mode 100644 index 0000000000..b28ee60b5d --- /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 0000000000..a73534f21b --- /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/LocalSourceFileSystem.kt b/source-local/src/main/java/tachiyomi/source/local/io/LocalSourceFileSystem.kt new file mode 100644 index 0000000000..f6c5e6d39f --- /dev/null +++ b/source-local/src/main/java/tachiyomi/source/local/io/LocalSourceFileSystem.kt @@ -0,0 +1,14 @@ +package tachiyomi.source.local.io + +import java.io.File + +interface LocalSourceFileSystem { + + 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 0000000000..d9ce323d5e --- /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 + } + } +}