mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-29 21:37:56 +01:00 
			
		
		
		
	Move Local Source to separate module (#9152)
* Move Local Source to separate module * Review changes
This commit is contained in:
		| @@ -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) | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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, | ||||
| ) | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
| @@ -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() | ||||
|     } | ||||
| } | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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<LocalSourceFileSystem> { AndroidLocalSourceFileSystem(app) } | ||||
|         addSingletonFactory<LocalCoverManager> { AndroidLocalCoverManager(app, get()) } | ||||
|  | ||||
|         // Asynchronously init expensive components for a faster cold start | ||||
|         ContextCompat.getMainExecutor(app).execute { | ||||
|             get<NetworkHelper>() | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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<Long, Source>(mapOf(LocalSource.ID to LocalSource(context))) | ||||
|                     val mutableMap = ConcurrentHashMap<Long, Source>( | ||||
|                         mapOf( | ||||
|                             LocalSource.ID to LocalSource( | ||||
|                                 context, | ||||
|                                 Injekt.get(), | ||||
|                                 Injekt.get(), | ||||
|                             ), | ||||
|                         ), | ||||
|                     ) | ||||
|                     extensions.forEach { extension -> | ||||
|                         extension.sources.forEach { | ||||
|                             mutableMap[it.id] = it | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import cafe.adriel.voyager.navigator.Navigator | ||||
| import cafe.adriel.voyager.navigator.tab.LocalTabNavigator | ||||
| import cafe.adriel.voyager.navigator.tab.TabOptions | ||||
| import eu.kanade.presentation.components.TabbedScreen | ||||
| import eu.kanade.presentation.extensions.RequestStoragePermission | ||||
| import eu.kanade.presentation.util.Tab | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.reader.loader | ||||
| import eu.kanade.tachiyomi.source.model.Page | ||||
| import eu.kanade.tachiyomi.ui.reader.model.ReaderPage | ||||
| import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder | ||||
| import eu.kanade.tachiyomi.util.system.ImageUtil | ||||
| import tachiyomi.core.util.system.ImageUtil | ||||
| import java.io.File | ||||
| import java.io.FileInputStream | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import com.github.junrar.rarfile.FileHeader | ||||
| import eu.kanade.tachiyomi.source.model.Page | ||||
| import eu.kanade.tachiyomi.ui.reader.model.ReaderPage | ||||
| import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder | ||||
| import eu.kanade.tachiyomi.util.system.ImageUtil | ||||
| import tachiyomi.core.util.system.ImageUtil | ||||
| import java.io.File | ||||
| import java.io.InputStream | ||||
| import java.io.PipedInputStream | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import android.os.Build | ||||
| import eu.kanade.tachiyomi.source.model.Page | ||||
| import eu.kanade.tachiyomi.ui.reader.model.ReaderPage | ||||
| import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder | ||||
| import eu.kanade.tachiyomi.util.system.ImageUtil | ||||
| import tachiyomi.core.util.system.ImageUtil | ||||
| import java.io.File | ||||
| import java.nio.charset.StandardCharsets | ||||
| import java.util.zip.ZipFile | ||||
|   | ||||
| @@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage | ||||
| import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView | ||||
| import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator | ||||
| import eu.kanade.tachiyomi.ui.webview.WebViewActivity | ||||
| import eu.kanade.tachiyomi.util.system.ImageUtil | ||||
| import eu.kanade.tachiyomi.widget.ViewPagerAdapter | ||||
| import kotlinx.coroutines.Job | ||||
| import kotlinx.coroutines.MainScope | ||||
| @@ -23,6 +22,7 @@ import kotlinx.coroutines.supervisorScope | ||||
| import tachiyomi.core.util.lang.launchIO | ||||
| import tachiyomi.core.util.lang.withIOContext | ||||
| import tachiyomi.core.util.lang.withUIContext | ||||
| import tachiyomi.core.util.system.ImageUtil | ||||
| import java.io.BufferedInputStream | ||||
| import java.io.ByteArrayInputStream | ||||
| import java.io.InputStream | ||||
|   | ||||
| @@ -18,7 +18,6 @@ import eu.kanade.tachiyomi.ui.reader.model.StencilPage | ||||
| import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView | ||||
| import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator | ||||
| import eu.kanade.tachiyomi.ui.webview.WebViewActivity | ||||
| import eu.kanade.tachiyomi.util.system.ImageUtil | ||||
| import eu.kanade.tachiyomi.util.system.dpToPx | ||||
| import kotlinx.coroutines.Job | ||||
| import kotlinx.coroutines.MainScope | ||||
| @@ -29,6 +28,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine | ||||
| import tachiyomi.core.util.lang.launchIO | ||||
| import tachiyomi.core.util.lang.withIOContext | ||||
| import tachiyomi.core.util.lang.withUIContext | ||||
| import tachiyomi.core.util.system.ImageUtil | ||||
| import java.io.BufferedInputStream | ||||
| import java.io.InputStream | ||||
|  | ||||
|   | ||||
| @@ -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<Long>, 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) | ||||
|   | ||||
| @@ -46,7 +46,6 @@ import tachiyomi.core.util.system.logcat | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.io.File | ||||
| import kotlin.math.max | ||||
| import kotlin.math.roundToInt | ||||
|  | ||||
| /** | ||||
| @@ -113,9 +112,6 @@ fun Context.hasPermission(permission: String) = ContextCompat.checkSelfPermissio | ||||
|     } | ||||
| } | ||||
|  | ||||
| val getDisplayMaxHeightInPx: Int | ||||
|     get() = Resources.getSystem().displayMetrics.let { max(it.heightPixels, it.widthPixels) } | ||||
|  | ||||
| /** | ||||
|  * Converts to px and takes into account LTR/RTL layout. | ||||
|  */ | ||||
|   | ||||
							
								
								
									
										1
									
								
								core-metadata/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								core-metadata/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| /build | ||||
							
								
								
									
										21
									
								
								core-metadata/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								core-metadata/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| plugins { | ||||
|     id("com.android.library") | ||||
|     kotlin("android") | ||||
|     kotlin("plugin.serialization") | ||||
| } | ||||
|  | ||||
| android { | ||||
|     namespace = "tachiyomi.core.metadata" | ||||
|  | ||||
|     defaultConfig { | ||||
|         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" | ||||
|         consumerProguardFiles("consumer-rules.pro") | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| dependencies { | ||||
|     implementation(project(":source-api")) | ||||
|  | ||||
|     implementation(kotlinx.bundles.serialization) | ||||
| } | ||||
							
								
								
									
										0
									
								
								core-metadata/consumer-rules.pro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								core-metadata/consumer-rules.pro
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										21
									
								
								core-metadata/proguard-rules.pro
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								core-metadata/proguard-rules.pro
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| # Add project specific ProGuard rules here. | ||||
| # You can control the set of applied configuration files using the | ||||
| # proguardFiles setting in build.gradle. | ||||
| # | ||||
| # For more details, see | ||||
| #   http://developer.android.com/guide/developing/tools/proguard.html | ||||
|  | ||||
| # If your project uses WebView with JS, uncomment the following | ||||
| # and specify the fully qualified class name to the JavaScript interface | ||||
| # class: | ||||
| #-keepclassmembers class fqcn.of.javascript.interface.for.webview { | ||||
| #   public *; | ||||
| #} | ||||
|  | ||||
| # Uncomment this to preserve the line number information for | ||||
| # debugging stack traces. | ||||
| #-keepattributes SourceFile,LineNumberTable | ||||
|  | ||||
| # If you keep the line number information, uncomment this to | ||||
| # hide the original source file name. | ||||
| #-renamesourcefileattribute SourceFile | ||||
							
								
								
									
										2
									
								
								core-metadata/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								core-metadata/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest /> | ||||
| @@ -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, | ||||
| ) { | ||||
| @@ -0,0 +1,13 @@ | ||||
| package tachiyomi.core.metadata.tachiyomi | ||||
|  | ||||
| import kotlinx.serialization.Serializable | ||||
|  | ||||
| @Serializable | ||||
| class MangaDetails( | ||||
|     val title: String? = null, | ||||
|     val author: String? = null, | ||||
|     val artist: String? = null, | ||||
|     val description: String? = null, | ||||
|     val genre: List<String>? = null, | ||||
|     val status: Int? = null, | ||||
| ) | ||||
| @@ -27,12 +27,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) | ||||
| } | ||||
|   | ||||
| @@ -1,15 +1,11 @@ | ||||
| package eu.kanade.tachiyomi.util.storage | ||||
| 
 | ||||
| import android.Manifest | ||||
| import android.content.Context | ||||
| import android.media.MediaScannerConnection | ||||
| import android.net.Uri | ||||
| import android.os.Environment | ||||
| import android.os.StatFs | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.core.content.ContextCompat | ||||
| import com.google.accompanist.permissions.rememberPermissionState | ||||
| import com.hippo.unifile.UniFile | ||||
| import eu.kanade.tachiyomi.util.lang.Hash | ||||
| import java.io.File | ||||
| @@ -117,16 +113,5 @@ object DiskUtil { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Launches request for [Manifest.permission.WRITE_EXTERNAL_STORAGE] permission | ||||
|      */ | ||||
|     @Composable | ||||
|     fun RequestStoragePermission() { | ||||
|         val permissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE) | ||||
|         LaunchedEffect(Unit) { | ||||
|             permissionState.launchPermissionRequest() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     const val NOMEDIA_FILE = ".nomedia" | ||||
| } | ||||
| @@ -1,15 +1,10 @@ | ||||
| package eu.kanade.tachiyomi.util.storage | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.source.model.SChapter | ||||
| import eu.kanade.tachiyomi.source.model.SManga | ||||
| import org.jsoup.Jsoup | ||||
| import org.jsoup.nodes.Document | ||||
| import java.io.Closeable | ||||
| import java.io.File | ||||
| import java.io.InputStream | ||||
| import java.text.ParseException | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.Locale | ||||
| import java.util.zip.ZipEntry | ||||
| import java.util.zip.ZipFile | ||||
| 
 | ||||
| @@ -49,58 +44,6 @@ class EpubFile(file: File) : Closeable { | ||||
|         return zip.getEntry(name) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fills manga metadata using this epub file's metadata. | ||||
|      */ | ||||
|     fun fillMangaMetadata(manga: SManga) { | ||||
|         val ref = getPackageHref() | ||||
|         val doc = getPackageDocument(ref) | ||||
| 
 | ||||
|         val creator = doc.getElementsByTag("dc:creator").first() | ||||
|         val description = doc.getElementsByTag("dc:description").first() | ||||
| 
 | ||||
|         manga.author = creator?.text() | ||||
|         manga.description = description?.text() | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fills chapter metadata using this epub file's metadata. | ||||
|      */ | ||||
|     fun fillChapterMetadata(chapter: SChapter) { | ||||
|         val ref = getPackageHref() | ||||
|         val doc = getPackageDocument(ref) | ||||
| 
 | ||||
|         val title = doc.getElementsByTag("dc:title").first() | ||||
|         val publisher = doc.getElementsByTag("dc:publisher").first() | ||||
|         val creator = doc.getElementsByTag("dc:creator").first() | ||||
|         var date = doc.getElementsByTag("dc:date").first() | ||||
|         if (date == null) { | ||||
|             date = doc.select("meta[property=dcterms:modified]").first() | ||||
|         } | ||||
| 
 | ||||
|         if (title != null) { | ||||
|             chapter.name = title.text() | ||||
|         } | ||||
| 
 | ||||
|         if (publisher != null) { | ||||
|             chapter.scanlator = publisher.text() | ||||
|         } else if (creator != null) { | ||||
|             chapter.scanlator = creator.text() | ||||
|         } | ||||
| 
 | ||||
|         if (date != null) { | ||||
|             val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()) | ||||
|             try { | ||||
|                 val parsedDate = dateFormat.parse(date.text()) | ||||
|                 if (parsedDate != null) { | ||||
|                     chapter.date_upload = parsedDate.time | ||||
|                 } | ||||
|             } catch (e: ParseException) { | ||||
|                 // Empty | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the path of all the images found in the epub file. | ||||
|      */ | ||||
| @@ -114,7 +57,7 @@ class EpubFile(file: File) : Closeable { | ||||
|     /** | ||||
|      * Returns the path to the package document. | ||||
|      */ | ||||
|     private fun getPackageHref(): String { | ||||
|     fun getPackageHref(): String { | ||||
|         val meta = zip.getEntry(resolveZipPath("META-INF", "container.xml")) | ||||
|         if (meta != null) { | ||||
|             val metaDoc = zip.getInputStream(meta).use { Jsoup.parse(it, null, "") } | ||||
| @@ -129,7 +72,7 @@ class EpubFile(file: File) : Closeable { | ||||
|     /** | ||||
|      * Returns the package document where all the files are listed. | ||||
|      */ | ||||
|     private fun getPackageDocument(ref: String): Document { | ||||
|     fun getPackageDocument(ref: String): Document { | ||||
|         val entry = zip.getEntry(ref) | ||||
|         return zip.getInputStream(entry).use { Jsoup.parse(it, null, "") } | ||||
|     } | ||||
| @@ -137,7 +80,7 @@ class EpubFile(file: File) : Closeable { | ||||
|     /** | ||||
|      * Returns all the pages from the epub. | ||||
|      */ | ||||
|     private fun getPagesFromDocument(document: Document): List<String> { | ||||
|     fun getPagesFromDocument(document: Document): List<String> { | ||||
|         val pages = document.select("manifest > item") | ||||
|             .filter { node -> "application/xhtml+xml" == node.attr("media-type") } | ||||
|             .associateBy { it.attr("id") } | ||||
| @@ -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) } | ||||
| @@ -45,3 +45,5 @@ include(":data") | ||||
| include(":domain") | ||||
| include(":presentation-widget") | ||||
| include(":presentation-core") | ||||
| include(":source-local") | ||||
| include(":core-metadata") | ||||
|   | ||||
							
								
								
									
										1
									
								
								source-local/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								source-local/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| /build | ||||
							
								
								
									
										29
									
								
								source-local/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								source-local/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| } | ||||
							
								
								
									
										0
									
								
								source-local/consumer-rules.pro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								source-local/consumer-rules.pro
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										21
									
								
								source-local/proguard-rules.pro
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								source-local/proguard-rules.pro
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| # Add project specific ProGuard rules here. | ||||
| # You can control the set of applied configuration files using the | ||||
| # proguardFiles setting in build.gradle. | ||||
| # | ||||
| # For more details, see | ||||
| #   http://developer.android.com/guide/developing/tools/proguard.html | ||||
|  | ||||
| # If your project uses WebView with JS, uncomment the following | ||||
| # and specify the fully qualified class name to the JavaScript interface | ||||
| # class: | ||||
| #-keepclassmembers class fqcn.of.javascript.interface.for.webview { | ||||
| #   public *; | ||||
| #} | ||||
|  | ||||
| # Uncomment this to preserve the line number information for | ||||
| # debugging stack traces. | ||||
| #-keepattributes SourceFile,LineNumberTable | ||||
|  | ||||
| # If you keep the line number information, uncomment this to | ||||
| # hide the original source file name. | ||||
| #-renamesourcefileattribute SourceFile | ||||
							
								
								
									
										2
									
								
								source-local/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								source-local/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest /> | ||||
| @@ -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<MangasPage> { | ||||
|         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<File>, 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<String>? = null, | ||||
|         val status: Int? = null, | ||||
|     ) | ||||
| 
 | ||||
|     // Chapters | ||||
|     override suspend fun getChapterList(manga: SManga): List<SChapter> { | ||||
|         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<File> { | ||||
|             val localFolder = context.getString(R.string.app_name) + File.separator + "local" | ||||
|             return DiskUtil.getExternalStorages(context) | ||||
|                 .map { File(it.absolutePath, localFolder) } | ||||
|                 .asSequence() | ||||
|         } | ||||
| 
 | ||||
|         private fun getBaseDirectoriesFiles(context: Context): Sequence<File> { | ||||
|             return getBaseDirectories(context) | ||||
|                 // Get all the files inside all baseDir | ||||
|                 .flatMap { it.listFiles().orEmpty().toList() } | ||||
|         } | ||||
| 
 | ||||
|         private fun getMangaDir(mangaUrl: String, baseDirsFile: Sequence<File>): File? { | ||||
|             return baseDirsFile | ||||
|                 // Get the first mangaDir or null | ||||
|                 .firstOrNull { it.isDirectory && it.name == mangaUrl } | ||||
|         } | ||||
| 
 | ||||
|         private fun getMangaDirsFiles(mangaUrl: String, baseDirsFile: Sequence<File>): Sequence<File> { | ||||
|             return baseDirsFile | ||||
|                 // Filter out ones that are not related to the manga and is not a directory | ||||
|                 .filter { it.isDirectory && it.name == mangaUrl } | ||||
|                 // Get all the files inside the filtered folders | ||||
|                 .flatMap { it.listFiles().orEmpty().toList() } | ||||
|         } | ||||
| 
 | ||||
|         private fun getCoverFile(mangaUrl: String, baseDirsFile: Sequence<File>): File? { | ||||
|             return getMangaDirsFiles(mangaUrl, baseDirsFile) | ||||
|                 // Get all file whose names start with 'cover' | ||||
|                 .filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) } | ||||
|                 // Get the first actual image | ||||
|                 .firstOrNull { | ||||
|                     ImageUtil.isImage(it.name) { it.inputStream() } | ||||
|                 } | ||||
|         } | ||||
| 
 | ||||
|         fun updateCover(context: Context, manga: SManga, inputStream: InputStream): File? { | ||||
|             val baseDirsFiles = getBaseDirectoriesFiles(context) | ||||
| 
 | ||||
|             val mangaDir = getMangaDir(manga.url, baseDirsFiles) | ||||
|             if (mangaDir == null) { | ||||
|                 inputStream.close() | ||||
|                 return null | ||||
|             } | ||||
| 
 | ||||
|             var coverFile = getCoverFile(manga.url, baseDirsFiles) | ||||
|             if (coverFile == null) { | ||||
|                 coverFile = File(mangaDir.absolutePath, DEFAULT_COVER_NAME) | ||||
|                 coverFile.createNewFile() | ||||
|             } | ||||
| 
 | ||||
|             // It might not exist at this point | ||||
|             coverFile.parentFile?.mkdirs() | ||||
|             inputStream.use { input -> | ||||
|                 coverFile.outputStream().use { output -> | ||||
|                     input.copyTo(output) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             DiskUtil.createNoMediaFile(UniFile.fromFile(mangaDir), context) | ||||
| 
 | ||||
|             manga.thumbnail_url = coverFile.absolutePath | ||||
|             return coverFile | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub") | ||||
| @@ -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)) | ||||
| } | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
| @@ -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? | ||||
| } | ||||
| @@ -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<File> { | ||||
|         return DiskUtil.getExternalStorages(context) | ||||
|             .map { File(it.absolutePath, baseFolderLocation) } | ||||
|             .asSequence() | ||||
|     } | ||||
|  | ||||
|     override fun getFilesInBaseDirectories(): Sequence<File> { | ||||
|         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<File> { | ||||
|         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() } | ||||
|     } | ||||
| } | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
| @@ -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() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,14 @@ | ||||
| package tachiyomi.source.local.io | ||||
|  | ||||
| import java.io.File | ||||
|  | ||||
| interface LocalSourceFileSystem { | ||||
|  | ||||
|     fun getBaseDirectories(): Sequence<File> | ||||
|  | ||||
|     fun getFilesInBaseDirectories(): Sequence<File> | ||||
|  | ||||
|     fun getMangaDirectory(name: String): File? | ||||
|  | ||||
|     fun getFilesInMangaDirectory(name: String): Sequence<File> | ||||
| } | ||||
| @@ -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 | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user