mirror of
https://git.mihon.tech/mihonapp/mihon
synced 2024-11-24 14:15:54 +03: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 {
|
||||
implementation(project(":i18n"))
|
||||
implementation(project(":core"))
|
||||
implementation(project(":core-metadata"))
|
||||
implementation(project(":source-api"))
|
||||
implementation(project(":source-local"))
|
||||
implementation(project(":data"))
|
||||
implementation(project(":domain"))
|
||||
implementation(project(":presentation-core"))
|
||||
|
@ -200,7 +202,7 @@ dependencies {
|
|||
// TLS 1.3 support for Android < 10
|
||||
implementation(libs.conscrypt.android)
|
||||
|
||||
// Data serialization (JSON, protobuf)
|
||||
// Data serialization (JSON, protobuf, xml)
|
||||
implementation(kotlinx.bundles.serialization)
|
||||
|
||||
// HTML parser
|
||||
|
@ -224,9 +226,6 @@ dependencies {
|
|||
}
|
||||
implementation(libs.image.decoder)
|
||||
|
||||
// Sort
|
||||
implementation(libs.natural.comparator)
|
||||
|
||||
// UI libraries
|
||||
implementation(libs.material)
|
||||
implementation(libs.flexible.adapter.core)
|
||||
|
|
|
@ -3,7 +3,6 @@ package eu.kanade.data.source
|
|||
import eu.kanade.domain.source.model.SourcePagingSourceType
|
||||
import eu.kanade.domain.source.repository.SourceRepository
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
@ -11,6 +10,7 @@ import kotlinx.coroutines.flow.map
|
|||
import tachiyomi.data.DatabaseHandler
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.domain.source.model.SourceWithCount
|
||||
import tachiyomi.source.local.LocalSource
|
||||
|
||||
class SourceRepositoryImpl(
|
||||
private val sourceManager: SourceManager,
|
||||
|
|
|
@ -2,12 +2,13 @@ package eu.kanade.domain.manga.model
|
|||
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.TriStateFilter
|
||||
import tachiyomi.source.local.LocalSource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
|
@ -91,3 +92,25 @@ fun Manga.isLocal(): Boolean = source == LocalSource.ID
|
|||
fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
|
||||
return coverCache.getCustomCoverFile(id).exists()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ComicInfo instance based on the manga and chapter metadata.
|
||||
*/
|
||||
fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String) = ComicInfo(
|
||||
title = ComicInfo.Title(chapter.name),
|
||||
series = ComicInfo.Series(manga.title),
|
||||
web = ComicInfo.Web(chapterUrl),
|
||||
summary = manga.description?.let { ComicInfo.Summary(it) },
|
||||
writer = manga.author?.let { ComicInfo.Writer(it) },
|
||||
penciller = manga.artist?.let { ComicInfo.Penciller(it) },
|
||||
translator = chapter.scanlator?.let { ComicInfo.Translator(it) },
|
||||
genre = manga.genre?.let { ComicInfo.Genre(it.joinToString()) },
|
||||
publishingStatus = ComicInfo.PublishingStatusTachiyomi(
|
||||
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
|
||||
),
|
||||
inker = null,
|
||||
colorist = null,
|
||||
letterer = null,
|
||||
coverArtist = null,
|
||||
tags = null,
|
||||
)
|
||||
|
|
|
@ -2,13 +2,13 @@ package eu.kanade.domain.source.interactor
|
|||
|
||||
import eu.kanade.domain.source.repository.SourceRepository
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import tachiyomi.domain.source.model.Pin
|
||||
import tachiyomi.domain.source.model.Pins
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.source.local.LocalSource
|
||||
|
||||
class GetEnabledSources(
|
||||
private val repository: SourceRepository,
|
||||
|
|
|
@ -22,7 +22,6 @@ import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid
|
|||
import eu.kanade.presentation.browse.components.BrowseSourceList
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
@ -32,6 +31,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
|||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.screens.EmptyScreenAction
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import tachiyomi.source.local.LocalSource
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceContent(
|
||||
|
|
|
@ -23,7 +23,6 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesState
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
|
@ -36,6 +35,7 @@ import tachiyomi.presentation.core.screens.EmptyScreen
|
|||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import tachiyomi.presentation.core.theme.header
|
||||
import tachiyomi.presentation.core.util.plus
|
||||
import tachiyomi.source.local.LocalSource
|
||||
|
||||
@Composable
|
||||
fun SourcesScreen(
|
||||
|
|
|
@ -31,9 +31,9 @@ import eu.kanade.domain.source.model.icon
|
|||
import eu.kanade.presentation.util.rememberResourceBitmapPainter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.source.local.LocalSource
|
||||
|
||||
private val defaultModifier = Modifier
|
||||
.height(40.dp)
|
||||
|
|
|
@ -19,9 +19,9 @@ import eu.kanade.presentation.components.RadioMenuItem
|
|||
import eu.kanade.presentation.components.SearchToolbar
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
import tachiyomi.source.local.LocalSource
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceToolbar(
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package eu.kanade.presentation.extensions
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
|
||||
/**
|
||||
* Launches request for [Manifest.permission.WRITE_EXTERNAL_STORAGE] permission
|
||||
*/
|
||||
@Composable
|
||||
fun DiskUtil.RequestStoragePermission() {
|
||||
val permissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
LaunchedEffect(Unit) {
|
||||
permissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
|
@ -37,6 +37,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.core.net.toUri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.domain.backup.service.BackupPreferences
|
||||
import eu.kanade.presentation.extensions.RequestStoragePermission
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.util.collectAsState
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
|
|
@ -48,6 +48,10 @@ import tachiyomi.data.Mangas
|
|||
import tachiyomi.data.dateAdapter
|
||||
import tachiyomi.data.listOfStringsAdapter
|
||||
import tachiyomi.data.updateStrategyAdapter
|
||||
import tachiyomi.source.local.image.AndroidLocalCoverManager
|
||||
import tachiyomi.source.local.image.LocalCoverManager
|
||||
import tachiyomi.source.local.io.AndroidLocalSourceFileSystem
|
||||
import tachiyomi.source.local.io.LocalSourceFileSystem
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
import uy.kohesive.injekt.api.addSingleton
|
||||
|
@ -133,6 +137,9 @@ class AppModule(val app: Application) : InjektModule {
|
|||
|
||||
addSingletonFactory { ImageSaver(app) }
|
||||
|
||||
addSingletonFactory<LocalSourceFileSystem> { AndroidLocalSourceFileSystem(app) }
|
||||
addSingletonFactory<LocalCoverManager> { AndroidLocalCoverManager(app, get()) }
|
||||
|
||||
// Asynchronously init expensive components for a faster cold start
|
||||
ContextCompat.getMainExecutor(app).execute {
|
||||
get<NetworkHelper>()
|
||||
|
|
|
@ -9,8 +9,8 @@ import coil.decode.ImageDecoderDecoder
|
|||
import coil.decode.ImageSource
|
||||
import coil.fetch.SourceResult
|
||||
import coil.request.Options
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import okio.BufferedSource
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import tachiyomi.decoder.ImageDecoder
|
||||
|
||||
/**
|
||||
|
|
|
@ -21,7 +21,6 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
|||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE
|
||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
|
@ -45,6 +44,7 @@ import tachiyomi.core.util.lang.launchIO
|
|||
import tachiyomi.core.util.lang.launchNow
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.lang.withUIContext
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
|
|
@ -15,9 +15,9 @@ import eu.kanade.tachiyomi.R
|
|||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import logcat.LogPriority
|
||||
import okio.IOException
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.graphics.drawable.Drawable
|
|||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import tachiyomi.domain.source.model.SourceData
|
||||
import tachiyomi.source.local.LocalSource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
|
|
|
@ -20,6 +20,9 @@ import kotlinx.coroutines.runBlocking
|
|||
import rx.Observable
|
||||
import tachiyomi.domain.source.model.SourceData
|
||||
import tachiyomi.domain.source.repository.SourceDataRepository
|
||||
import tachiyomi.source.local.LocalSource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
|
@ -43,7 +46,15 @@ class SourceManager(
|
|||
scope.launch {
|
||||
extensionManager.installedExtensionsFlow
|
||||
.collectLatest { extensions ->
|
||||
val mutableMap = ConcurrentHashMap<Long, Source>(mapOf(LocalSource.ID to LocalSource(context)))
|
||||
val mutableMap = ConcurrentHashMap<Long, Source>(
|
||||
mapOf(
|
||||
LocalSource.ID to LocalSource(
|
||||
context,
|
||||
Injekt.get(),
|
||||
Injekt.get(),
|
||||
),
|
||||
),
|
||||
)
|
||||
extensions.forEach { extension ->
|
||||
extension.sources.forEach {
|
||||
mutableMap[it.id] = it
|
||||
|
|
|
@ -14,6 +14,7 @@ import cafe.adriel.voyager.navigator.Navigator
|
|||
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
||||
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||
import eu.kanade.presentation.components.TabbedScreen
|
||||
import eu.kanade.presentation.extensions.RequestStoragePermission
|
||||
import eu.kanade.presentation.util.Tab
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
|
||||
|
|
|
@ -23,7 +23,6 @@ import eu.kanade.presentation.browse.BrowseSourceContent
|
|||
import eu.kanade.presentation.components.SearchToolbar
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
|
||||
import eu.kanade.tachiyomi.ui.home.HomeScreen
|
||||
|
@ -34,6 +33,7 @@ import tachiyomi.core.Constants
|
|||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.source.local.LocalSource
|
||||
|
||||
data class SourceSearchScreen(
|
||||
private val oldManga: Manga,
|
||||
|
|
|
@ -45,7 +45,6 @@ import eu.kanade.presentation.util.AssistContentScreen
|
|||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesScreen
|
||||
|
@ -61,6 +60,7 @@ import tachiyomi.core.util.lang.launchIO
|
|||
import tachiyomi.presentation.core.components.material.Divider
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.source.local.LocalSource
|
||||
|
||||
data class BrowseSourceScreen(
|
||||
private val sourceId: Long,
|
||||
|
|
|
@ -121,7 +121,7 @@ class MangaCoverScreenModel(
|
|||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
context.contentResolver.openInputStream(data)?.use {
|
||||
try {
|
||||
manga.editCover(context, it, updateManga, coverCache)
|
||||
manga.editCover(Injekt.get(), it, updateManga, coverCache)
|
||||
notifyCoverUpdated(context)
|
||||
} catch (e: Exception) {
|
||||
notifyFailedCoverUpdate(context, e)
|
||||
|
|
|
@ -774,7 +774,7 @@ class ReaderViewModel(
|
|||
|
||||
viewModelScope.launchNonCancellable {
|
||||
val result = try {
|
||||
manga.editCover(context, stream())
|
||||
manga.editCover(Injekt.get(), stream())
|
||||
if (manga.isLocal() || manga.favorite) {
|
||||
SetAsCoverResult.Success
|
||||
} else {
|
||||
|
|
|
@ -5,7 +5,6 @@ import com.github.junrar.exception.UnsupportedRarV5Exception
|
|||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
|
@ -13,6 +12,8 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
|||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.source.local.LocalSource
|
||||
import tachiyomi.source.local.io.Format
|
||||
|
||||
/**
|
||||
* Loader used to retrieve the [PageLoader] for a given chapter.
|
||||
|
@ -80,14 +81,14 @@ class ChapterLoader(
|
|||
source is HttpSource -> HttpPageLoader(chapter, source)
|
||||
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
|
||||
when (format) {
|
||||
is LocalSource.Format.Directory -> DirectoryPageLoader(format.file)
|
||||
is LocalSource.Format.Zip -> ZipPageLoader(format.file)
|
||||
is LocalSource.Format.Rar -> try {
|
||||
is Format.Directory -> DirectoryPageLoader(format.file)
|
||||
is Format.Zip -> ZipPageLoader(format.file)
|
||||
is Format.Rar -> try {
|
||||
RarPageLoader(format.file)
|
||||
} catch (e: UnsupportedRarV5Exception) {
|
||||
error(context.getString(R.string.loader_rar5_error))
|
||||
}
|
||||
is LocalSource.Format.Epub -> EpubPageLoader(format.file)
|
||||
is Format.Epub -> EpubPageLoader(format.file)
|
||||
}
|
||||
}
|
||||
source is SourceManager.StubSource -> throw source.getSourceNotInstalledException()
|
||||
|
|
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.reader.loader
|
|||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import com.github.junrar.rarfile.FileHeader
|
|||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.PipedInputStream
|
||||
|
|
|
@ -4,7 +4,7 @@ import android.os.Build
|
|||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import java.io.File
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.zip.ZipFile
|
||||
|
|
|
@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
|||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
|
||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.MainScope
|
||||
|
@ -23,6 +22,7 @@ import kotlinx.coroutines.supervisorScope
|
|||
import tachiyomi.core.util.lang.launchIO
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.lang.withUIContext
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
|
|
|
@ -18,7 +18,6 @@ import eu.kanade.tachiyomi.ui.reader.model.StencilPage
|
|||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
|
||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.MainScope
|
||||
|
@ -29,6 +28,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine
|
|||
import tachiyomi.core.util.lang.launchIO
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.lang.withUIContext
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.InputStream
|
||||
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
package eu.kanade.tachiyomi.util
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.domain.download.service.DownloadPreferences
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.manga.model.hasCustomCover
|
||||
import eu.kanade.domain.manga.model.isLocal
|
||||
import eu.kanade.domain.manga.model.toSManga
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.source.local.image.LocalCoverManager
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.InputStream
|
||||
|
@ -77,13 +76,13 @@ fun Manga.shouldDownloadNewChapters(dbCategories: List<Long>, preferences: Downl
|
|||
}
|
||||
|
||||
suspend fun Manga.editCover(
|
||||
context: Context,
|
||||
coverManager: LocalCoverManager,
|
||||
stream: InputStream,
|
||||
updateManga: UpdateManga = Injekt.get(),
|
||||
coverCache: CoverCache = Injekt.get(),
|
||||
) {
|
||||
if (isLocal()) {
|
||||
LocalSource.updateCover(context, toSManga(), stream)
|
||||
coverManager.update(toSManga(), stream)
|
||||
updateManga.awaitUpdateCoverLastModified(id)
|
||||
} else if (favorite) {
|
||||
coverCache.setCustomCoverToCache(this, stream)
|
||||
|
|
|
@ -46,7 +46,6 @@ import tachiyomi.core.util.system.logcat
|
|||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
|
@ -113,9 +112,6 @@ fun Context.hasPermission(permission: String) = ContextCompat.checkSelfPermissio
|
|||
}
|
||||
}
|
||||
|
||||
val getDisplayMaxHeightInPx: Int
|
||||
get() = Resources.getSystem().displayMetrics.let { max(it.heightPixels, it.widthPixels) }
|
||||
|
||||
/**
|
||||
* Converts to px and takes into account LTR/RTL layout.
|
||||
*/
|
||||
|
|
1
core-metadata/.gitignore
vendored
Normal file
1
core-metadata/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
21
core-metadata/build.gradle.kts
Normal file
21
core-metadata/build.gradle.kts
Normal file
|
@ -0,0 +1,21 @@
|
|||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
kotlin("plugin.serialization")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "tachiyomi.core.metadata"
|
||||
|
||||
defaultConfig {
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":source-api"))
|
||||
|
||||
implementation(kotlinx.bundles.serialization)
|
||||
}
|
0
core-metadata/consumer-rules.pro
Normal file
0
core-metadata/consumer-rules.pro
Normal file
21
core-metadata/proguard-rules.pro
vendored
Normal file
21
core-metadata/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
2
core-metadata/src/main/AndroidManifest.xml
Normal file
2
core-metadata/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
|
@ -5,33 +5,9 @@ import kotlinx.serialization.Serializable
|
|||
import nl.adaptivity.xmlutil.serialization.XmlElement
|
||||
import nl.adaptivity.xmlutil.serialization.XmlSerialName
|
||||
import nl.adaptivity.xmlutil.serialization.XmlValue
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
const val COMIC_INFO_FILE = "ComicInfo.xml"
|
||||
|
||||
/**
|
||||
* Creates a ComicInfo instance based on the manga and chapter metadata.
|
||||
*/
|
||||
fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String) = ComicInfo(
|
||||
title = ComicInfo.Title(chapter.name),
|
||||
series = ComicInfo.Series(manga.title),
|
||||
web = ComicInfo.Web(chapterUrl),
|
||||
summary = manga.description?.let { ComicInfo.Summary(it) },
|
||||
writer = manga.author?.let { ComicInfo.Writer(it) },
|
||||
penciller = manga.artist?.let { ComicInfo.Penciller(it) },
|
||||
translator = chapter.scanlator?.let { ComicInfo.Translator(it) },
|
||||
genre = manga.genre?.let { ComicInfo.Genre(it.joinToString()) },
|
||||
publishingStatus = ComicInfo.PublishingStatusTachiyomi(
|
||||
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
|
||||
),
|
||||
inker = null,
|
||||
colorist = null,
|
||||
letterer = null,
|
||||
coverArtist = null,
|
||||
tags = null,
|
||||
)
|
||||
|
||||
fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
|
||||
comicInfo.series?.let { title = it.value }
|
||||
comicInfo.writer?.let { author = it.value }
|
||||
|
@ -149,7 +125,7 @@ data class ComicInfo(
|
|||
data class PublishingStatusTachiyomi(@XmlValue(true) val value: String = "")
|
||||
}
|
||||
|
||||
private enum class ComicInfoPublishingStatus(
|
||||
enum class ComicInfoPublishingStatus(
|
||||
val comicInfoValue: String,
|
||||
val sMangaModelValue: Int,
|
||||
) {
|
|
@ -0,0 +1,13 @@
|
|||
package tachiyomi.core.metadata.tachiyomi
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class MangaDetails(
|
||||
val title: String? = null,
|
||||
val author: String? = null,
|
||||
val artist: String? = null,
|
||||
val description: String? = null,
|
||||
val genre: List<String>? = null,
|
||||
val status: Int? = null,
|
||||
)
|
|
@ -27,12 +27,21 @@ dependencies {
|
|||
api(libs.okhttp.dnsoverhttps)
|
||||
api(libs.okio)
|
||||
|
||||
implementation(libs.image.decoder)
|
||||
|
||||
implementation(libs.unifile)
|
||||
|
||||
api(kotlinx.coroutines.core)
|
||||
api(kotlinx.serialization.json)
|
||||
api(kotlinx.serialization.json.okio)
|
||||
|
||||
api(libs.preferencektx)
|
||||
|
||||
implementation(libs.jsoup)
|
||||
|
||||
// Sort
|
||||
implementation(libs.natural.comparator)
|
||||
|
||||
// JavaScript engine
|
||||
implementation(libs.bundles.js.engine)
|
||||
}
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
package eu.kanade.tachiyomi.util.storage
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.media.MediaScannerConnection
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.os.StatFs
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.util.lang.Hash
|
||||
import java.io.File
|
||||
|
@ -117,16 +113,5 @@ object DiskUtil {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches request for [Manifest.permission.WRITE_EXTERNAL_STORAGE] permission
|
||||
*/
|
||||
@Composable
|
||||
fun RequestStoragePermission() {
|
||||
val permissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
LaunchedEffect(Unit) {
|
||||
permissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
|
||||
const val NOMEDIA_FILE = ".nomedia"
|
||||
}
|
|
@ -1,15 +1,10 @@
|
|||
package eu.kanade.tachiyomi.util.storage
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
|
@ -49,58 +44,6 @@ class EpubFile(file: File) : Closeable {
|
|||
return zip.getEntry(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills manga metadata using this epub file's metadata.
|
||||
*/
|
||||
fun fillMangaMetadata(manga: SManga) {
|
||||
val ref = getPackageHref()
|
||||
val doc = getPackageDocument(ref)
|
||||
|
||||
val creator = doc.getElementsByTag("dc:creator").first()
|
||||
val description = doc.getElementsByTag("dc:description").first()
|
||||
|
||||
manga.author = creator?.text()
|
||||
manga.description = description?.text()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills chapter metadata using this epub file's metadata.
|
||||
*/
|
||||
fun fillChapterMetadata(chapter: SChapter) {
|
||||
val ref = getPackageHref()
|
||||
val doc = getPackageDocument(ref)
|
||||
|
||||
val title = doc.getElementsByTag("dc:title").first()
|
||||
val publisher = doc.getElementsByTag("dc:publisher").first()
|
||||
val creator = doc.getElementsByTag("dc:creator").first()
|
||||
var date = doc.getElementsByTag("dc:date").first()
|
||||
if (date == null) {
|
||||
date = doc.select("meta[property=dcterms:modified]").first()
|
||||
}
|
||||
|
||||
if (title != null) {
|
||||
chapter.name = title.text()
|
||||
}
|
||||
|
||||
if (publisher != null) {
|
||||
chapter.scanlator = publisher.text()
|
||||
} else if (creator != null) {
|
||||
chapter.scanlator = creator.text()
|
||||
}
|
||||
|
||||
if (date != null) {
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault())
|
||||
try {
|
||||
val parsedDate = dateFormat.parse(date.text())
|
||||
if (parsedDate != null) {
|
||||
chapter.date_upload = parsedDate.time
|
||||
}
|
||||
} catch (e: ParseException) {
|
||||
// Empty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path of all the images found in the epub file.
|
||||
*/
|
||||
|
@ -114,7 +57,7 @@ class EpubFile(file: File) : Closeable {
|
|||
/**
|
||||
* Returns the path to the package document.
|
||||
*/
|
||||
private fun getPackageHref(): String {
|
||||
fun getPackageHref(): String {
|
||||
val meta = zip.getEntry(resolveZipPath("META-INF", "container.xml"))
|
||||
if (meta != null) {
|
||||
val metaDoc = zip.getInputStream(meta).use { Jsoup.parse(it, null, "") }
|
||||
|
@ -129,7 +72,7 @@ class EpubFile(file: File) : Closeable {
|
|||
/**
|
||||
* Returns the package document where all the files are listed.
|
||||
*/
|
||||
private fun getPackageDocument(ref: String): Document {
|
||||
fun getPackageDocument(ref: String): Document {
|
||||
val entry = zip.getEntry(ref)
|
||||
return zip.getInputStream(entry).use { Jsoup.parse(it, null, "") }
|
||||
}
|
||||
|
@ -137,7 +80,7 @@ class EpubFile(file: File) : Closeable {
|
|||
/**
|
||||
* Returns all the pages from the epub.
|
||||
*/
|
||||
private fun getPagesFromDocument(document: Document): List<String> {
|
||||
fun getPagesFromDocument(document: Document): List<String> {
|
||||
val pages = document.select("manifest > item")
|
||||
.filter { node -> "application/xhtml+xml" == node.attr("media-type") }
|
||||
.associateBy { it.attr("id") }
|
|
@ -1,7 +1,8 @@
|
|||
package eu.kanade.tachiyomi.util.system
|
||||
package tachiyomi.core.util.system
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.BitmapRegionDecoder
|
||||
|
@ -22,7 +23,6 @@ import androidx.core.graphics.green
|
|||
import androidx.core.graphics.red
|
||||
import com.hippo.unifile.UniFile
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.decoder.Format
|
||||
import tachiyomi.decoder.ImageDecoder
|
||||
import java.io.BufferedInputStream
|
||||
|
@ -31,6 +31,7 @@ import java.io.ByteArrayOutputStream
|
|||
import java.io.InputStream
|
||||
import java.net.URLConnection
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
object ImageUtil {
|
||||
|
@ -587,3 +588,6 @@ object ImageUtil {
|
|||
"image/jxl" to "jxl",
|
||||
)
|
||||
}
|
||||
|
||||
val getDisplayMaxHeightInPx: Int
|
||||
get() = Resources.getSystem().displayMetrics.let { max(it.heightPixels, it.widthPixels) }
|
|
@ -45,3 +45,5 @@ include(":data")
|
|||
include(":domain")
|
||||
include(":presentation-widget")
|
||||
include(":presentation-core")
|
||||
include(":source-local")
|
||||
include(":core-metadata")
|
||||
|
|
1
source-local/.gitignore
vendored
Normal file
1
source-local/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
29
source-local/build.gradle.kts
Normal file
29
source-local/build.gradle.kts
Normal file
|
@ -0,0 +1,29 @@
|
|||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "tachiyomi.source.local"
|
||||
|
||||
defaultConfig {
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(project(":source-api"))
|
||||
implementation(project(":core"))
|
||||
implementation(project(":core-metadata"))
|
||||
|
||||
// Move ChapterRecognition to separate module?
|
||||
implementation(project(":domain"))
|
||||
|
||||
implementation(kotlinx.bundles.serialization)
|
||||
|
||||
implementation(libs.unifile)
|
||||
implementation(libs.junrar)
|
||||
}
|
0
source-local/consumer-rules.pro
Normal file
0
source-local/consumer-rules.pro
Normal file
21
source-local/proguard-rules.pro
vendored
Normal file
21
source-local/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
2
source-local/src/main/AndroidManifest.xml
Normal file
2
source-local/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
|
@ -1,32 +1,36 @@
|
|||
package eu.kanade.tachiyomi.source
|
||||
package tachiyomi.source.local
|
||||
|
||||
import android.content.Context
|
||||
import com.github.junrar.Archive
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.domain.manga.model.COMIC_INFO_FILE
|
||||
import eu.kanade.domain.manga.model.ComicInfo
|
||||
import eu.kanade.domain.manga.model.copyFromComicInfo
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import logcat.LogPriority
|
||||
import nl.adaptivity.xmlutil.AndroidXmlReader
|
||||
import nl.adaptivity.xmlutil.serialization.XML
|
||||
import rx.Observable
|
||||
import tachiyomi.core.metadata.tachiyomi.MangaDetails
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.chapter.service.ChapterRecognition
|
||||
import tachiyomi.source.local.filter.OrderBy
|
||||
import tachiyomi.source.local.image.LocalCoverManager
|
||||
import tachiyomi.source.local.io.Archive
|
||||
import tachiyomi.source.local.io.Format
|
||||
import tachiyomi.source.local.io.LocalSourceFileSystem
|
||||
import tachiyomi.source.local.metadata.fillChapterMetadata
|
||||
import tachiyomi.source.local.metadata.fillMangaMetadata
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
|
@ -34,14 +38,20 @@ import java.io.InputStream
|
|||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.zip.ZipFile
|
||||
import com.github.junrar.Archive as JunrarArchive
|
||||
|
||||
class LocalSource(
|
||||
private val context: Context,
|
||||
private val fileSystem: LocalSourceFileSystem,
|
||||
private val coverManager: LocalCoverManager,
|
||||
) : CatalogueSource, UnmeteredSource {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
private val xml: XML by injectLazy()
|
||||
|
||||
private val POPULAR_FILTERS = FilterList(OrderBy.Popular(context))
|
||||
private val LATEST_FILTERS = FilterList(OrderBy.Latest(context))
|
||||
|
||||
override val name: String = context.getString(R.string.local_source)
|
||||
|
||||
override val id: Long = ID
|
||||
|
@ -58,16 +68,13 @@ class LocalSource(
|
|||
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
val baseDirsFiles = getBaseDirectoriesFiles(context)
|
||||
|
||||
val baseDirsFiles = fileSystem.getFilesInBaseDirectories()
|
||||
val lastModifiedLimit by lazy { if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L }
|
||||
var mangaDirs = baseDirsFiles
|
||||
// Filter out files that are hidden and is not a folder
|
||||
.filter { it.isDirectory && !it.name.startsWith('.') }
|
||||
.distinctBy { it.name }
|
||||
|
||||
val lastModifiedLimit = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
||||
// Filter by query or last modified
|
||||
mangaDirs = mangaDirs.filter {
|
||||
.filter { // Filter by query or last modified
|
||||
if (lastModifiedLimit == 0L) {
|
||||
it.name.contains(query, ignoreCase = true)
|
||||
} else {
|
||||
|
@ -77,24 +84,20 @@ class LocalSource(
|
|||
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is OrderBy -> {
|
||||
when (filter.state!!.index) {
|
||||
0 -> {
|
||||
is OrderBy.Popular -> {
|
||||
mangaDirs = if (filter.state!!.ascending) {
|
||||
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
} else {
|
||||
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
}
|
||||
}
|
||||
1 -> {
|
||||
is OrderBy.Latest -> {
|
||||
mangaDirs = if (filter.state!!.ascending) {
|
||||
mangaDirs.sortedBy(File::lastModified)
|
||||
} else {
|
||||
mangaDirs.sortedByDescending(File::lastModified)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
/* Do nothing */
|
||||
|
@ -109,10 +112,9 @@ class LocalSource(
|
|||
url = mangaDir.name
|
||||
|
||||
// Try to find the cover
|
||||
val cover = getCoverFile(mangaDir.name, baseDirsFiles)
|
||||
if (cover != null && cover.exists()) {
|
||||
thumbnail_url = cover.absolutePath
|
||||
}
|
||||
coverManager.find(mangaDir.name)
|
||||
?.takeIf(File::exists)
|
||||
?.let { thumbnail_url = it.absolutePath }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -143,15 +145,13 @@ class LocalSource(
|
|||
|
||||
// Manga details related
|
||||
override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext {
|
||||
val baseDirsFile = getBaseDirectoriesFiles(context)
|
||||
|
||||
getCoverFile(manga.url, baseDirsFile)?.let {
|
||||
coverManager.find(manga.url)?.let {
|
||||
manga.thumbnail_url = it.absolutePath
|
||||
}
|
||||
|
||||
// Augment manga details based on metadata files
|
||||
try {
|
||||
val mangaDirFiles = getMangaDirsFiles(manga.url, baseDirsFile).toList()
|
||||
val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url).toList()
|
||||
|
||||
val comicInfoFile = mangaDirFiles
|
||||
.firstOrNull { it.name == COMIC_INFO_FILE }
|
||||
|
@ -182,10 +182,10 @@ class LocalSource(
|
|||
// Copy ComicInfo.xml from chapter archive to top level if found
|
||||
noXmlFile == null -> {
|
||||
val chapterArchives = mangaDirFiles
|
||||
.filter { isSupportedArchiveFile(it.extension) }
|
||||
.filter(Archive::isSupported)
|
||||
.toList()
|
||||
|
||||
val mangaDir = getMangaDir(manga.url, baseDirsFile)
|
||||
val mangaDir = fileSystem.getMangaDirectory(manga.url)
|
||||
val folderPath = mangaDir?.absolutePath
|
||||
|
||||
val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)
|
||||
|
@ -206,7 +206,7 @@ class LocalSource(
|
|||
|
||||
private fun copyComicInfoFileFromArchive(chapterArchives: List<File>, folderPath: String?): File? {
|
||||
for (chapter in chapterArchives) {
|
||||
when (getFormat(chapter)) {
|
||||
when (Format.valueOf(chapter)) {
|
||||
is Format.Zip -> {
|
||||
ZipFile(chapter).use { zip: ZipFile ->
|
||||
zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile ->
|
||||
|
@ -217,7 +217,7 @@ class LocalSource(
|
|||
}
|
||||
}
|
||||
is Format.Rar -> {
|
||||
Archive(chapter).use { rar: Archive ->
|
||||
JunrarArchive(chapter).use { rar ->
|
||||
rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile ->
|
||||
rar.getInputStream(comicInfoFile).buffered().use { stream ->
|
||||
return copyComicInfoFile(stream, folderPath)
|
||||
|
@ -247,22 +247,11 @@ class LocalSource(
|
|||
manga.copyFromComicInfo(comicInfo)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class MangaDetails(
|
||||
val title: String? = null,
|
||||
val author: String? = null,
|
||||
val artist: String? = null,
|
||||
val description: String? = null,
|
||||
val genre: List<String>? = null,
|
||||
val status: Int? = null,
|
||||
)
|
||||
|
||||
// Chapters
|
||||
override suspend fun getChapterList(manga: SManga): List<SChapter> {
|
||||
val baseDirsFile = getBaseDirectoriesFiles(context)
|
||||
return getMangaDirsFiles(manga.url, baseDirsFile)
|
||||
return fileSystem.getFilesInMangaDirectory(manga.url)
|
||||
// Only keep supported formats
|
||||
.filter { it.isDirectory || isSupportedArchiveFile(it.extension) }
|
||||
.filter { it.isDirectory || Archive.isSupported(it) }
|
||||
.map { chapterFile ->
|
||||
SChapter.create().apply {
|
||||
url = "${manga.url}/${chapterFile.name}"
|
||||
|
@ -274,7 +263,7 @@ class LocalSource(
|
|||
date_upload = chapterFile.lastModified()
|
||||
chapter_number = ChapterRecognition.parseChapterNumber(manga.title, this.name, this.chapter_number)
|
||||
|
||||
val format = getFormat(chapterFile)
|
||||
val format = Format.valueOf(chapterFile)
|
||||
if (format is Format.Epub) {
|
||||
EpubFile(format.file).use { epub ->
|
||||
epub.fillChapterMetadata(this)
|
||||
|
@ -290,44 +279,22 @@ class LocalSource(
|
|||
}
|
||||
|
||||
// Filters
|
||||
override fun getFilterList() = FilterList(OrderBy(context))
|
||||
|
||||
private val POPULAR_FILTERS = FilterList(OrderBy(context))
|
||||
private val LATEST_FILTERS = FilterList(OrderBy(context).apply { state = Filter.Sort.Selection(1, false) })
|
||||
|
||||
private class OrderBy(context: Context) : Filter.Sort(
|
||||
context.getString(R.string.local_filter_order_by),
|
||||
arrayOf(context.getString(R.string.title), context.getString(R.string.date)),
|
||||
Selection(0, true),
|
||||
)
|
||||
override fun getFilterList() = FilterList(OrderBy.Popular(context))
|
||||
|
||||
// Unused stuff
|
||||
override suspend fun getPageList(chapter: SChapter) = throw UnsupportedOperationException("Unused")
|
||||
|
||||
// Miscellaneous
|
||||
private fun isSupportedArchiveFile(extension: String): Boolean {
|
||||
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
|
||||
}
|
||||
|
||||
fun getFormat(chapter: SChapter): Format {
|
||||
val baseDirs = getBaseDirectories(context)
|
||||
|
||||
for (dir in baseDirs) {
|
||||
val chapFile = File(dir, chapter.url)
|
||||
if (!chapFile.exists()) continue
|
||||
|
||||
return getFormat(chapFile)
|
||||
}
|
||||
throw Exception(context.getString(R.string.chapter_not_found))
|
||||
}
|
||||
|
||||
private fun getFormat(file: File) = with(file) {
|
||||
when {
|
||||
isDirectory -> Format.Directory(this)
|
||||
extension.equals("zip", true) || extension.equals("cbz", true) -> Format.Zip(this)
|
||||
extension.equals("rar", true) || extension.equals("cbr", true) -> Format.Rar(this)
|
||||
extension.equals("epub", true) -> Format.Epub(this)
|
||||
else -> throw Exception(context.getString(R.string.local_invalid_format))
|
||||
try {
|
||||
return fileSystem.getBaseDirectories()
|
||||
.map { directory -> File(directory, chapter.url) }
|
||||
.find { chapterFile -> chapterFile.exists() }
|
||||
?.let(Format.Companion::valueOf)
|
||||
?: throw Exception(context.getString(R.string.chapter_not_found))
|
||||
} catch (e: Format.UnknownFormatException) {
|
||||
throw Exception(context.getString(R.string.local_invalid_format))
|
||||
} catch (e: Exception) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -339,7 +306,7 @@ class LocalSource(
|
|||
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
||||
|
||||
entry?.let { updateCover(context, manga, it.inputStream()) }
|
||||
entry?.let { coverManager.update(manga, it.inputStream()) }
|
||||
}
|
||||
is Format.Zip -> {
|
||||
ZipFile(format.file).use { zip ->
|
||||
|
@ -347,16 +314,16 @@ class LocalSource(
|
|||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||
|
||||
entry?.let { updateCover(context, manga, zip.getInputStream(it)) }
|
||||
entry?.let { coverManager.update(manga, zip.getInputStream(it)) }
|
||||
}
|
||||
}
|
||||
is Format.Rar -> {
|
||||
Archive(format.file).use { archive ->
|
||||
JunrarArchive(format.file).use { archive ->
|
||||
val entry = archive.fileHeaders
|
||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
||||
|
||||
entry?.let { updateCover(context, manga, archive.getInputStream(it)) }
|
||||
entry?.let { coverManager.update(manga, archive.getInputStream(it)) }
|
||||
}
|
||||
}
|
||||
is Format.Epub -> {
|
||||
|
@ -365,7 +332,7 @@ class LocalSource(
|
|||
.firstOrNull()
|
||||
?.let { epub.getEntry(it) }
|
||||
|
||||
entry?.let { updateCover(context, manga, epub.getInputStream(it)) }
|
||||
entry?.let { coverManager.update(manga, epub.getInputStream(it)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -375,86 +342,10 @@ class LocalSource(
|
|||
}
|
||||
}
|
||||
|
||||
sealed class Format {
|
||||
data class Directory(val file: File) : Format()
|
||||
data class Zip(val file: File) : Format()
|
||||
data class Rar(val file: File) : Format()
|
||||
data class Epub(val file: File) : Format()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ID = 0L
|
||||
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
|
||||
|
||||
private const val DEFAULT_COVER_NAME = "cover.jpg"
|
||||
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||
|
||||
private fun getBaseDirectories(context: Context): Sequence<File> {
|
||||
val localFolder = context.getString(R.string.app_name) + File.separator + "local"
|
||||
return DiskUtil.getExternalStorages(context)
|
||||
.map { File(it.absolutePath, localFolder) }
|
||||
.asSequence()
|
||||
}
|
||||
|
||||
private fun getBaseDirectoriesFiles(context: Context): Sequence<File> {
|
||||
return getBaseDirectories(context)
|
||||
// Get all the files inside all baseDir
|
||||
.flatMap { it.listFiles().orEmpty().toList() }
|
||||
}
|
||||
|
||||
private fun getMangaDir(mangaUrl: String, baseDirsFile: Sequence<File>): File? {
|
||||
return baseDirsFile
|
||||
// Get the first mangaDir or null
|
||||
.firstOrNull { it.isDirectory && it.name == mangaUrl }
|
||||
}
|
||||
|
||||
private fun getMangaDirsFiles(mangaUrl: String, baseDirsFile: Sequence<File>): Sequence<File> {
|
||||
return baseDirsFile
|
||||
// Filter out ones that are not related to the manga and is not a directory
|
||||
.filter { it.isDirectory && it.name == mangaUrl }
|
||||
// Get all the files inside the filtered folders
|
||||
.flatMap { it.listFiles().orEmpty().toList() }
|
||||
}
|
||||
|
||||
private fun getCoverFile(mangaUrl: String, baseDirsFile: Sequence<File>): File? {
|
||||
return getMangaDirsFiles(mangaUrl, baseDirsFile)
|
||||
// Get all file whose names start with 'cover'
|
||||
.filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
|
||||
// Get the first actual image
|
||||
.firstOrNull {
|
||||
ImageUtil.isImage(it.name) { it.inputStream() }
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCover(context: Context, manga: SManga, inputStream: InputStream): File? {
|
||||
val baseDirsFiles = getBaseDirectoriesFiles(context)
|
||||
|
||||
val mangaDir = getMangaDir(manga.url, baseDirsFiles)
|
||||
if (mangaDir == null) {
|
||||
inputStream.close()
|
||||
return null
|
||||
}
|
||||
|
||||
var coverFile = getCoverFile(manga.url, baseDirsFiles)
|
||||
if (coverFile == null) {
|
||||
coverFile = File(mangaDir.absolutePath, DEFAULT_COVER_NAME)
|
||||
coverFile.createNewFile()
|
||||
}
|
||||
|
||||
// It might not exist at this point
|
||||
coverFile.parentFile?.mkdirs()
|
||||
inputStream.use { input ->
|
||||
coverFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
DiskUtil.createNoMediaFile(UniFile.fromFile(mangaDir), context)
|
||||
|
||||
manga.thumbnail_url = coverFile.absolutePath
|
||||
return coverFile
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
|
|
@ -0,0 +1,14 @@
|
|||
package tachiyomi.source.local.filter
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import tachiyomi.source.local.R
|
||||
|
||||
sealed class OrderBy(context: Context, selection: Selection) : Filter.Sort(
|
||||
context.getString(R.string.local_filter_order_by),
|
||||
arrayOf(context.getString(R.string.title), context.getString(R.string.date)),
|
||||
selection,
|
||||
) {
|
||||
class Popular(context: Context) : OrderBy(context, Selection(0, true))
|
||||
class Latest(context: Context) : OrderBy(context, Selection(1, false))
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package tachiyomi.source.local.image
|
||||
|
||||
import android.content.Context
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import tachiyomi.core.util.system.ImageUtil
|
||||
import tachiyomi.source.local.io.LocalSourceFileSystem
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
private const val DEFAULT_COVER_NAME = "cover.jpg"
|
||||
|
||||
class AndroidLocalCoverManager(
|
||||
private val context: Context,
|
||||
private val fileSystem: LocalSourceFileSystem,
|
||||
) : LocalCoverManager {
|
||||
|
||||
override fun find(mangaUrl: String): File? {
|
||||
return fileSystem.getFilesInMangaDirectory(mangaUrl)
|
||||
// Get all file whose names start with 'cover'
|
||||
.filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
|
||||
// Get the first actual image
|
||||
.firstOrNull {
|
||||
ImageUtil.isImage(it.name) { it.inputStream() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun update(manga: SManga, inputStream: InputStream): File? {
|
||||
val directory = fileSystem.getMangaDirectory(manga.url)
|
||||
if (directory == null) {
|
||||
inputStream.close()
|
||||
return null
|
||||
}
|
||||
|
||||
var targetFile = find(manga.url)
|
||||
if (targetFile == null) {
|
||||
targetFile = File(directory.absolutePath, DEFAULT_COVER_NAME)
|
||||
targetFile.createNewFile()
|
||||
}
|
||||
|
||||
// It might not exist at this point
|
||||
targetFile.parentFile?.mkdirs()
|
||||
inputStream.use { input ->
|
||||
targetFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
DiskUtil.createNoMediaFile(UniFile.fromFile(directory), context)
|
||||
|
||||
manga.thumbnail_url = targetFile.absolutePath
|
||||
return targetFile
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package tachiyomi.source.local.image
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
interface LocalCoverManager {
|
||||
|
||||
fun find(mangaUrl: String): File?
|
||||
|
||||
fun update(manga: SManga, inputStream: InputStream): File?
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package tachiyomi.source.local.io
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import tachiyomi.source.local.R
|
||||
import java.io.File
|
||||
|
||||
class AndroidLocalSourceFileSystem(
|
||||
private val context: Context,
|
||||
) : LocalSourceFileSystem {
|
||||
|
||||
private val baseFolderLocation = "${context.getString(R.string.app_name)}${File.separator}local"
|
||||
|
||||
override fun getBaseDirectories(): Sequence<File> {
|
||||
return DiskUtil.getExternalStorages(context)
|
||||
.map { File(it.absolutePath, baseFolderLocation) }
|
||||
.asSequence()
|
||||
}
|
||||
|
||||
override fun getFilesInBaseDirectories(): Sequence<File> {
|
||||
return getBaseDirectories()
|
||||
// Get all the files inside all baseDir
|
||||
.flatMap { it.listFiles().orEmpty().toList() }
|
||||
}
|
||||
|
||||
override fun getMangaDirectory(name: String): File? {
|
||||
return getFilesInBaseDirectories()
|
||||
// Get the first mangaDir or null
|
||||
.firstOrNull { it.isDirectory && it.name == name }
|
||||
}
|
||||
|
||||
override fun getFilesInMangaDirectory(name: String): Sequence<File> {
|
||||
return getFilesInBaseDirectories()
|
||||
// Filter out ones that are not related to the manga and is not a directory
|
||||
.filter { it.isDirectory && it.name == name }
|
||||
// Get all the files inside the filtered folders
|
||||
.flatMap { it.listFiles().orEmpty().toList() }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package tachiyomi.source.local.io
|
||||
|
||||
import java.io.File
|
||||
|
||||
object Archive {
|
||||
|
||||
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
|
||||
|
||||
fun isSupported(file: File): Boolean = with(file) {
|
||||
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package tachiyomi.source.local.io
|
||||
|
||||
import java.io.File
|
||||
|
||||
sealed class Format {
|
||||
data class Directory(val file: File) : Format()
|
||||
data class Zip(val file: File) : Format()
|
||||
data class Rar(val file: File) : Format()
|
||||
data class Epub(val file: File) : Format()
|
||||
|
||||
class UnknownFormatException : Exception()
|
||||
|
||||
companion object {
|
||||
|
||||
fun valueOf(file: File) = with(file) {
|
||||
when {
|
||||
isDirectory -> Directory(this)
|
||||
extension.equals("zip", true) || extension.equals("cbz", true) -> Zip(this)
|
||||
extension.equals("rar", true) || extension.equals("cbr", true) -> Rar(this)
|
||||
extension.equals("epub", true) -> Epub(this)
|
||||
else -> throw UnknownFormatException()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package tachiyomi.source.local.io
|
||||
|
||||
import java.io.File
|
||||
|
||||
interface LocalSourceFileSystem {
|
||||
|
||||
fun getBaseDirectories(): Sequence<File>
|
||||
|
||||
fun getFilesInBaseDirectories(): Sequence<File>
|
||||
|
||||
fun getMangaDirectory(name: String): File?
|
||||
|
||||
fun getFilesInMangaDirectory(name: String): Sequence<File>
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package tachiyomi.source.local.metadata
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Fills manga metadata using this epub file's metadata.
|
||||
*/
|
||||
fun EpubFile.fillMangaMetadata(manga: SManga) {
|
||||
val ref = getPackageHref()
|
||||
val doc = getPackageDocument(ref)
|
||||
|
||||
val creator = doc.getElementsByTag("dc:creator").first()
|
||||
val description = doc.getElementsByTag("dc:description").first()
|
||||
|
||||
manga.author = creator?.text()
|
||||
manga.description = description?.text()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills chapter metadata using this epub file's metadata.
|
||||
*/
|
||||
fun EpubFile.fillChapterMetadata(chapter: SChapter) {
|
||||
val ref = getPackageHref()
|
||||
val doc = getPackageDocument(ref)
|
||||
|
||||
val title = doc.getElementsByTag("dc:title").first()
|
||||
val publisher = doc.getElementsByTag("dc:publisher").first()
|
||||
val creator = doc.getElementsByTag("dc:creator").first()
|
||||
var date = doc.getElementsByTag("dc:date").first()
|
||||
if (date == null) {
|
||||
date = doc.select("meta[property=dcterms:modified]").first()
|
||||
}
|
||||
|
||||
if (title != null) {
|
||||
chapter.name = title.text()
|
||||
}
|
||||
|
||||
if (publisher != null) {
|
||||
chapter.scanlator = publisher.text()
|
||||
} else if (creator != null) {
|
||||
chapter.scanlator = creator.text()
|
||||
}
|
||||
|
||||
if (date != null) {
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault())
|
||||
try {
|
||||
val parsedDate = dateFormat.parse(date.text())
|
||||
if (parsedDate != null) {
|
||||
chapter.date_upload = parsedDate.time
|
||||
}
|
||||
} catch (e: ParseException) {
|
||||
// Empty
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue