Move Local Source to separate module (#9152)
* Move Local Source to separate module * Review changes (cherry picked from commit f27dc19b378f2f38ed2885f40d93f6d8817ef584) # Conflicts: # app/src/main/java/eu/kanade/domain/source/interactor/GetEnabledSources.kt # app/src/main/java/eu/kanade/tachiyomi/AppModule.kt # app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt # app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt # app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt # app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt # core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt # source-local/src/main/java/tachiyomi/source/local/LocalSource.kt
This commit is contained in:
parent
2b96fa46ba
commit
b5b5dd0e81
@ -138,7 +138,9 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":i18n"))
|
implementation(project(":i18n"))
|
||||||
implementation(project(":core"))
|
implementation(project(":core"))
|
||||||
|
implementation(project(":core-metadata"))
|
||||||
implementation(project(":source-api"))
|
implementation(project(":source-api"))
|
||||||
|
implementation(project(":source-local"))
|
||||||
implementation(project(":data"))
|
implementation(project(":data"))
|
||||||
implementation(project(":domain"))
|
implementation(project(":domain"))
|
||||||
implementation(project(":presentation-core"))
|
implementation(project(":presentation-core"))
|
||||||
@ -198,7 +200,7 @@ dependencies {
|
|||||||
// TLS 1.3 support for Android < 10
|
// TLS 1.3 support for Android < 10
|
||||||
implementation(libs.conscrypt.android)
|
implementation(libs.conscrypt.android)
|
||||||
|
|
||||||
// Data serialization (JSON, protobuf)
|
// Data serialization (JSON, protobuf, xml)
|
||||||
implementation(kotlinx.bundles.serialization)
|
implementation(kotlinx.bundles.serialization)
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
@ -222,9 +224,6 @@ dependencies {
|
|||||||
}
|
}
|
||||||
implementation(libs.image.decoder)
|
implementation(libs.image.decoder)
|
||||||
|
|
||||||
// Sort
|
|
||||||
implementation(libs.natural.comparator)
|
|
||||||
|
|
||||||
// UI libraries
|
// UI libraries
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
implementation(libs.flexible.adapter.core)
|
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.model.SourcePagingSourceType
|
||||||
import eu.kanade.domain.source.repository.SourceRepository
|
import eu.kanade.domain.source.repository.SourceRepository
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.online.all.EHentai
|
import eu.kanade.tachiyomi.source.online.all.EHentai
|
||||||
@ -13,6 +12,7 @@ import kotlinx.coroutines.flow.map
|
|||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.DatabaseHandler
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
import tachiyomi.domain.source.model.SourceWithCount
|
import tachiyomi.domain.source.model.SourceWithCount
|
||||||
|
import tachiyomi.source.local.LocalSource
|
||||||
|
|
||||||
class SourceRepositoryImpl(
|
class SourceRepositoryImpl(
|
||||||
private val sourceManager: SourceManager,
|
private val sourceManager: SourceManager,
|
||||||
|
@ -51,7 +51,7 @@ fun List<ChapterItem>.applyFilters(manga: Manga): Sequence<ChapterItem> {
|
|||||||
.filter { applyFilter(downloadedFilter) { it.isDownloaded || isLocalManga } }
|
.filter { applyFilter(downloadedFilter) { it.isDownloaded || isLocalManga } }
|
||||||
// SY -->
|
// SY -->
|
||||||
.filter { chapter ->
|
.filter { chapter ->
|
||||||
manga.filteredScanlators.isNullOrEmpty() || MdUtil.getScanlators(chapter.scanlator).any { group -> manga.filteredScanlators!!.contains(group) }
|
manga.filteredScanlators.isNullOrEmpty() || MdUtil.getScanlators(chapter.chapter.scanlator).any { group -> manga.filteredScanlators!!.contains(group) }
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
.sortedWith { (chapter1), (chapter2) -> getChapterSort(manga).invoke(chapter1, chapter2) }
|
.sortedWith { (chapter1), (chapter2) -> getChapterSort(manga).invoke(chapter1, chapter2) }
|
||||||
|
@ -2,12 +2,13 @@ package eu.kanade.domain.manga.model
|
|||||||
|
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
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.Manga
|
||||||
import tachiyomi.domain.manga.model.TriStateFilter
|
import tachiyomi.domain.manga.model.TriStateFilter
|
||||||
|
import tachiyomi.source.local.LocalSource
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
@ -99,3 +100,25 @@ fun Manga.isLocal(): Boolean = source == LocalSource.ID
|
|||||||
fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
|
fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
|
||||||
return coverCache.getCustomCoverFile(id).exists()
|
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,7 +2,6 @@ package eu.kanade.domain.source.interactor
|
|||||||
|
|
||||||
import eu.kanade.domain.source.repository.SourceRepository
|
import eu.kanade.domain.source.repository.SourceRepository
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import exh.source.BlacklistedSources
|
import exh.source.BlacklistedSources
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
@ -10,6 +9,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
|||||||
import tachiyomi.domain.source.model.Pin
|
import tachiyomi.domain.source.model.Pin
|
||||||
import tachiyomi.domain.source.model.Pins
|
import tachiyomi.domain.source.model.Pins
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
|
import tachiyomi.source.local.LocalSource
|
||||||
|
|
||||||
class GetEnabledSources(
|
class GetEnabledSources(
|
||||||
private val repository: SourceRepository,
|
private val repository: SourceRepository,
|
||||||
|
@ -23,7 +23,6 @@ import eu.kanade.presentation.browse.components.BrowseSourceEHentaiList
|
|||||||
import eu.kanade.presentation.browse.components.BrowseSourceList
|
import eu.kanade.presentation.browse.components.BrowseSourceList
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||||
@ -35,6 +34,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
|||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreenAction
|
import tachiyomi.presentation.core.screens.EmptyScreenAction
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
import tachiyomi.source.local.LocalSource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseSourceContent(
|
fun BrowseSourceContent(
|
||||||
|
@ -30,7 +30,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
import eu.kanade.presentation.browse.components.BaseSourceItem
|
||||||
import eu.kanade.tachiyomi.R
|
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.SourcesState
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
@ -43,6 +42,7 @@ import tachiyomi.presentation.core.screens.EmptyScreen
|
|||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
import tachiyomi.presentation.core.theme.header
|
import tachiyomi.presentation.core.theme.header
|
||||||
import tachiyomi.presentation.core.util.plus
|
import tachiyomi.presentation.core.util.plus
|
||||||
|
import tachiyomi.source.local.LocalSource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SourcesScreen(
|
fun SourcesScreen(
|
||||||
|
@ -31,9 +31,9 @@ import eu.kanade.domain.source.model.icon
|
|||||||
import eu.kanade.presentation.util.rememberResourceBitmapPainter
|
import eu.kanade.presentation.util.rememberResourceBitmapPainter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
|
import tachiyomi.source.local.LocalSource
|
||||||
|
|
||||||
private val defaultModifier = Modifier
|
private val defaultModifier = Modifier
|
||||||
.height(40.dp)
|
.height(40.dp)
|
||||||
|
@ -21,10 +21,10 @@ import eu.kanade.presentation.components.RadioMenuItem
|
|||||||
import eu.kanade.presentation.components.SearchToolbar
|
import eu.kanade.presentation.components.SearchToolbar
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import exh.source.anyIs
|
import exh.source.anyIs
|
||||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||||
|
import tachiyomi.source.local.LocalSource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseSourceToolbar(
|
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 androidx.core.net.toUri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.domain.backup.service.BackupPreferences
|
import eu.kanade.domain.backup.service.BackupPreferences
|
||||||
|
import eu.kanade.presentation.extensions.RequestStoragePermission
|
||||||
import eu.kanade.presentation.more.settings.Preference
|
import eu.kanade.presentation.more.settings.Preference
|
||||||
import eu.kanade.presentation.util.collectAsState
|
import eu.kanade.presentation.util.collectAsState
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
@ -56,6 +56,10 @@ import tachiyomi.data.listOfStringsAdapter
|
|||||||
import tachiyomi.data.listOfStringsAndAdapter
|
import tachiyomi.data.listOfStringsAndAdapter
|
||||||
import tachiyomi.data.updateStrategyAdapter
|
import tachiyomi.data.updateStrategyAdapter
|
||||||
import tachiyomi.domain.manga.interactor.GetCustomMangaInfo
|
import tachiyomi.domain.manga.interactor.GetCustomMangaInfo
|
||||||
|
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.InjektModule
|
||||||
import uy.kohesive.injekt.api.InjektRegistrar
|
import uy.kohesive.injekt.api.InjektRegistrar
|
||||||
import uy.kohesive.injekt.api.addSingleton
|
import uy.kohesive.injekt.api.addSingleton
|
||||||
@ -149,6 +153,9 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
|
|
||||||
addSingletonFactory { ImageSaver(app) }
|
addSingletonFactory { ImageSaver(app) }
|
||||||
|
|
||||||
|
addSingletonFactory<LocalSourceFileSystem> { AndroidLocalSourceFileSystem(app) }
|
||||||
|
addSingletonFactory<LocalCoverManager> { AndroidLocalCoverManager(app, get()) }
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
addSingletonFactory { EHentaiUpdateHelper(app) }
|
addSingletonFactory { EHentaiUpdateHelper(app) }
|
||||||
|
|
||||||
|
@ -9,8 +9,8 @@ import coil.decode.ImageDecoderDecoder
|
|||||||
import coil.decode.ImageSource
|
import coil.decode.ImageSource
|
||||||
import coil.fetch.SourceResult
|
import coil.fetch.SourceResult
|
||||||
import coil.request.Options
|
import coil.request.Options
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
|
import tachiyomi.core.util.system.ImageUtil
|
||||||
import tachiyomi.decoder.ImageDecoder
|
import tachiyomi.decoder.ImageDecoder
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,7 +22,6 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
|||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE
|
import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE
|
||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
|
||||||
import exh.util.DataSaver
|
import exh.util.DataSaver
|
||||||
import exh.util.DataSaver.Companion.getImage
|
import exh.util.DataSaver.Companion.getImage
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
@ -48,6 +47,7 @@ import tachiyomi.core.util.lang.launchIO
|
|||||||
import tachiyomi.core.util.lang.launchNow
|
import tachiyomi.core.util.lang.launchNow
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import tachiyomi.core.util.lang.withUIContext
|
import tachiyomi.core.util.lang.withUIContext
|
||||||
|
import tachiyomi.core.util.system.ImageUtil
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.manga.model.Manga
|
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.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import okio.IOException
|
import okio.IOException
|
||||||
|
import tachiyomi.core.util.system.ImageUtil
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
@ -4,6 +4,7 @@ import android.graphics.drawable.Drawable
|
|||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import tachiyomi.domain.source.model.SourceData
|
import tachiyomi.domain.source.model.SourceData
|
||||||
|
import tachiyomi.source.local.LocalSource
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
@ -42,6 +42,9 @@ import kotlinx.coroutines.runBlocking
|
|||||||
import rx.Observable
|
import rx.Observable
|
||||||
import tachiyomi.domain.source.model.SourceData
|
import tachiyomi.domain.source.model.SourceData
|
||||||
import tachiyomi.domain.source.repository.SourceDataRepository
|
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 uy.kohesive.injekt.injectLazy
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
@ -75,7 +78,18 @@ class SourceManager(
|
|||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
.collectLatest { (extensions, enableExhentai) ->
|
.collectLatest { (extensions, enableExhentai) ->
|
||||||
val mutableMap = ConcurrentHashMap<Long, Source>(mapOf(LocalSource.ID to LocalSource(context))).apply {
|
val mutableMap = ConcurrentHashMap<Long, Source>(
|
||||||
|
mapOf(
|
||||||
|
LocalSource.ID to LocalSource(
|
||||||
|
context,
|
||||||
|
Injekt.get(),
|
||||||
|
Injekt.get(),
|
||||||
|
// SY -->
|
||||||
|
preferences.allowLocalSourceHiddenFolders()::get,
|
||||||
|
// SY <--
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).apply {
|
||||||
// SY -->
|
// SY -->
|
||||||
put(EH_SOURCE_ID, EHentai(EH_SOURCE_ID, false, context))
|
put(EH_SOURCE_ID, EHentai(EH_SOURCE_ID, false, context))
|
||||||
if (enableExhentai) {
|
if (enableExhentai) {
|
||||||
|
@ -18,6 +18,7 @@ import cafe.adriel.voyager.navigator.tab.TabOptions
|
|||||||
import eu.kanade.core.prefs.asState
|
import eu.kanade.core.prefs.asState
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
import eu.kanade.presentation.components.TabbedScreen
|
import eu.kanade.presentation.components.TabbedScreen
|
||||||
|
import eu.kanade.presentation.extensions.RequestStoragePermission
|
||||||
import eu.kanade.presentation.util.Tab
|
import eu.kanade.presentation.util.Tab
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
|
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
|
||||||
|
@ -16,7 +16,6 @@ import eu.kanade.presentation.browse.BrowseSourceContent
|
|||||||
import eu.kanade.presentation.browse.components.BrowseSourceFloatingActionButton
|
import eu.kanade.presentation.browse.components.BrowseSourceFloatingActionButton
|
||||||
import eu.kanade.presentation.components.SearchToolbar
|
import eu.kanade.presentation.components.SearchToolbar
|
||||||
import eu.kanade.presentation.util.Screen
|
import eu.kanade.presentation.util.Screen
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen
|
import eu.kanade.tachiyomi.ui.browse.migration.advanced.process.MigrationListScreen
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
|
||||||
@ -25,6 +24,7 @@ import eu.kanade.tachiyomi.ui.webview.WebViewScreen
|
|||||||
import tachiyomi.core.Constants
|
import tachiyomi.core.Constants
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
import tachiyomi.source.local.LocalSource
|
||||||
|
|
||||||
data class SourceSearchScreen(
|
data class SourceSearchScreen(
|
||||||
private val oldManga: Manga,
|
private val oldManga: Manga,
|
||||||
|
@ -48,7 +48,6 @@ import eu.kanade.presentation.util.AssistContentScreen
|
|||||||
import eu.kanade.presentation.util.Screen
|
import eu.kanade.presentation.util.Screen
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen
|
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen
|
||||||
@ -67,6 +66,7 @@ import tachiyomi.core.util.lang.launchIO
|
|||||||
import tachiyomi.presentation.core.components.material.Divider
|
import tachiyomi.presentation.core.components.material.Divider
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.source.local.LocalSource
|
||||||
|
|
||||||
data class BrowseSourceScreen(
|
data class BrowseSourceScreen(
|
||||||
private val sourceId: Long,
|
private val sourceId: Long,
|
||||||
|
@ -39,7 +39,6 @@ import eu.kanade.tachiyomi.data.download.DownloadCache
|
|||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackStatus
|
import eu.kanade.tachiyomi.data.track.TrackStatus
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
@ -104,6 +103,7 @@ import tachiyomi.domain.manga.model.applyFilter
|
|||||||
import tachiyomi.domain.track.interactor.GetTracks
|
import tachiyomi.domain.track.interactor.GetTracks
|
||||||
import tachiyomi.domain.track.interactor.GetTracksPerManga
|
import tachiyomi.domain.track.interactor.GetTracksPerManga
|
||||||
import tachiyomi.domain.track.model.Track
|
import tachiyomi.domain.track.model.Track
|
||||||
|
import tachiyomi.source.local.LocalSource
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
|
@ -121,7 +121,7 @@ class MangaCoverScreenModel(
|
|||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
context.contentResolver.openInputStream(data)?.use {
|
context.contentResolver.openInputStream(data)?.use {
|
||||||
try {
|
try {
|
||||||
manga.editCover(context, it, updateManga, coverCache)
|
manga.editCover(Injekt.get(), it, updateManga, coverCache)
|
||||||
notifyCoverUpdated(context)
|
notifyCoverUpdated(context)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
notifyFailedCoverUpdate(context, e)
|
notifyFailedCoverUpdate(context, e)
|
||||||
|
@ -46,7 +46,6 @@ import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
|||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.network.HttpException
|
import eu.kanade.tachiyomi.network.HttpException
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import eu.kanade.tachiyomi.source.PagePreviewSource
|
import eu.kanade.tachiyomi.source.PagePreviewSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
@ -116,6 +115,7 @@ import tachiyomi.domain.manga.model.TriStateFilter
|
|||||||
import tachiyomi.domain.manga.model.applyFilter
|
import tachiyomi.domain.manga.model.applyFilter
|
||||||
import tachiyomi.domain.track.interactor.GetTracks
|
import tachiyomi.domain.track.interactor.GetTracks
|
||||||
import tachiyomi.domain.track.interactor.InsertTrack
|
import tachiyomi.domain.track.interactor.InsertTrack
|
||||||
|
import tachiyomi.source.local.LocalSource
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
@ -54,7 +54,6 @@ import eu.kanade.tachiyomi.util.lang.byteSize
|
|||||||
import eu.kanade.tachiyomi.util.lang.takeBytes
|
import eu.kanade.tachiyomi.util.lang.takeBytes
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
|
||||||
import eu.kanade.tachiyomi.util.system.isOnline
|
import eu.kanade.tachiyomi.util.system.isOnline
|
||||||
import exh.md.utils.FollowStatus
|
import exh.md.utils.FollowStatus
|
||||||
import exh.md.utils.MdUtil
|
import exh.md.utils.MdUtil
|
||||||
@ -86,6 +85,7 @@ import tachiyomi.core.util.lang.launchIO
|
|||||||
import tachiyomi.core.util.lang.launchNonCancellable
|
import tachiyomi.core.util.lang.launchNonCancellable
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import tachiyomi.core.util.lang.withUIContext
|
import tachiyomi.core.util.lang.withUIContext
|
||||||
|
import tachiyomi.core.util.system.ImageUtil
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.decoder.ImageDecoder
|
import tachiyomi.decoder.ImageDecoder
|
||||||
import tachiyomi.domain.chapter.interactor.GetChapterByMangaId
|
import tachiyomi.domain.chapter.interactor.GetChapterByMangaId
|
||||||
@ -989,7 +989,7 @@ class ReaderViewModel(
|
|||||||
|
|
||||||
viewModelScope.launchNonCancellable {
|
viewModelScope.launchNonCancellable {
|
||||||
val result = try {
|
val result = try {
|
||||||
manga.editCover(context, stream())
|
manga.editCover(Injekt.get(), stream())
|
||||||
if (manga.isLocal() || manga.favorite) {
|
if (manga.isLocal() || manga.favorite) {
|
||||||
SetAsCoverResult.Success
|
SetAsCoverResult.Success
|
||||||
} else {
|
} else {
|
||||||
|
@ -5,7 +5,6 @@ import com.github.junrar.exception.UnsupportedRarV5Exception
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
@ -16,6 +15,8 @@ import tachiyomi.core.util.lang.withIOContext
|
|||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.MergedMangaReference
|
import tachiyomi.domain.manga.model.MergedMangaReference
|
||||||
|
import tachiyomi.source.local.LocalSource
|
||||||
|
import tachiyomi.source.local.io.Format
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loader used to retrieve the [PageLoader] for a given chapter.
|
* Loader used to retrieve the [PageLoader] for a given chapter.
|
||||||
@ -98,10 +99,14 @@ class ChapterLoader(
|
|||||||
source is HttpSource -> HttpPageLoader(chapter, source)
|
source is HttpSource -> HttpPageLoader(chapter, source)
|
||||||
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
|
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
|
||||||
when (format) {
|
when (format) {
|
||||||
is LocalSource.Format.Directory -> DirectoryPageLoader(format.file)
|
is Format.Directory -> DirectoryPageLoader(format.file)
|
||||||
is LocalSource.Format.Zip -> ZipPageLoader(format.file)
|
is Format.Zip -> ZipPageLoader(format.file)
|
||||||
is LocalSource.Format.Rar -> RarPageLoader(format.file)
|
is Format.Rar -> try {
|
||||||
is LocalSource.Format.Epub -> EpubPageLoader(format.file)
|
RarPageLoader(format.file)
|
||||||
|
} catch (e: UnsupportedRarV5Exception) {
|
||||||
|
error(context.getString(R.string.loader_rar5_error))
|
||||||
|
}
|
||||||
|
is Format.Epub -> EpubPageLoader(format.file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> error(context.getString(R.string.loader_not_implemented_error))
|
else -> error(context.getString(R.string.loader_not_implemented_error))
|
||||||
@ -112,14 +117,14 @@ class ChapterLoader(
|
|||||||
source is HttpSource -> HttpPageLoader(chapter, source)
|
source is HttpSource -> HttpPageLoader(chapter, source)
|
||||||
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
|
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
|
||||||
when (format) {
|
when (format) {
|
||||||
is LocalSource.Format.Directory -> DirectoryPageLoader(format.file)
|
is Format.Directory -> DirectoryPageLoader(format.file)
|
||||||
is LocalSource.Format.Zip -> ZipPageLoader(format.file)
|
is Format.Zip -> ZipPageLoader(format.file)
|
||||||
is LocalSource.Format.Rar -> try {
|
is Format.Rar -> try {
|
||||||
RarPageLoader(format.file)
|
RarPageLoader(format.file)
|
||||||
} catch (e: UnsupportedRarV5Exception) {
|
} catch (e: UnsupportedRarV5Exception) {
|
||||||
error(context.getString(R.string.loader_rar5_error))
|
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()
|
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.source.model.Page
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
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.File
|
||||||
import java.io.FileInputStream
|
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.source.model.Page
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
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.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.PipedInputStream
|
import java.io.PipedInputStream
|
||||||
|
@ -4,7 +4,7 @@ import android.os.Build
|
|||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
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.File
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.util.zip.ZipFile
|
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.ReaderPageImageView
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
|
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
|
||||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
|
||||||
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
|
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
@ -25,6 +24,7 @@ import logcat.LogPriority
|
|||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import tachiyomi.core.util.lang.withUIContext
|
import tachiyomi.core.util.lang.withUIContext
|
||||||
|
import tachiyomi.core.util.system.ImageUtil
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.decoder.ImageDecoder
|
import tachiyomi.decoder.ImageDecoder
|
||||||
import java.io.BufferedInputStream
|
import java.io.BufferedInputStream
|
||||||
|
@ -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.ReaderPageImageView
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
|
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
|
||||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
|
||||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
@ -29,6 +28,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine
|
|||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import tachiyomi.core.util.lang.withUIContext
|
import tachiyomi.core.util.lang.withUIContext
|
||||||
|
import tachiyomi.core.util.system.ImageUtil
|
||||||
import java.io.BufferedInputStream
|
import java.io.BufferedInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.util
|
package eu.kanade.tachiyomi.util
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.domain.download.service.DownloadPreferences
|
import eu.kanade.domain.download.service.DownloadPreferences
|
||||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||||
import eu.kanade.domain.manga.model.hasCustomCover
|
import eu.kanade.domain.manga.model.hasCustomCover
|
||||||
import eu.kanade.domain.manga.model.isLocal
|
import eu.kanade.domain.manga.model.isLocal
|
||||||
import eu.kanade.domain.manga.model.toSManga
|
import eu.kanade.domain.manga.model.toSManga
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.source.local.image.LocalCoverManager
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@ -77,13 +76,13 @@ fun Manga.shouldDownloadNewChapters(dbCategories: List<Long>, preferences: Downl
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun Manga.editCover(
|
suspend fun Manga.editCover(
|
||||||
context: Context,
|
coverManager: LocalCoverManager,
|
||||||
stream: InputStream,
|
stream: InputStream,
|
||||||
updateManga: UpdateManga = Injekt.get(),
|
updateManga: UpdateManga = Injekt.get(),
|
||||||
coverCache: CoverCache = Injekt.get(),
|
coverCache: CoverCache = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
if (isLocal()) {
|
if (isLocal()) {
|
||||||
LocalSource.updateCover(context, toSManga(), stream)
|
coverManager.update(toSManga(), stream)
|
||||||
updateManga.awaitUpdateCoverLastModified(id)
|
updateManga.awaitUpdateCoverLastModified(id)
|
||||||
} else if (favorite) {
|
} else if (favorite) {
|
||||||
coverCache.setCustomCoverToCache(this, stream)
|
coverCache.setCustomCoverToCache(this, stream)
|
||||||
|
@ -46,7 +46,6 @@ import tachiyomi.core.util.system.logcat
|
|||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.roundToInt
|
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.
|
* 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.XmlElement
|
||||||
import nl.adaptivity.xmlutil.serialization.XmlSerialName
|
import nl.adaptivity.xmlutil.serialization.XmlSerialName
|
||||||
import nl.adaptivity.xmlutil.serialization.XmlValue
|
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"
|
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) {
|
fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
|
||||||
comicInfo.series?.let { title = it.value }
|
comicInfo.series?.let { title = it.value }
|
||||||
comicInfo.writer?.let { author = it.value }
|
comicInfo.writer?.let { author = it.value }
|
||||||
@ -149,7 +125,7 @@ data class ComicInfo(
|
|||||||
data class PublishingStatusTachiyomi(@XmlValue(true) val value: String = "")
|
data class PublishingStatusTachiyomi(@XmlValue(true) val value: String = "")
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum class ComicInfoPublishingStatus(
|
enum class ComicInfoPublishingStatus(
|
||||||
val comicInfoValue: String,
|
val comicInfoValue: String,
|
||||||
val sMangaModelValue: Int,
|
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,
|
||||||
|
)
|
@ -28,12 +28,21 @@ dependencies {
|
|||||||
api(libs.okhttp.dnsoverhttps)
|
api(libs.okhttp.dnsoverhttps)
|
||||||
api(libs.okio)
|
api(libs.okio)
|
||||||
|
|
||||||
|
implementation(libs.image.decoder)
|
||||||
|
|
||||||
|
implementation(libs.unifile)
|
||||||
|
|
||||||
api(kotlinx.coroutines.core)
|
api(kotlinx.coroutines.core)
|
||||||
api(kotlinx.serialization.json)
|
api(kotlinx.serialization.json)
|
||||||
api(kotlinx.serialization.json.okio)
|
api(kotlinx.serialization.json.okio)
|
||||||
|
|
||||||
api(libs.preferencektx)
|
api(libs.preferencektx)
|
||||||
|
|
||||||
|
implementation(libs.jsoup)
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
implementation(libs.natural.comparator)
|
||||||
|
|
||||||
// JavaScript engine
|
// JavaScript engine
|
||||||
implementation(libs.bundles.js.engine)
|
implementation(libs.bundles.js.engine)
|
||||||
|
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.util.storage
|
package eu.kanade.tachiyomi.util.storage
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.MediaScannerConnection
|
import android.media.MediaScannerConnection
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.StatFs
|
import android.os.StatFs
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.util.lang.Hash
|
import eu.kanade.tachiyomi.util.lang.Hash
|
||||||
import java.io.File
|
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"
|
const val NOMEDIA_FILE = ".nomedia"
|
||||||
}
|
}
|
@ -1,15 +1,10 @@
|
|||||||
package eu.kanade.tachiyomi.util.storage
|
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.Jsoup
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
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.ZipEntry
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
@ -49,58 +44,6 @@ class EpubFile(file: File) : Closeable {
|
|||||||
return zip.getEntry(name)
|
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.
|
* 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.
|
* Returns the path to the package document.
|
||||||
*/
|
*/
|
||||||
private fun getPackageHref(): String {
|
fun getPackageHref(): String {
|
||||||
val meta = zip.getEntry(resolveZipPath("META-INF", "container.xml"))
|
val meta = zip.getEntry(resolveZipPath("META-INF", "container.xml"))
|
||||||
if (meta != null) {
|
if (meta != null) {
|
||||||
val metaDoc = zip.getInputStream(meta).use { Jsoup.parse(it, 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.
|
* 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)
|
val entry = zip.getEntry(ref)
|
||||||
return zip.getInputStream(entry).use { Jsoup.parse(it, null, "") }
|
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.
|
* 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")
|
val pages = document.select("manifest > item")
|
||||||
.filter { node -> "application/xhtml+xml" == node.attr("media-type") }
|
.filter { node -> "application/xhtml+xml" == node.attr("media-type") }
|
||||||
.associateBy { it.attr("id") }
|
.associateBy { it.attr("id") }
|
@ -1,6 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.util.system
|
package tachiyomi.core.util.system
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.content.res.Resources
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.graphics.BitmapRegionDecoder
|
import android.graphics.BitmapRegionDecoder
|
||||||
@ -22,7 +24,6 @@ import androidx.core.graphics.green
|
|||||||
import androidx.core.graphics.red
|
import androidx.core.graphics.red
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.util.system.logcat
|
|
||||||
import tachiyomi.decoder.Format
|
import tachiyomi.decoder.Format
|
||||||
import tachiyomi.decoder.ImageDecoder
|
import tachiyomi.decoder.ImageDecoder
|
||||||
import java.io.BufferedInputStream
|
import java.io.BufferedInputStream
|
||||||
@ -669,3 +670,6 @@ object ImageUtil {
|
|||||||
private val Bitmap.rect: Rect
|
private val Bitmap.rect: Rect
|
||||||
get() = Rect(0, 0, width, height)
|
get() = Rect(0, 0, width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val getDisplayMaxHeightInPx: Int
|
||||||
|
get() = Resources.getSystem().displayMetrics.let { max(it.heightPixels, it.widthPixels) }
|
@ -48,3 +48,5 @@ include(":data")
|
|||||||
include(":domain")
|
include(":domain")
|
||||||
include(":presentation-widget")
|
include(":presentation-widget")
|
||||||
include(":presentation-core")
|
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,24 +1,18 @@
|
|||||||
package eu.kanade.tachiyomi.source
|
package tachiyomi.source.local
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.github.junrar.Archive
|
|
||||||
import com.hippo.unifile.UniFile
|
|
||||||
import eu.kanade.domain.UnsortedPreferences
|
|
||||||
import eu.kanade.domain.manga.model.COMIC_INFO_FILE
|
import eu.kanade.domain.manga.model.COMIC_INFO_FILE
|
||||||
import eu.kanade.domain.manga.model.ComicInfo
|
import eu.kanade.domain.manga.model.ComicInfo
|
||||||
import eu.kanade.domain.manga.model.copyFromComicInfo
|
import eu.kanade.domain.manga.model.copyFromComicInfo
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
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.storage.EpubFile
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
import kotlinx.serialization.json.encodeToStream
|
import kotlinx.serialization.json.encodeToStream
|
||||||
@ -26,9 +20,18 @@ import logcat.LogPriority
|
|||||||
import nl.adaptivity.xmlutil.AndroidXmlReader
|
import nl.adaptivity.xmlutil.AndroidXmlReader
|
||||||
import nl.adaptivity.xmlutil.serialization.XML
|
import nl.adaptivity.xmlutil.serialization.XML
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import tachiyomi.core.metadata.tachiyomi.MangaDetails
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
|
import tachiyomi.core.util.system.ImageUtil
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.chapter.service.ChapterRecognition
|
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 uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
@ -36,17 +39,22 @@ import java.io.InputStream
|
|||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
|
import com.github.junrar.Archive as JunrarArchive
|
||||||
|
|
||||||
class LocalSource(
|
class LocalSource(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
private val fileSystem: LocalSourceFileSystem,
|
||||||
|
private val coverManager: LocalCoverManager,
|
||||||
|
// SY -->
|
||||||
|
private val allowHiddenFiles: () -> Boolean,
|
||||||
|
// SY <--
|
||||||
) : CatalogueSource, UnmeteredSource {
|
) : CatalogueSource, UnmeteredSource {
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
private val xml: XML by injectLazy()
|
private val xml: XML by injectLazy()
|
||||||
|
|
||||||
// SY -->
|
private val POPULAR_FILTERS = FilterList(OrderBy.Popular(context))
|
||||||
private val preferences: UnsortedPreferences by injectLazy()
|
private val LATEST_FILTERS = FilterList(OrderBy.Latest(context))
|
||||||
// SY <--
|
|
||||||
|
|
||||||
override val name: String = context.getString(R.string.local_source)
|
override val name: String = context.getString(R.string.local_source)
|
||||||
|
|
||||||
@ -64,19 +72,17 @@ class LocalSource(
|
|||||||
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
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 }
|
||||||
// SY -->
|
// SY -->
|
||||||
val allowLocalSourceHiddenFolders = preferences.allowLocalSourceHiddenFolders().get()
|
val allowLocalSourceHiddenFolders = allowHiddenFiles()
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
var mangaDirs = baseDirsFiles
|
var mangaDirs = baseDirsFiles
|
||||||
// Filter out files that are hidden and is not a folder
|
// Filter out files that are hidden and is not a folder
|
||||||
.filter { it.isDirectory && /* SY --> */ (!it.name.startsWith('.') || allowLocalSourceHiddenFolders) /* SY <-- */ }
|
.filter { it.isDirectory && /* SY --> */ (!it.name.startsWith('.') || allowLocalSourceHiddenFolders) /* SY <-- */ }
|
||||||
.distinctBy { it.name }
|
.distinctBy { it.name }
|
||||||
|
.filter { // Filter by query or last modified
|
||||||
val lastModifiedLimit = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
|
||||||
// Filter by query or last modified
|
|
||||||
mangaDirs = mangaDirs.filter {
|
|
||||||
if (lastModifiedLimit == 0L) {
|
if (lastModifiedLimit == 0L) {
|
||||||
it.name.contains(query, ignoreCase = true)
|
it.name.contains(query, ignoreCase = true)
|
||||||
} else {
|
} else {
|
||||||
@ -86,24 +92,20 @@ class LocalSource(
|
|||||||
|
|
||||||
filters.forEach { filter ->
|
filters.forEach { filter ->
|
||||||
when (filter) {
|
when (filter) {
|
||||||
is OrderBy -> {
|
is OrderBy.Popular -> {
|
||||||
when (filter.state!!.index) {
|
|
||||||
0 -> {
|
|
||||||
mangaDirs = if (filter.state!!.ascending) {
|
mangaDirs = if (filter.state!!.ascending) {
|
||||||
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||||
} else {
|
} else {
|
||||||
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
|
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1 -> {
|
is OrderBy.Latest -> {
|
||||||
mangaDirs = if (filter.state!!.ascending) {
|
mangaDirs = if (filter.state!!.ascending) {
|
||||||
mangaDirs.sortedBy(File::lastModified)
|
mangaDirs.sortedBy(File::lastModified)
|
||||||
} else {
|
} else {
|
||||||
mangaDirs.sortedByDescending(File::lastModified)
|
mangaDirs.sortedByDescending(File::lastModified)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
/* Do nothing */
|
/* Do nothing */
|
||||||
@ -118,10 +120,9 @@ class LocalSource(
|
|||||||
url = mangaDir.name
|
url = mangaDir.name
|
||||||
|
|
||||||
// Try to find the cover
|
// Try to find the cover
|
||||||
val cover = getCoverFile(mangaDir.name, baseDirsFiles)
|
coverManager.find(mangaDir.name)
|
||||||
if (cover != null && cover.exists()) {
|
?.takeIf(File::exists)
|
||||||
thumbnail_url = cover.absolutePath
|
?.let { thumbnail_url = it.absolutePath }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +153,7 @@ class LocalSource(
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
fun updateMangaInfo(manga: SManga) {
|
fun updateMangaInfo(manga: SManga) {
|
||||||
val directory = getBaseDirectories(context).map { File(it, manga.url) }.find {
|
val directory = fileSystem.getFilesInBaseDirectories().map { File(it, manga.url) }.find {
|
||||||
it.exists()
|
it.exists()
|
||||||
} ?: return
|
} ?: return
|
||||||
val existingFileName = directory.listFiles()?.find { it.extension == "json" }?.name
|
val existingFileName = directory.listFiles()?.find { it.extension == "json" }?.name
|
||||||
@ -169,15 +170,13 @@ class LocalSource(
|
|||||||
|
|
||||||
// Manga details related
|
// Manga details related
|
||||||
override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext {
|
override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext {
|
||||||
val baseDirsFile = getBaseDirectoriesFiles(context)
|
coverManager.find(manga.url)?.let {
|
||||||
|
|
||||||
getCoverFile(manga.url, baseDirsFile)?.let {
|
|
||||||
manga.thumbnail_url = it.absolutePath
|
manga.thumbnail_url = it.absolutePath
|
||||||
}
|
}
|
||||||
|
|
||||||
// Augment manga details based on metadata files
|
// Augment manga details based on metadata files
|
||||||
try {
|
try {
|
||||||
val mangaDirFiles = getMangaDirsFiles(manga.url, baseDirsFile).toList()
|
val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url).toList()
|
||||||
|
|
||||||
val comicInfoFile = mangaDirFiles
|
val comicInfoFile = mangaDirFiles
|
||||||
.firstOrNull { it.name == COMIC_INFO_FILE }
|
.firstOrNull { it.name == COMIC_INFO_FILE }
|
||||||
@ -208,10 +207,10 @@ class LocalSource(
|
|||||||
// Copy ComicInfo.xml from chapter archive to top level if found
|
// Copy ComicInfo.xml from chapter archive to top level if found
|
||||||
noXmlFile == null -> {
|
noXmlFile == null -> {
|
||||||
val chapterArchives = mangaDirFiles
|
val chapterArchives = mangaDirFiles
|
||||||
.filter { isSupportedArchiveFile(it.extension) }
|
.filter(Archive::isSupported)
|
||||||
.toList()
|
.toList()
|
||||||
|
|
||||||
val mangaDir = getMangaDir(manga.url, baseDirsFile)
|
val mangaDir = fileSystem.getMangaDirectory(manga.url)
|
||||||
val folderPath = mangaDir?.absolutePath
|
val folderPath = mangaDir?.absolutePath
|
||||||
|
|
||||||
val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)
|
val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)
|
||||||
@ -232,7 +231,7 @@ class LocalSource(
|
|||||||
|
|
||||||
private fun copyComicInfoFileFromArchive(chapterArchives: List<File>, folderPath: String?): File? {
|
private fun copyComicInfoFileFromArchive(chapterArchives: List<File>, folderPath: String?): File? {
|
||||||
for (chapter in chapterArchives) {
|
for (chapter in chapterArchives) {
|
||||||
when (getFormat(chapter)) {
|
when (Format.valueOf(chapter)) {
|
||||||
is Format.Zip -> {
|
is Format.Zip -> {
|
||||||
ZipFile(chapter).use { zip: ZipFile ->
|
ZipFile(chapter).use { zip: ZipFile ->
|
||||||
zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile ->
|
zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile ->
|
||||||
@ -243,7 +242,7 @@ class LocalSource(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Format.Rar -> {
|
is Format.Rar -> {
|
||||||
Archive(chapter).use { rar: Archive ->
|
JunrarArchive(chapter).use { rar ->
|
||||||
rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile ->
|
rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile ->
|
||||||
rar.getInputStream(comicInfoFile).buffered().use { stream ->
|
rar.getInputStream(comicInfoFile).buffered().use { stream ->
|
||||||
return copyComicInfoFile(stream, folderPath)
|
return copyComicInfoFile(stream, folderPath)
|
||||||
@ -273,22 +272,11 @@ class LocalSource(
|
|||||||
manga.copyFromComicInfo(comicInfo)
|
manga.copyFromComicInfo(comicInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data 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
|
// Chapters
|
||||||
override suspend fun getChapterList(manga: SManga): List<SChapter> {
|
override suspend fun getChapterList(manga: SManga): List<SChapter> {
|
||||||
val baseDirsFile = getBaseDirectoriesFiles(context)
|
return fileSystem.getFilesInMangaDirectory(manga.url)
|
||||||
return getMangaDirsFiles(manga.url, baseDirsFile)
|
|
||||||
// Only keep supported formats
|
// Only keep supported formats
|
||||||
.filter { it.isDirectory || isSupportedArchiveFile(it.extension) }
|
.filter { it.isDirectory || Archive.isSupported(it) }
|
||||||
.map { chapterFile ->
|
.map { chapterFile ->
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
url = "${manga.url}/${chapterFile.name}"
|
url = "${manga.url}/${chapterFile.name}"
|
||||||
@ -300,7 +288,7 @@ class LocalSource(
|
|||||||
date_upload = chapterFile.lastModified()
|
date_upload = chapterFile.lastModified()
|
||||||
chapter_number = ChapterRecognition.parseChapterNumber(manga.title, this.name, this.chapter_number)
|
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) {
|
if (format is Format.Epub) {
|
||||||
EpubFile(format.file).use { epub ->
|
EpubFile(format.file).use { epub ->
|
||||||
epub.fillChapterMetadata(this)
|
epub.fillChapterMetadata(this)
|
||||||
@ -316,44 +304,22 @@ class LocalSource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
override fun getFilterList() = FilterList(OrderBy(context))
|
override fun getFilterList() = FilterList(OrderBy.Popular(context))
|
||||||
|
|
||||||
private val POPULAR_FILTERS = FilterList(OrderBy(context))
|
|
||||||
private val LATEST_FILTERS = FilterList(OrderBy(context).apply { state = Filter.Sort.Selection(1, false) })
|
|
||||||
|
|
||||||
private class OrderBy(context: Context) : Filter.Sort(
|
|
||||||
context.getString(R.string.local_filter_order_by),
|
|
||||||
arrayOf(context.getString(R.string.title), context.getString(R.string.date)),
|
|
||||||
Selection(0, true),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Unused stuff
|
// Unused stuff
|
||||||
override suspend fun getPageList(chapter: SChapter) = throw UnsupportedOperationException("Unused")
|
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 {
|
fun getFormat(chapter: SChapter): Format {
|
||||||
val baseDirs = getBaseDirectories(context)
|
try {
|
||||||
|
return fileSystem.getBaseDirectories()
|
||||||
for (dir in baseDirs) {
|
.map { directory -> File(directory, chapter.url) }
|
||||||
val chapFile = File(dir, chapter.url)
|
.find { chapterFile -> chapterFile.exists() }
|
||||||
if (!chapFile.exists()) continue
|
?.let(Format.Companion::valueOf)
|
||||||
|
?: throw Exception(context.getString(R.string.chapter_not_found))
|
||||||
return getFormat(chapFile)
|
} catch (e: Format.UnknownFormatException) {
|
||||||
}
|
throw Exception(context.getString(R.string.local_invalid_format))
|
||||||
throw Exception(context.getString(R.string.chapter_not_found))
|
} catch (e: Exception) {
|
||||||
}
|
throw e
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,7 +331,7 @@ class LocalSource(
|
|||||||
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||||
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
?.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 -> {
|
is Format.Zip -> {
|
||||||
ZipFile(format.file).use { zip ->
|
ZipFile(format.file).use { zip ->
|
||||||
@ -373,16 +339,16 @@ class LocalSource(
|
|||||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||||
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
.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 -> {
|
is Format.Rar -> {
|
||||||
Archive(format.file).use { archive ->
|
JunrarArchive(format.file).use { archive ->
|
||||||
val entry = archive.fileHeaders
|
val entry = archive.fileHeaders
|
||||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||||
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
.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 -> {
|
is Format.Epub -> {
|
||||||
@ -391,7 +357,7 @@ class LocalSource(
|
|||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
?.let { epub.getEntry(it) }
|
?.let { epub.getEntry(it) }
|
||||||
|
|
||||||
entry?.let { updateCover(context, manga, epub.getInputStream(it)) }
|
entry?.let { coverManager.update(manga, epub.getInputStream(it)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -401,86 +367,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 {
|
companion object {
|
||||||
const val ID = 0L
|
const val ID = 0L
|
||||||
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
|
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 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user