mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-10 03:51:59 +01:00
Move Local Source to separate module (#9152)
* Move Local Source to separate module * Review changes
This commit is contained in:
parent
2368c50ebb
commit
f27dc19b37
57 changed files with 523 additions and 314 deletions
|
@ -140,7 +140,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"))
|
||||||
|
@ -200,7 +202,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
|
||||||
|
@ -224,9 +226,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 kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
@ -11,6 +10,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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
@ -91,3 +92,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,13 +2,13 @@ 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 kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
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,
|
||||||
|
|
|
@ -22,7 +22,6 @@ import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid
|
||||||
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 kotlinx.coroutines.flow.StateFlow
|
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.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(
|
||||||
|
|
|
@ -23,7 +23,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
|
||||||
|
@ -36,6 +35,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)
|
||||||
|
|
|
@ -19,9 +19,9 @@ 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 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
|
||||||
|
|
|
@ -48,6 +48,10 @@ import tachiyomi.data.Mangas
|
||||||
import tachiyomi.data.dateAdapter
|
import tachiyomi.data.dateAdapter
|
||||||
import tachiyomi.data.listOfStringsAdapter
|
import tachiyomi.data.listOfStringsAdapter
|
||||||
import tachiyomi.data.updateStrategyAdapter
|
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.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
|
||||||
|
@ -133,6 +137,9 @@ class AppModule(val app: Application) : InjektModule {
|
||||||
|
|
||||||
addSingletonFactory { ImageSaver(app) }
|
addSingletonFactory { ImageSaver(app) }
|
||||||
|
|
||||||
|
addSingletonFactory<LocalSourceFileSystem> { AndroidLocalSourceFileSystem(app) }
|
||||||
|
addSingletonFactory<LocalCoverManager> { AndroidLocalCoverManager(app, get()) }
|
||||||
|
|
||||||
// Asynchronously init expensive components for a faster cold start
|
// Asynchronously init expensive components for a faster cold start
|
||||||
ContextCompat.getMainExecutor(app).execute {
|
ContextCompat.getMainExecutor(app).execute {
|
||||||
get<NetworkHelper>()
|
get<NetworkHelper>()
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
|
||||||
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 kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
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.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
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,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
|
||||||
|
|
||||||
|
@ -43,7 +46,15 @@ class SourceManager(
|
||||||
scope.launch {
|
scope.launch {
|
||||||
extensionManager.installedExtensionsFlow
|
extensionManager.installedExtensionsFlow
|
||||||
.collectLatest { extensions ->
|
.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 ->
|
extensions.forEach { extension ->
|
||||||
extension.sources.forEach {
|
extension.sources.forEach {
|
||||||
mutableMap[it.id] = it
|
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.LocalTabNavigator
|
||||||
import cafe.adriel.voyager.navigator.tab.TabOptions
|
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||||
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
|
||||||
|
|
|
@ -23,7 +23,6 @@ import eu.kanade.presentation.browse.BrowseSourceContent
|
||||||
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.R
|
import eu.kanade.tachiyomi.R
|
||||||
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.source.browse.BrowseSourceScreenModel
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
|
||||||
import eu.kanade.tachiyomi.ui.home.HomeScreen
|
import eu.kanade.tachiyomi.ui.home.HomeScreen
|
||||||
|
@ -34,6 +33,7 @@ import tachiyomi.core.Constants
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||||
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,
|
||||||
|
|
|
@ -45,7 +45,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
|
||||||
|
@ -61,6 +60,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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -774,7 +774,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
|
||||||
|
@ -13,6 +12,8 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
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.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.
|
||||||
|
@ -80,14 +81,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
|
||||||
|
@ -23,6 +22,7 @@ import kotlinx.coroutines.supervisorScope
|
||||||
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.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.InputStream
|
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.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,
|
||||||
|
)
|
|
@ -27,12 +27,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,7 +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 +23,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
|
||||||
|
@ -31,6 +31,7 @@ import java.io.ByteArrayOutputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.net.URLConnection
|
import java.net.URLConnection
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
object ImageUtil {
|
object ImageUtil {
|
||||||
|
@ -587,3 +588,6 @@ object ImageUtil {
|
||||||
"image/jxl" to "jxl",
|
"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(":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,32 +1,36 @@
|
||||||
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.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 logcat.LogPriority
|
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
|
||||||
|
@ -34,14 +38,20 @@ 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,
|
||||||
) : 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()
|
||||||
|
|
||||||
|
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 name: String = context.getString(R.string.local_source)
|
||||||
|
|
||||||
override val id: Long = ID
|
override val id: Long = ID
|
||||||
|
@ -58,41 +68,34 @@ 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 }
|
||||||
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 && !it.name.startsWith('.') }
|
.filter { it.isDirectory && !it.name.startsWith('.') }
|
||||||
.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
|
if (lastModifiedLimit == 0L) {
|
||||||
// Filter by query or last modified
|
it.name.contains(query, ignoreCase = true)
|
||||||
mangaDirs = mangaDirs.filter {
|
} else {
|
||||||
if (lastModifiedLimit == 0L) {
|
it.lastModified() >= lastModifiedLimit
|
||||||
it.name.contains(query, ignoreCase = true)
|
}
|
||||||
} else {
|
|
||||||
it.lastModified() >= lastModifiedLimit
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
filters.forEach { filter ->
|
filters.forEach { filter ->
|
||||||
when (filter) {
|
when (filter) {
|
||||||
is OrderBy -> {
|
is OrderBy.Popular -> {
|
||||||
when (filter.state!!.index) {
|
mangaDirs = if (filter.state!!.ascending) {
|
||||||
0 -> {
|
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||||
mangaDirs = if (filter.state!!.ascending) {
|
} else {
|
||||||
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||||
} else {
|
}
|
||||||
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
|
}
|
||||||
}
|
is OrderBy.Latest -> {
|
||||||
}
|
mangaDirs = if (filter.state!!.ascending) {
|
||||||
1 -> {
|
mangaDirs.sortedBy(File::lastModified)
|
||||||
mangaDirs = if (filter.state!!.ascending) {
|
} else {
|
||||||
mangaDirs.sortedBy(File::lastModified)
|
mangaDirs.sortedByDescending(File::lastModified)
|
||||||
} else {
|
|
||||||
mangaDirs.sortedByDescending(File::lastModified)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,10 +112,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 }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,15 +145,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 }
|
||||||
|
@ -182,10 +182,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)
|
||||||
|
@ -206,7 +206,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 ->
|
||||||
|
@ -217,7 +217,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)
|
||||||
|
@ -247,22 +247,11 @@ class LocalSource(
|
||||||
manga.copyFromComicInfo(comicInfo)
|
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
|
// 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}"
|
||||||
|
@ -274,7 +263,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)
|
||||||
|
@ -290,44 +279,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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -339,7 +306,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 ->
|
||||||
|
@ -347,16 +314,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 -> {
|
||||||
|
@ -365,7 +332,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)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
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…
Reference in a new issue