mirror of
https://github.com/mihonapp/mihon.git
synced 2024-11-15 15:02:49 +01:00
chore: merge upstream changes.
Signed-off-by: KaiserBh <kaiserbh@proton.me>
This commit is contained in:
commit
73130bc3dd
@ -164,7 +164,6 @@ dependencies {
|
|||||||
implementation(compose.ui.tooling.preview)
|
implementation(compose.ui.tooling.preview)
|
||||||
implementation(compose.ui.util)
|
implementation(compose.ui.util)
|
||||||
implementation(compose.accompanist.webview)
|
implementation(compose.accompanist.webview)
|
||||||
implementation(compose.accompanist.permissions)
|
|
||||||
implementation(compose.accompanist.systemuicontroller)
|
implementation(compose.accompanist.systemuicontroller)
|
||||||
lintChecks(compose.lintchecks)
|
lintChecks(compose.lintchecks)
|
||||||
|
|
||||||
|
@ -7,9 +7,6 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
|
||||||
<!-- Storage -->
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
|
||||||
|
|
||||||
<!-- For background jobs -->
|
<!-- For background jobs -->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
@ -39,7 +36,6 @@
|
|||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:localeConfig="@xml/locales_config"
|
android:localeConfig="@xml/locales_config"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:requestLegacyExternalStorage="true"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Tachiyomi">
|
android:theme="@style/Theme.Tachiyomi">
|
||||||
|
@ -27,7 +27,6 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
@ -36,12 +35,12 @@ import eu.kanade.presentation.more.settings.Preference
|
|||||||
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
|
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
|
||||||
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
|
||||||
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
|
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
|
||||||
import eu.kanade.presentation.permissions.PermissionRequestHelper
|
|
||||||
import eu.kanade.presentation.util.relativeTimeSpanString
|
import eu.kanade.presentation.util.relativeTimeSpanString
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateJob
|
import eu.kanade.tachiyomi.data.backup.BackupCreateJob
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreJob
|
import eu.kanade.tachiyomi.data.backup.BackupRestoreJob
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
|
import eu.kanade.tachiyomi.data.download.DownloadCache
|
||||||
import eu.kanade.tachiyomi.data.sync.SyncDataJob
|
import eu.kanade.tachiyomi.data.sync.SyncDataJob
|
||||||
import eu.kanade.tachiyomi.data.sync.SyncManager
|
import eu.kanade.tachiyomi.data.sync.SyncManager
|
||||||
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
|
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
|
||||||
@ -77,8 +76,6 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
val backupPreferences = Injekt.get<BackupPreferences>()
|
val backupPreferences = Injekt.get<BackupPreferences>()
|
||||||
val storagePreferences = Injekt.get<StoragePreferences>()
|
val storagePreferences = Injekt.get<StoragePreferences>()
|
||||||
|
|
||||||
PermissionRequestHelper.requestStoragePermission()
|
|
||||||
|
|
||||||
val syncPreferences = remember { Injekt.get<SyncPreferences>() }
|
val syncPreferences = remember { Injekt.get<SyncPreferences>() }
|
||||||
val syncService by syncPreferences.syncService().collectAsState()
|
val syncService by syncPreferences.syncService().collectAsState()
|
||||||
|
|
||||||
@ -107,8 +104,10 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
|
|
||||||
context.contentResolver.takePersistableUriPermission(uri, flags)
|
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||||
|
|
||||||
val file = UniFile.fromUri(context, uri)
|
UniFile.fromUri(context, uri)?.let {
|
||||||
storageDirPref.set(file.uri.toString())
|
storageDirPref.set(it.uri.toString())
|
||||||
|
}
|
||||||
|
Injekt.get<DownloadCache>().invalidateCache()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package eu.kanade.presentation.more.stats
|
package eu.kanade.presentation.more.stats
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@ -12,6 +14,7 @@ import androidx.compose.material.icons.outlined.Schedule
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import eu.kanade.presentation.more.stats.components.StatsItem
|
import eu.kanade.presentation.more.stats.components.StatsItem
|
||||||
import eu.kanade.presentation.more.stats.components.StatsOverviewItem
|
import eu.kanade.presentation.more.stats.components.StatsOverviewItem
|
||||||
@ -63,22 +66,24 @@ private fun OverviewSection(
|
|||||||
.toDurationString(context, fallback = none)
|
.toDurationString(context, fallback = none)
|
||||||
}
|
}
|
||||||
StatsSection(MR.strings.label_overview_section) {
|
StatsSection(MR.strings.label_overview_section) {
|
||||||
Row {
|
Row(
|
||||||
|
modifier = Modifier.height(IntrinsicSize.Min),
|
||||||
|
) {
|
||||||
StatsOverviewItem(
|
StatsOverviewItem(
|
||||||
title = data.libraryMangaCount.toString(),
|
title = data.libraryMangaCount.toString(),
|
||||||
subtitle = stringResource(MR.strings.in_library),
|
subtitle = stringResource(MR.strings.in_library),
|
||||||
icon = Icons.Outlined.CollectionsBookmark,
|
icon = Icons.Outlined.CollectionsBookmark,
|
||||||
)
|
)
|
||||||
StatsOverviewItem(
|
|
||||||
title = data.completedMangaCount.toString(),
|
|
||||||
subtitle = stringResource(MR.strings.label_completed_titles),
|
|
||||||
icon = Icons.Outlined.LocalLibrary,
|
|
||||||
)
|
|
||||||
StatsOverviewItem(
|
StatsOverviewItem(
|
||||||
title = readDurationString,
|
title = readDurationString,
|
||||||
subtitle = stringResource(MR.strings.label_read_duration),
|
subtitle = stringResource(MR.strings.label_read_duration),
|
||||||
icon = Icons.Outlined.Schedule,
|
icon = Icons.Outlined.Schedule,
|
||||||
)
|
)
|
||||||
|
StatsOverviewItem(
|
||||||
|
title = data.completedMangaCount.toString(),
|
||||||
|
subtitle = stringResource(MR.strings.label_completed_titles),
|
||||||
|
icon = Icons.Outlined.LocalLibrary,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ package eu.kanade.presentation.more.stats.components
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@ -53,7 +55,9 @@ private fun RowScope.BaseStatsItem(
|
|||||||
icon: ImageVector? = null,
|
icon: ImageVector? = null,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxHeight(),
|
||||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
@ -74,6 +78,7 @@ private fun RowScope.BaseStatsItem(
|
|||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
if (icon != null) {
|
if (icon != null) {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
package eu.kanade.presentation.permissions
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Launches request for [Manifest.permission.WRITE_EXTERNAL_STORAGE] permission
|
|
||||||
*/
|
|
||||||
object PermissionRequestHelper {
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun requestStoragePermission() {
|
|
||||||
val permissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
permissionState.launchPermissionRequest()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -87,8 +87,8 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
|||||||
if (packageName != process) WebView.setDataDirectorySuffix(process)
|
if (packageName != process) WebView.setDataDirectorySuffix(process)
|
||||||
}
|
}
|
||||||
|
|
||||||
Injekt.importModule(AppModule(this))
|
|
||||||
Injekt.importModule(PreferenceModule(this))
|
Injekt.importModule(PreferenceModule(this))
|
||||||
|
Injekt.importModule(AppModule(this))
|
||||||
Injekt.importModule(DomainModule())
|
Injekt.importModule(DomainModule())
|
||||||
|
|
||||||
setupAcra()
|
setupAcra()
|
||||||
|
@ -21,7 +21,7 @@ import eu.kanade.tachiyomi.util.system.workManager
|
|||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.backup.service.BackupPreferences
|
import tachiyomi.domain.backup.service.BackupPreferences
|
||||||
import tachiyomi.domain.storage.service.StoragePreferences
|
import tachiyomi.domain.storage.service.StorageManager
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@ -43,6 +43,8 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
|||||||
|
|
||||||
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
|
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
|
||||||
?: getAutomaticBackupLocation()
|
?: getAutomaticBackupLocation()
|
||||||
|
?: return Result.failure()
|
||||||
|
|
||||||
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupCreateFlags.AutomaticDefaults)
|
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupCreateFlags.AutomaticDefaults)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -56,7 +58,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
|||||||
if (isAutoBackup) {
|
if (isAutoBackup) {
|
||||||
backupPreferences.lastAutoBackupTimestamp().set(Date().time)
|
backupPreferences.lastAutoBackupTimestamp().set(Date().time)
|
||||||
} else {
|
} else {
|
||||||
notifier.showBackupComplete(UniFile.fromUri(context, location.toUri()))
|
notifier.showBackupComplete(UniFile.fromUri(context, location.toUri())!!)
|
||||||
}
|
}
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -75,13 +77,9 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAutomaticBackupLocation(): Uri {
|
private fun getAutomaticBackupLocation(): Uri? {
|
||||||
val storagePreferences = Injekt.get<StoragePreferences>()
|
val storageManager = Injekt.get<StorageManager>()
|
||||||
return storagePreferences.baseStorageDirectory().get().let {
|
return storageManager.getAutomaticBackupsDirectory()?.uri
|
||||||
val dir = UniFile.fromUri(context, it.toUri())
|
|
||||||
.createDirectory(StoragePreferences.BACKUP_DIR)
|
|
||||||
dir.uri
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -93,17 +93,16 @@ class BackupCreator(
|
|||||||
if (isAutoBackup) {
|
if (isAutoBackup) {
|
||||||
// Get dir of file and create
|
// Get dir of file and create
|
||||||
val dir = UniFile.fromUri(context, uri)
|
val dir = UniFile.fromUri(context, uri)
|
||||||
.createDirectory("automatic")
|
|
||||||
|
|
||||||
// Delete older backups
|
// Delete older backups
|
||||||
dir.listFiles { _, filename -> Backup.filenameRegex.matches(filename) }
|
dir?.listFiles { _, filename -> Backup.filenameRegex.matches(filename) }
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.sortedByDescending { it.name }
|
.sortedByDescending { it.name }
|
||||||
.drop(MAX_AUTO_BACKUPS - 1)
|
.drop(MAX_AUTO_BACKUPS - 1)
|
||||||
.forEach { it.delete() }
|
.forEach { it.delete() }
|
||||||
|
|
||||||
// Create new file to place backup
|
// Create new file to place backup
|
||||||
dir.createFile(Backup.getFilename())
|
dir?.createFile(Backup.getFilename())
|
||||||
} else {
|
} else {
|
||||||
UniFile.fromUri(context, uri)
|
UniFile.fromUri(context, uri)
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.download
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
@ -19,6 +18,7 @@ import kotlinx.coroutines.ensureActive
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.drop
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
@ -40,6 +40,8 @@ import kotlinx.serialization.encoding.Decoder
|
|||||||
import kotlinx.serialization.encoding.Encoder
|
import kotlinx.serialization.encoding.Encoder
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
import tachiyomi.core.storage.extension
|
||||||
|
import tachiyomi.core.storage.nameWithoutExtension
|
||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import tachiyomi.core.util.lang.launchNonCancellable
|
import tachiyomi.core.util.lang.launchNonCancellable
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
@ -64,7 +66,7 @@ class DownloadCache(
|
|||||||
private val provider: DownloadProvider = Injekt.get(),
|
private val provider: DownloadProvider = Injekt.get(),
|
||||||
private val sourceManager: SourceManager = Injekt.get(),
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
private val extensionManager: ExtensionManager = Injekt.get(),
|
private val extensionManager: ExtensionManager = Injekt.get(),
|
||||||
private val storagePreferences: StoragePreferences = Injekt.get(),
|
storagePreferences: StoragePreferences = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
@ -95,16 +97,9 @@ class DownloadCache(
|
|||||||
get() = File(context.cacheDir, "dl_index_cache")
|
get() = File(context.cacheDir, "dl_index_cache")
|
||||||
|
|
||||||
private val rootDownloadsDirLock = Mutex()
|
private val rootDownloadsDirLock = Mutex()
|
||||||
private var rootDownloadsDir = RootDirectory(getDirectoryFromPreference())
|
private var rootDownloadsDir = RootDirectory(provider.downloadsDir)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
storagePreferences.baseStorageDirectory().changes()
|
|
||||||
.onEach {
|
|
||||||
rootDownloadsDir = RootDirectory(getDirectoryFromPreference())
|
|
||||||
invalidateCache()
|
|
||||||
}
|
|
||||||
.launchIn(scope)
|
|
||||||
|
|
||||||
// Attempt to read cache file
|
// Attempt to read cache file
|
||||||
scope.launch {
|
scope.launch {
|
||||||
rootDownloadsDirLock.withLock {
|
rootDownloadsDirLock.withLock {
|
||||||
@ -119,6 +114,14 @@ class DownloadCache(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
storagePreferences.baseStorageDirectory().changes()
|
||||||
|
.drop(1)
|
||||||
|
.onEach {
|
||||||
|
rootDownloadsDir = RootDirectory(provider.downloadsDir)
|
||||||
|
invalidateCache()
|
||||||
|
}
|
||||||
|
.launchIn(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -293,14 +296,6 @@ class DownloadCache(
|
|||||||
renewalJob?.cancel()
|
renewalJob?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the downloads directory from the user's preferences.
|
|
||||||
*/
|
|
||||||
private fun getDirectoryFromPreference(): UniFile {
|
|
||||||
return UniFile.fromUri(context, storagePreferences.baseStorageDirectory().get().toUri())
|
|
||||||
.createDirectory(StoragePreferences.DOWNLOADS_DIR)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renews the downloads cache.
|
* Renews the downloads cache.
|
||||||
*/
|
*/
|
||||||
@ -332,7 +327,7 @@ class DownloadCache(
|
|||||||
val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id }
|
val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id }
|
||||||
|
|
||||||
rootDownloadsDirLock.withLock {
|
rootDownloadsDirLock.withLock {
|
||||||
val sourceDirs = rootDownloadsDir.dir.listFiles().orEmpty()
|
val sourceDirs = rootDownloadsDir.dir?.listFiles().orEmpty()
|
||||||
.filter { it.isDirectory && !it.name.isNullOrBlank() }
|
.filter { it.isDirectory && !it.name.isNullOrBlank() }
|
||||||
.mapNotNull { dir ->
|
.mapNotNull { dir ->
|
||||||
val sourceId = sourceMap[dir.name!!.lowercase()]
|
val sourceId = sourceMap[dir.name!!.lowercase()]
|
||||||
@ -345,12 +340,12 @@ class DownloadCache(
|
|||||||
sourceDirs.values
|
sourceDirs.values
|
||||||
.map { sourceDir ->
|
.map { sourceDir ->
|
||||||
async {
|
async {
|
||||||
sourceDir.mangaDirs = sourceDir.dir.listFiles().orEmpty()
|
sourceDir.mangaDirs = sourceDir.dir?.listFiles().orEmpty()
|
||||||
.filter { it.isDirectory && !it.name.isNullOrBlank() }
|
.filter { it.isDirectory && !it.name.isNullOrBlank() }
|
||||||
.associate { it.name!! to MangaDirectory(it) }
|
.associate { it.name!! to MangaDirectory(it) }
|
||||||
|
|
||||||
sourceDir.mangaDirs.values.forEach { mangaDir ->
|
sourceDir.mangaDirs.values.forEach { mangaDir ->
|
||||||
val chapterDirs = mangaDir.dir.listFiles().orEmpty()
|
val chapterDirs = mangaDir.dir?.listFiles().orEmpty()
|
||||||
.mapNotNull {
|
.mapNotNull {
|
||||||
when {
|
when {
|
||||||
// Ignore incomplete downloads
|
// Ignore incomplete downloads
|
||||||
@ -358,8 +353,7 @@ class DownloadCache(
|
|||||||
// Folder of images
|
// Folder of images
|
||||||
it.isDirectory -> it.name
|
it.isDirectory -> it.name
|
||||||
// CBZ files
|
// CBZ files
|
||||||
it.isFile && it.name?.endsWith(".cbz") == true ->
|
it.isFile && it.extension == "cbz" -> it.nameWithoutExtension
|
||||||
it.name!!.substringBeforeLast(".cbz")
|
|
||||||
// Anything else is irrelevant
|
// Anything else is irrelevant
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@ -427,7 +421,7 @@ class DownloadCache(
|
|||||||
@Serializable
|
@Serializable
|
||||||
private class RootDirectory(
|
private class RootDirectory(
|
||||||
@Serializable(with = UniFileAsStringSerializer::class)
|
@Serializable(with = UniFileAsStringSerializer::class)
|
||||||
val dir: UniFile,
|
val dir: UniFile?,
|
||||||
var sourceDirs: Map<Long, SourceDirectory> = mapOf(),
|
var sourceDirs: Map<Long, SourceDirectory> = mapOf(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -437,7 +431,7 @@ private class RootDirectory(
|
|||||||
@Serializable
|
@Serializable
|
||||||
private class SourceDirectory(
|
private class SourceDirectory(
|
||||||
@Serializable(with = UniFileAsStringSerializer::class)
|
@Serializable(with = UniFileAsStringSerializer::class)
|
||||||
val dir: UniFile,
|
val dir: UniFile?,
|
||||||
var mangaDirs: Map<String, MangaDirectory> = mapOf(),
|
var mangaDirs: Map<String, MangaDirectory> = mapOf(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -447,17 +441,26 @@ private class SourceDirectory(
|
|||||||
@Serializable
|
@Serializable
|
||||||
private class MangaDirectory(
|
private class MangaDirectory(
|
||||||
@Serializable(with = UniFileAsStringSerializer::class)
|
@Serializable(with = UniFileAsStringSerializer::class)
|
||||||
val dir: UniFile,
|
val dir: UniFile?,
|
||||||
var chapterDirs: MutableSet<String> = mutableSetOf(),
|
var chapterDirs: MutableSet<String> = mutableSetOf(),
|
||||||
)
|
)
|
||||||
|
|
||||||
private object UniFileAsStringSerializer : KSerializer<UniFile> {
|
private object UniFileAsStringSerializer : KSerializer<UniFile?> {
|
||||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UniFile", PrimitiveKind.STRING)
|
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UniFile", PrimitiveKind.STRING)
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: UniFile) {
|
override fun serialize(encoder: Encoder, value: UniFile?) {
|
||||||
return encoder.encodeString(value.uri.toString())
|
return if (value == null) {
|
||||||
|
encoder.encodeNull()
|
||||||
|
} else {
|
||||||
|
encoder.encodeString(value.uri.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): UniFile? {
|
||||||
|
return if (decoder.decodeNotNullMark()) {
|
||||||
|
UniFile.fromUri(Injekt.get<Application>(), Uri.parse(decoder.decodeString()))
|
||||||
|
} else {
|
||||||
|
decoder.decodeNull()
|
||||||
}
|
}
|
||||||
override fun deserialize(decoder: Decoder): UniFile {
|
|
||||||
return UniFile.fromUri(Injekt.get<Application>(), Uri.parse(decoder.decodeString()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.onStart
|
|||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
|
import tachiyomi.core.storage.extension
|
||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.category.interactor.GetCategories
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
@ -340,7 +341,7 @@ class DownloadManager(
|
|||||||
.firstOrNull() ?: return
|
.firstOrNull() ?: return
|
||||||
|
|
||||||
var newName = provider.getChapterDirName(newChapter.name, newChapter.scanlator)
|
var newName = provider.getChapterDirName(newChapter.name, newChapter.scanlator)
|
||||||
if (oldDownload.isFile && oldDownload.name?.endsWith(".cbz") == true) {
|
if (oldDownload.isFile && oldDownload.extension == "cbz") {
|
||||||
newName += ".cbz"
|
newName += ".cbz"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,19 +1,15 @@
|
|||||||
package eu.kanade.tachiyomi.data.download
|
package eu.kanade.tachiyomi.data.download
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.net.toUri
|
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import kotlinx.coroutines.MainScope
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
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
|
||||||
import tachiyomi.domain.storage.service.StoragePreferences
|
import tachiyomi.domain.storage.service.StorageManager
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@ -26,30 +22,11 @@ import uy.kohesive.injekt.api.get
|
|||||||
*/
|
*/
|
||||||
class DownloadProvider(
|
class DownloadProvider(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val storagePreferences: StoragePreferences = Injekt.get(),
|
private val storageManager: StorageManager = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val scope = MainScope()
|
val downloadsDir: UniFile?
|
||||||
|
get() = storageManager.getDownloadsDirectory()
|
||||||
/**
|
|
||||||
* The root directory for downloads.
|
|
||||||
*/
|
|
||||||
private var downloadsDir = setDownloadsLocation()
|
|
||||||
|
|
||||||
init {
|
|
||||||
storagePreferences.baseStorageDirectory().changes()
|
|
||||||
.onEach { downloadsDir = setDownloadsLocation() }
|
|
||||||
.launchIn(scope)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setDownloadsLocation(): UniFile {
|
|
||||||
return storagePreferences.baseStorageDirectory().get().let {
|
|
||||||
val dir = UniFile.fromUri(context, it.toUri())
|
|
||||||
.createDirectory(StoragePreferences.DOWNLOADS_DIR)
|
|
||||||
DiskUtil.createNoMediaFile(dir, context)
|
|
||||||
dir
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the download directory for a manga. For internal use only.
|
* Returns the download directory for a manga. For internal use only.
|
||||||
@ -59,12 +36,12 @@ class DownloadProvider(
|
|||||||
*/
|
*/
|
||||||
internal fun getMangaDir(mangaTitle: String, source: Source): UniFile {
|
internal fun getMangaDir(mangaTitle: String, source: Source): UniFile {
|
||||||
try {
|
try {
|
||||||
return downloadsDir
|
return downloadsDir!!
|
||||||
.createDirectory(getSourceDirName(source))
|
.createDirectory(getSourceDirName(source))!!
|
||||||
.createDirectory(getMangaDirName(mangaTitle))
|
.createDirectory(getMangaDirName(mangaTitle))!!
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
logcat(LogPriority.ERROR, e) { "Invalid download directory" }
|
logcat(LogPriority.ERROR, e) { "Invalid download directory" }
|
||||||
throw Exception(context.stringResource(MR.strings.invalid_location, downloadsDir))
|
throw Exception(context.stringResource(MR.strings.invalid_location, downloadsDir ?: ""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +51,7 @@ class DownloadProvider(
|
|||||||
* @param source the source to query.
|
* @param source the source to query.
|
||||||
*/
|
*/
|
||||||
fun findSourceDir(source: Source): UniFile? {
|
fun findSourceDir(source: Source): UniFile? {
|
||||||
return downloadsDir.findFile(getSourceDirName(source), true)
|
return downloadsDir?.findFile(getSourceDirName(source), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -114,7 +91,7 @@ class DownloadProvider(
|
|||||||
val mangaDir = findMangaDir(manga.title, source) ?: return null to emptyList()
|
val mangaDir = findMangaDir(manga.title, source) ?: return null to emptyList()
|
||||||
return mangaDir to chapters.mapNotNull { chapter ->
|
return mangaDir to chapters.mapNotNull { chapter ->
|
||||||
getValidChapterDirNames(chapter.name, chapter.scanlator).asSequence()
|
getValidChapterDirNames(chapter.name, chapter.scanlator).asSequence()
|
||||||
.mapNotNull { mangaDir.findFile(it) }
|
.mapNotNull { mangaDir.findFile(it, true) }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,7 @@ import okhttp3.Response
|
|||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
|
import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
|
||||||
import tachiyomi.core.metadata.comicinfo.ComicInfo
|
import tachiyomi.core.metadata.comicinfo.ComicInfo
|
||||||
|
import tachiyomi.core.storage.extension
|
||||||
import tachiyomi.core.util.lang.launchIO
|
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
|
||||||
@ -334,7 +335,7 @@ class Downloader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val chapterDirname = provider.getChapterDirName(download.chapter.name, download.chapter.scanlator)
|
val chapterDirname = provider.getChapterDirName(download.chapter.name, download.chapter.scanlator)
|
||||||
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)
|
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)!!
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// If the page list already exists, start from the file
|
// If the page list already exists, start from the file
|
||||||
@ -353,7 +354,7 @@ class Downloader(
|
|||||||
|
|
||||||
// Delete all temporary (unfinished) files
|
// Delete all temporary (unfinished) files
|
||||||
tmpDir.listFiles()
|
tmpDir.listFiles()
|
||||||
?.filter { it.name!!.endsWith(".tmp") }
|
?.filter { it.extension == "tmp" }
|
||||||
?.forEach { it.delete() }
|
?.forEach { it.delete() }
|
||||||
|
|
||||||
download.status = Download.State.DOWNLOADING
|
download.status = Download.State.DOWNLOADING
|
||||||
@ -479,7 +480,7 @@ class Downloader(
|
|||||||
page.progress = 0
|
page.progress = 0
|
||||||
return flow {
|
return flow {
|
||||||
val response = source.getImage(page)
|
val response = source.getImage(page)
|
||||||
val file = tmpDir.createFile("$filename.tmp")
|
val file = tmpDir.createFile("$filename.tmp")!!
|
||||||
try {
|
try {
|
||||||
response.body.source().saveTo(file.openOutputStream())
|
response.body.source().saveTo(file.openOutputStream())
|
||||||
val extension = getImageExtension(response, file)
|
val extension = getImageExtension(response, file)
|
||||||
@ -511,7 +512,7 @@ class Downloader(
|
|||||||
* @param filename the filename of the image.
|
* @param filename the filename of the image.
|
||||||
*/
|
*/
|
||||||
private fun copyImageFromCache(cacheFile: File, tmpDir: UniFile, filename: String): UniFile {
|
private fun copyImageFromCache(cacheFile: File, tmpDir: UniFile, filename: String): UniFile {
|
||||||
val tmpFile = tmpDir.createFile("$filename.tmp")
|
val tmpFile = tmpDir.createFile("$filename.tmp")!!
|
||||||
cacheFile.inputStream().use { input ->
|
cacheFile.inputStream().use { input ->
|
||||||
tmpFile.openOutputStream().use { output ->
|
tmpFile.openOutputStream().use { output ->
|
||||||
input.copyTo(output)
|
input.copyTo(output)
|
||||||
@ -602,7 +603,7 @@ class Downloader(
|
|||||||
dirname: String,
|
dirname: String,
|
||||||
tmpDir: UniFile,
|
tmpDir: UniFile,
|
||||||
) {
|
) {
|
||||||
val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")
|
val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")!!
|
||||||
ZipOutputStream(BufferedOutputStream(zip.openOutputStream())).use { zipOut ->
|
ZipOutputStream(BufferedOutputStream(zip.openOutputStream())).use { zipOut ->
|
||||||
zipOut.setMethod(ZipEntry.STORED)
|
zipOut.setMethod(ZipEntry.STORED)
|
||||||
|
|
||||||
@ -641,8 +642,8 @@ class Downloader(
|
|||||||
val categories = getCategories.await(manga.id).map { it.name.trim() }.takeUnless { it.isEmpty() }
|
val categories = getCategories.await(manga.id).map { it.name.trim() }.takeUnless { it.isEmpty() }
|
||||||
val comicInfo = getComicInfo(manga, chapter, chapterUrl, categories)
|
val comicInfo = getComicInfo(manga, chapter, chapterUrl, categories)
|
||||||
// Remove the old file
|
// Remove the old file
|
||||||
dir.findFile(COMIC_INFO_FILE)?.delete()
|
dir.findFile(COMIC_INFO_FILE, true)?.delete()
|
||||||
dir.createFile(COMIC_INFO_FILE).openOutputStream().use {
|
dir.createFile(COMIC_INFO_FILE)!!.openOutputStream().use {
|
||||||
val comicInfoString = xml.encodeToString(ComicInfo.serializer(), comicInfo)
|
val comicInfoString = xml.encodeToString(ComicInfo.serializer(), comicInfo)
|
||||||
it.write(comicInfoString.toByteArray())
|
it.write(comicInfoString.toByteArray())
|
||||||
}
|
}
|
||||||
|
@ -33,12 +33,14 @@ import tachiyomi.domain.chapter.model.Chapter
|
|||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.math.RoundingMode
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
|
|
||||||
class LibraryUpdateNotifier(private val context: Context) {
|
class LibraryUpdateNotifier(private val context: Context) {
|
||||||
|
|
||||||
private val preferences: SecurityPreferences by injectLazy()
|
private val preferences: SecurityPreferences by injectLazy()
|
||||||
private val percentFormatter = NumberFormat.getPercentInstance().apply {
|
private val percentFormatter = NumberFormat.getPercentInstance().apply {
|
||||||
|
roundingMode = RoundingMode.DOWN
|
||||||
maximumFractionDigits = 0
|
maximumFractionDigits = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,12 +80,6 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
* @param total the total progress.
|
* @param total the total progress.
|
||||||
*/
|
*/
|
||||||
fun showProgressNotification(manga: List<Manga>, current: Int, total: Int) {
|
fun showProgressNotification(manga: List<Manga>, current: Int, total: Int) {
|
||||||
if (preferences.hideNotificationContent().get()) {
|
|
||||||
progressNotificationBuilder
|
|
||||||
.setContentTitle(context.stringResource(MR.strings.notification_check_updates))
|
|
||||||
.setContentText("($current/$total)")
|
|
||||||
} else {
|
|
||||||
val updatingText = manga.joinToString("\n") { it.title.chop(40) }
|
|
||||||
progressNotificationBuilder
|
progressNotificationBuilder
|
||||||
.setContentTitle(
|
.setContentTitle(
|
||||||
context.stringResource(
|
context.stringResource(
|
||||||
@ -91,7 +87,10 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
percentFormatter.format(current.toFloat() / total),
|
percentFormatter.format(current.toFloat() / total),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
|
|
||||||
|
if (!preferences.hideNotificationContent().get()) {
|
||||||
|
val updatingText = manga.joinToString("\n") { it.title.chop(40) }
|
||||||
|
progressNotificationBuilder.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
|
||||||
}
|
}
|
||||||
|
|
||||||
context.notify(
|
context.notify(
|
||||||
|
@ -4,6 +4,7 @@ import eu.kanade.domain.track.service.TrackPreferences
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import eu.kanade.tachiyomi.util.lang.htmlDecode
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@ -25,7 +26,7 @@ data class ALManga(
|
|||||||
title = title_user_pref
|
title = title_user_pref
|
||||||
total_chapters = this@ALManga.total_chapters
|
total_chapters = this@ALManga.total_chapters
|
||||||
cover_url = image_url_lge
|
cover_url = image_url_lge
|
||||||
summary = description ?: ""
|
summary = description?.htmlDecode() ?: ""
|
||||||
tracking_url = AnilistApi.mangaUrl(media_id)
|
tracking_url = AnilistApi.mangaUrl(media_id)
|
||||||
publishing_status = this@ALManga.publishing_status
|
publishing_status = this@ALManga.publishing_status
|
||||||
publishing_type = format
|
publishing_type = format
|
||||||
|
@ -26,6 +26,7 @@ import kotlinx.serialization.json.Json
|
|||||||
import nl.adaptivity.xmlutil.XmlDeclMode
|
import nl.adaptivity.xmlutil.XmlDeclMode
|
||||||
import nl.adaptivity.xmlutil.core.XmlVersion
|
import nl.adaptivity.xmlutil.core.XmlVersion
|
||||||
import nl.adaptivity.xmlutil.serialization.XML
|
import nl.adaptivity.xmlutil.serialization.XML
|
||||||
|
import tachiyomi.core.storage.AndroidStorageFolderProvider
|
||||||
import tachiyomi.data.AndroidDatabaseHandler
|
import tachiyomi.data.AndroidDatabaseHandler
|
||||||
import tachiyomi.data.Database
|
import tachiyomi.data.Database
|
||||||
import tachiyomi.data.DatabaseHandler
|
import tachiyomi.data.DatabaseHandler
|
||||||
@ -35,6 +36,7 @@ import tachiyomi.data.Mangas
|
|||||||
import tachiyomi.data.StringListColumnAdapter
|
import tachiyomi.data.StringListColumnAdapter
|
||||||
import tachiyomi.data.UpdateStrategyColumnAdapter
|
import tachiyomi.data.UpdateStrategyColumnAdapter
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
|
import tachiyomi.domain.storage.service.StorageManager
|
||||||
import tachiyomi.source.local.image.LocalCoverManager
|
import tachiyomi.source.local.image.LocalCoverManager
|
||||||
import tachiyomi.source.local.io.LocalSourceFileSystem
|
import tachiyomi.source.local.io.LocalSourceFileSystem
|
||||||
import uy.kohesive.injekt.api.InjektModule
|
import uy.kohesive.injekt.api.InjektModule
|
||||||
@ -124,8 +126,10 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
|
|
||||||
addSingletonFactory { ImageSaver(app) }
|
addSingletonFactory { ImageSaver(app) }
|
||||||
|
|
||||||
addSingletonFactory { LocalSourceFileSystem(app) }
|
addSingletonFactory { AndroidStorageFolderProvider(app) }
|
||||||
|
addSingletonFactory { LocalSourceFileSystem(get()) }
|
||||||
addSingletonFactory { LocalCoverManager(app, get()) }
|
addSingletonFactory { LocalCoverManager(app, get()) }
|
||||||
|
addSingletonFactory { StorageManager(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 {
|
||||||
|
@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
|||||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||||
import tachiyomi.core.preference.AndroidPreferenceStore
|
import tachiyomi.core.preference.AndroidPreferenceStore
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
import tachiyomi.core.provider.AndroidStorageFolderProvider
|
import tachiyomi.core.storage.AndroidStorageFolderProvider
|
||||||
import tachiyomi.domain.backup.service.BackupPreferences
|
import tachiyomi.domain.backup.service.BackupPreferences
|
||||||
import tachiyomi.domain.download.service.DownloadPreferences
|
import tachiyomi.domain.download.service.DownloadPreferences
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
@ -55,9 +55,6 @@ class PreferenceModule(val app: Application) : InjektModule {
|
|||||||
addSingletonFactory {
|
addSingletonFactory {
|
||||||
BackupPreferences(get())
|
BackupPreferences(get())
|
||||||
}
|
}
|
||||||
addSingletonFactory {
|
|
||||||
AndroidStorageFolderProvider(app)
|
|
||||||
}
|
|
||||||
addSingletonFactory {
|
addSingletonFactory {
|
||||||
StoragePreferences(
|
StoragePreferences(
|
||||||
folderProvider = get<AndroidStorageFolderProvider>(),
|
folderProvider = get<AndroidStorageFolderProvider>(),
|
||||||
|
@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
|||||||
import eu.kanade.tachiyomi.ui.security.UnlockActivity
|
import eu.kanade.tachiyomi.ui.security.UnlockActivity
|
||||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
|
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
|
||||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
|
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
|
||||||
|
import eu.kanade.tachiyomi.util.system.overridePendingTransitionCompat
|
||||||
import eu.kanade.tachiyomi.util.view.setSecureScreen
|
import eu.kanade.tachiyomi.util.view.setSecureScreen
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
@ -106,7 +107,7 @@ class SecureActivityDelegateImpl : SecureActivityDelegate, DefaultLifecycleObser
|
|||||||
if (activity.isAuthenticationSupported()) {
|
if (activity.isAuthenticationSupported()) {
|
||||||
if (!SecureActivityDelegate.requireUnlock) return
|
if (!SecureActivityDelegate.requireUnlock) return
|
||||||
activity.startActivity(Intent(activity, UnlockActivity::class.java))
|
activity.startActivity(Intent(activity, UnlockActivity::class.java))
|
||||||
activity.overridePendingTransition(0, 0)
|
activity.overridePendingTransitionCompat(0, 0)
|
||||||
} else {
|
} else {
|
||||||
securityPreferences.useAuthenticator().set(false)
|
securityPreferences.useAuthenticator().set(false)
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,6 @@ 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.permissions.PermissionRequestHelper
|
|
||||||
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
|
||||||
@ -66,9 +65,6 @@ data class BrowseTab(
|
|||||||
onChangeSearchQuery = extensionsScreenModel::search,
|
onChangeSearchQuery = extensionsScreenModel::search,
|
||||||
)
|
)
|
||||||
|
|
||||||
// For local source
|
|
||||||
PermissionRequestHelper.requestStoragePermission()
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
(context as? MainActivity)?.ready = true
|
(context as? MainActivity)?.ready = true
|
||||||
}
|
}
|
||||||
|
@ -66,6 +66,7 @@ object HomeScreen : Screen() {
|
|||||||
private val showBottomNavEvent = Channel<Boolean>()
|
private val showBottomNavEvent = Channel<Boolean>()
|
||||||
|
|
||||||
private const val TabFadeDuration = 200
|
private const val TabFadeDuration = 200
|
||||||
|
private const val TabNavigatorKey = "HomeTabs"
|
||||||
|
|
||||||
private val tabs = listOf(
|
private val tabs = listOf(
|
||||||
LibraryTab,
|
LibraryTab,
|
||||||
@ -80,6 +81,7 @@ object HomeScreen : Screen() {
|
|||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
TabNavigator(
|
TabNavigator(
|
||||||
tab = LibraryTab,
|
tab = LibraryTab,
|
||||||
|
key = TabNavigatorKey,
|
||||||
) { tabNavigator ->
|
) { tabNavigator ->
|
||||||
// Provide usable navigator to content screen
|
// Provide usable navigator to content screen
|
||||||
CompositionLocalProvider(LocalNavigator provides navigator) {
|
CompositionLocalProvider(LocalNavigator provides navigator) {
|
||||||
|
@ -70,6 +70,7 @@ 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.hasDisplayCutout
|
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
||||||
import eu.kanade.tachiyomi.util.system.isNightMode
|
import eu.kanade.tachiyomi.util.system.isNightMode
|
||||||
|
import eu.kanade.tachiyomi.util.system.overridePendingTransitionCompat
|
||||||
import eu.kanade.tachiyomi.util.system.toShareIntent
|
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.util.view.setComposeContent
|
import eu.kanade.tachiyomi.util.view.setComposeContent
|
||||||
@ -138,7 +139,7 @@ class ReaderActivity : BaseActivity() {
|
|||||||
*/
|
*/
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
registerSecureActivity(this)
|
registerSecureActivity(this)
|
||||||
overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit)
|
overridePendingTransitionCompat(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit)
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@ -269,7 +270,7 @@ class ReaderActivity : BaseActivity() {
|
|||||||
override fun finish() {
|
override fun finish() {
|
||||||
viewModel.onActivityFinish()
|
viewModel.onActivityFinish()
|
||||||
super.finish()
|
super.finish()
|
||||||
overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit)
|
overridePendingTransitionCompat(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
|
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
|
@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
|
import tachiyomi.core.storage.toTempFile
|
||||||
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
|
||||||
@ -88,13 +89,13 @@ class ChapterLoader(
|
|||||||
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
|
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
|
||||||
when (format) {
|
when (format) {
|
||||||
is Format.Directory -> DirectoryPageLoader(format.file)
|
is Format.Directory -> DirectoryPageLoader(format.file)
|
||||||
is Format.Zip -> ZipPageLoader(format.file)
|
is Format.Zip -> ZipPageLoader(format.file.toTempFile(context))
|
||||||
is Format.Rar -> try {
|
is Format.Rar -> try {
|
||||||
RarPageLoader(format.file)
|
RarPageLoader(format.file.toTempFile(context))
|
||||||
} catch (e: UnsupportedRarV5Exception) {
|
} catch (e: UnsupportedRarV5Exception) {
|
||||||
error(context.stringResource(MR.strings.loader_rar5_error))
|
error(context.stringResource(MR.strings.loader_rar5_error))
|
||||||
}
|
}
|
||||||
is Format.Epub -> EpubPageLoader(format.file)
|
is Format.Epub -> EpubPageLoader(format.file.toTempFile(context))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
source is HttpSource -> HttpPageLoader(chapter, source)
|
source is HttpSource -> HttpPageLoader(chapter, source)
|
||||||
|
@ -1,25 +1,24 @@
|
|||||||
package eu.kanade.tachiyomi.ui.reader.loader
|
package eu.kanade.tachiyomi.ui.reader.loader
|
||||||
|
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
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 tachiyomi.core.util.system.ImageUtil
|
import tachiyomi.core.util.system.ImageUtil
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loader used to load a chapter from a directory given on [file].
|
* Loader used to load a chapter from a directory given on [file].
|
||||||
*/
|
*/
|
||||||
internal class DirectoryPageLoader(val file: File) : PageLoader() {
|
internal class DirectoryPageLoader(val file: UniFile) : PageLoader() {
|
||||||
|
|
||||||
override var isLocal: Boolean = true
|
override var isLocal: Boolean = true
|
||||||
|
|
||||||
override suspend fun getPages(): List<ReaderPage> {
|
override suspend fun getPages(): List<ReaderPage> {
|
||||||
return file.listFiles()
|
return file.listFiles()
|
||||||
?.filter { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
?.filter { !it.isDirectory && ImageUtil.isImage(it.name) { it.openInputStream() } }
|
||||||
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
?.sortedWith { f1, f2 -> f1.name.orEmpty().compareToCaseInsensitiveNaturalOrder(f2.name.orEmpty()) }
|
||||||
?.mapIndexed { i, file ->
|
?.mapIndexed { i, file ->
|
||||||
val streamFn = { FileInputStream(file) }
|
val streamFn = { file.openInputStream() }
|
||||||
ReaderPage(i).apply {
|
ReaderPage(i).apply {
|
||||||
stream = streamFn
|
stream = streamFn
|
||||||
status = Page.State.READY
|
status = Page.State.READY
|
||||||
|
@ -10,9 +10,9 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
|
import tachiyomi.core.storage.toTempFile
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loader used to load a chapter from the downloaded chapters.
|
* Loader used to load a chapter from the downloaded chapters.
|
||||||
@ -47,7 +47,7 @@ internal class DownloadPageLoader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getPagesFromArchive(chapterPath: UniFile): List<ReaderPage> {
|
private suspend fun getPagesFromArchive(chapterPath: UniFile): List<ReaderPage> {
|
||||||
val loader = ZipPageLoader(File(chapterPath.filePath!!)).also { zipPageLoader = it }
|
val loader = ZipPageLoader(chapterPath.toTempFile(context)).also { zipPageLoader = it }
|
||||||
return loader.getPages()
|
return loader.getPages()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
|||||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||||
|
import eu.kanade.tachiyomi.util.system.overridePendingTransitionCompat
|
||||||
import eu.kanade.tachiyomi.util.system.toShareIntent
|
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.util.view.setComposeContent
|
import eu.kanade.tachiyomi.util.view.setComposeContent
|
||||||
@ -35,7 +36,7 @@ class WebViewActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit)
|
overridePendingTransitionCompat(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit)
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
if (!WebViewUtil.supportsWebView(this)) {
|
if (!WebViewUtil.supportsWebView(this)) {
|
||||||
@ -77,7 +78,7 @@ class WebViewActivity : BaseActivity() {
|
|||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
super.finish()
|
super.finish()
|
||||||
overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit)
|
overridePendingTransitionCompat(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shareWebpage(url: String) {
|
private fun shareWebpage(url: String) {
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
package eu.kanade.tachiyomi.util.system
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.AnimRes
|
||||||
|
|
||||||
|
fun Activity.overridePendingTransitionCompat(@AnimRes enterAnim: Int, @AnimRes exitAnim: Int) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
overrideActivityTransition(Activity.OVERRIDE_TRANSITION_OPEN, enterAnim, exitAnim)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
overridePendingTransition(enterAnim, exitAnim)
|
||||||
|
}
|
||||||
|
}
|
@ -165,7 +165,7 @@ fun Context.createReaderThemeContext(): Context {
|
|||||||
* @return document size of [uri] or null if size can't be obtained
|
* @return document size of [uri] or null if size can't be obtained
|
||||||
*/
|
*/
|
||||||
fun Context.getUriSize(uri: Uri): Long? {
|
fun Context.getUriSize(uri: Uri): Long? {
|
||||||
return UniFile.fromUri(this, uri).length().takeIf { it >= 0 }
|
return UniFile.fromUri(this, uri)?.length()?.takeIf { it >= 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
object AndroidConfig {
|
object AndroidConfig {
|
||||||
const val compileSdk = 34
|
const val compileSdk = 34
|
||||||
const val minSdk = 23
|
const val minSdk = 23
|
||||||
const val targetSdk = 29
|
const val targetSdk = 30
|
||||||
const val ndk = "22.1.7171670"
|
const val ndk = "22.1.7171670"
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,7 @@ package eu.kanade.tachiyomi.util.storage
|
|||||||
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.StatFs
|
import android.os.StatFs
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
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
|
||||||
@ -64,23 +62,6 @@ object DiskUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the root folders of all the available external storages.
|
|
||||||
*/
|
|
||||||
fun getExternalStorages(context: Context): List<File> {
|
|
||||||
return ContextCompat.getExternalFilesDirs(context, null)
|
|
||||||
.filterNotNull()
|
|
||||||
.mapNotNull {
|
|
||||||
val file = File(it.absolutePath.substringBefore("/Android/"))
|
|
||||||
val state = Environment.getExternalStorageState(file)
|
|
||||||
if (state == Environment.MEDIA_MOUNTED || state == Environment.MEDIA_MOUNTED_READ_ONLY) {
|
|
||||||
file
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Don't display downloaded chapters in gallery apps creating `.nomedia`.
|
* Don't display downloaded chapters in gallery apps creating `.nomedia`.
|
||||||
*/
|
*/
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package tachiyomi.core.provider
|
package tachiyomi.core.storage
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Environment
|
import android.os.Environment
|
@ -1,4 +1,4 @@
|
|||||||
package tachiyomi.core.provider
|
package tachiyomi.core.storage
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
@ -0,0 +1,38 @@
|
|||||||
|
package tachiyomi.core.storage
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.FileUtils
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
val UniFile.extension: String?
|
||||||
|
get() = name?.substringAfterLast('.')
|
||||||
|
|
||||||
|
val UniFile.nameWithoutExtension: String?
|
||||||
|
get() = name?.substringBeforeLast('.')
|
||||||
|
|
||||||
|
fun UniFile.toTempFile(context: Context): File {
|
||||||
|
val inputStream = context.contentResolver.openInputStream(uri)!!
|
||||||
|
val tempFile = File.createTempFile(
|
||||||
|
nameWithoutExtension.orEmpty(),
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
FileUtils.copy(inputStream, tempFile.outputStream())
|
||||||
|
} else {
|
||||||
|
BufferedOutputStream(tempFile.outputStream()).use { tmpOut ->
|
||||||
|
inputStream.use { input ->
|
||||||
|
val buffer = ByteArray(8192)
|
||||||
|
var count: Int
|
||||||
|
while (input.read(buffer).also { count = it } > 0) {
|
||||||
|
tmpOut.write(buffer, 0, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tempFile
|
||||||
|
}
|
@ -37,7 +37,9 @@ import kotlin.math.min
|
|||||||
|
|
||||||
object ImageUtil {
|
object ImageUtil {
|
||||||
|
|
||||||
fun isImage(name: String, openStream: (() -> InputStream)? = null): Boolean {
|
fun isImage(name: String?, openStream: (() -> InputStream)? = null): Boolean {
|
||||||
|
if (name == null) return false
|
||||||
|
|
||||||
val contentType = try {
|
val contentType = try {
|
||||||
URLConnection.guessContentTypeFromName(name)
|
URLConnection.guessContentTypeFromName(name)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -243,7 +245,7 @@ object ImageUtil {
|
|||||||
// Remove pre-existing split if exists (this split shouldn't exist under normal circumstances)
|
// Remove pre-existing split if exists (this split shouldn't exist under normal circumstances)
|
||||||
tmpDir.findFile(splitImageName)?.delete()
|
tmpDir.findFile(splitImageName)?.delete()
|
||||||
|
|
||||||
val splitFile = tmpDir.createFile(splitImageName)
|
val splitFile = tmpDir.createFile(splitImageName)!!
|
||||||
|
|
||||||
val region = Rect(0, splitData.topOffset, splitData.splitWidth, splitData.bottomOffset)
|
val region = Rect(0, splitData.topOffset, splitData.splitWidth, splitData.bottomOffset)
|
||||||
|
|
||||||
|
@ -19,6 +19,8 @@ dependencies {
|
|||||||
implementation(platform(kotlinx.coroutines.bom))
|
implementation(platform(kotlinx.coroutines.bom))
|
||||||
implementation(kotlinx.bundles.coroutines)
|
implementation(kotlinx.bundles.coroutines)
|
||||||
|
|
||||||
|
implementation(libs.unifile)
|
||||||
|
|
||||||
api(libs.sqldelight.android.paging)
|
api(libs.sqldelight.android.paging)
|
||||||
|
|
||||||
testImplementation(libs.bundles.test)
|
testImplementation(libs.bundles.test)
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
package tachiyomi.domain.storage.service
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
||||||
|
class StorageManager(
|
||||||
|
private val context: Context,
|
||||||
|
storagePreferences: StoragePreferences,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
|
private var baseDir: UniFile? = storagePreferences.baseStorageDirectory().get().let(::getBaseDir)
|
||||||
|
|
||||||
|
init {
|
||||||
|
storagePreferences.baseStorageDirectory().changes()
|
||||||
|
.onEach { baseDir = getBaseDir(it) }
|
||||||
|
.launchIn(scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBaseDir(path: String): UniFile? {
|
||||||
|
val file = UniFile.fromUri(context, path.toUri())
|
||||||
|
|
||||||
|
return file.takeIf { it?.exists() == true }?.also { parent ->
|
||||||
|
parent.createDirectory(AUTOMATIC_BACKUPS_PATH)
|
||||||
|
parent.createDirectory(LOCAL_SOURCE_PATH)
|
||||||
|
parent.createDirectory(DOWNLOADS_PATH).also {
|
||||||
|
DiskUtil.createNoMediaFile(it, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAutomaticBackupsDirectory(): UniFile? {
|
||||||
|
return baseDir?.createDirectory(AUTOMATIC_BACKUPS_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDownloadsDirectory(): UniFile? {
|
||||||
|
return baseDir?.createDirectory(DOWNLOADS_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLocalSourceDirectory(): UniFile? {
|
||||||
|
return baseDir?.createDirectory(LOCAL_SOURCE_PATH)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val AUTOMATIC_BACKUPS_PATH = "autobackup"
|
||||||
|
private const val DOWNLOADS_PATH = "downloads"
|
||||||
|
private const val LOCAL_SOURCE_PATH = "local"
|
@ -1,7 +1,7 @@
|
|||||||
package tachiyomi.domain.storage.service
|
package tachiyomi.domain.storage.service
|
||||||
|
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
import tachiyomi.core.provider.FolderProvider
|
import tachiyomi.core.storage.FolderProvider
|
||||||
|
|
||||||
class StoragePreferences(
|
class StoragePreferences(
|
||||||
private val folderProvider: FolderProvider,
|
private val folderProvider: FolderProvider,
|
||||||
@ -9,9 +9,4 @@ class StoragePreferences(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
fun baseStorageDirectory() = preferenceStore.getString("storage_dir", folderProvider.path())
|
fun baseStorageDirectory() = preferenceStore.getString("storage_dir", folderProvider.path())
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val BACKUP_DIR = "backup"
|
|
||||||
const val DOWNLOADS_DIR = "downloads"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp_version = "8.1.3"
|
agp_version = "8.1.4"
|
||||||
lifecycle_version = "2.6.2"
|
lifecycle_version = "2.6.2"
|
||||||
paging_version = "3.2.1"
|
paging_version = "3.2.1"
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
compiler = "1.5.4"
|
compiler = "1.5.4"
|
||||||
compose-bom = "2023.12.00-alpha01"
|
compose-bom = "2023.12.00-alpha02"
|
||||||
accompanist = "0.33.2-alpha"
|
accompanist = "0.33.2-alpha"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
@ -22,7 +22,6 @@ material-core = { module = "androidx.compose.material:material" }
|
|||||||
glance = "androidx.glance:glance-appwidget:1.0.0"
|
glance = "androidx.glance:glance-appwidget:1.0.0"
|
||||||
|
|
||||||
accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanist" }
|
accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanist" }
|
||||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
|
|
||||||
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" }
|
accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" }
|
||||||
|
|
||||||
lintchecks = { module = "com.slack.lint.compose:compose-lint-checks", version = "1.2.0" }
|
lintchecks = { module = "com.slack.lint.compose:compose-lint-checks", version = "1.2.0" }
|
@ -30,7 +30,7 @@ quickjs-android = "app.cash.quickjs:quickjs-android:0.9.2"
|
|||||||
jsoup = "org.jsoup:jsoup:1.16.2"
|
jsoup = "org.jsoup:jsoup:1.16.2"
|
||||||
|
|
||||||
disklrucache = "com.jakewharton:disklrucache:2.0.2"
|
disklrucache = "com.jakewharton:disklrucache:2.0.2"
|
||||||
unifile = "com.github.tachiyomiorg:unifile:17bec43"
|
unifile = "com.github.tachiyomiorg:unifile:7c257e1c64"
|
||||||
junrar = "com.github.junrar:junrar:7.5.5"
|
junrar = "com.github.junrar:junrar:7.5.5"
|
||||||
|
|
||||||
sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" }
|
sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" }
|
||||||
|
@ -839,7 +839,6 @@
|
|||||||
<string name="download_queue_size_warning">Warning: large bulk downloads may lead to sources becoming slower and/or blocking Tachiyomi. Tap to learn more.</string>
|
<string name="download_queue_size_warning">Warning: large bulk downloads may lead to sources becoming slower and/or blocking Tachiyomi. Tap to learn more.</string>
|
||||||
|
|
||||||
<!-- Library update service notifications -->
|
<!-- Library update service notifications -->
|
||||||
<string name="notification_check_updates">Checking for new chapters</string>
|
|
||||||
<string name="notification_updating_progress">Updating library… (%s)</string>
|
<string name="notification_updating_progress">Updating library… (%s)</string>
|
||||||
<string name="notification_size_warning">Large updates harm sources and may lead to slower updates and also increased battery usage. Tap to learn more.</string>
|
<string name="notification_size_warning">Large updates harm sources and may lead to slower updates and also increased battery usage. Tap to learn more.</string>
|
||||||
<string name="notification_new_chapters">New chapters found</string>
|
<string name="notification_new_chapters">New chapters found</string>
|
||||||
|
@ -4,6 +4,7 @@ import androidx.compose.animation.core.animate
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
|
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshState
|
import androidx.compose.material3.pulltorefresh.PullToRefreshState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -46,6 +47,7 @@ fun PullRefresh(
|
|||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
val state = rememberPullToRefreshState(
|
val state = rememberPullToRefreshState(
|
||||||
|
initialRefreshing = refreshing,
|
||||||
extraVerticalOffset = indicatorPadding.calculateTopPadding(),
|
extraVerticalOffset = indicatorPadding.calculateTopPadding(),
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
)
|
)
|
||||||
@ -84,12 +86,15 @@ fun PullRefresh(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopCenter)
|
.align(Alignment.TopCenter)
|
||||||
.padding(contentPadding),
|
.padding(contentPadding),
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun rememberPullToRefreshState(
|
private fun rememberPullToRefreshState(
|
||||||
|
initialRefreshing: Boolean,
|
||||||
extraVerticalOffset: Dp,
|
extraVerticalOffset: Dp,
|
||||||
positionalThreshold: Dp = 64.dp,
|
positionalThreshold: Dp = 64.dp,
|
||||||
enabled: () -> Boolean = { true },
|
enabled: () -> Boolean = { true },
|
||||||
@ -108,7 +113,7 @@ private fun rememberPullToRefreshState(
|
|||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
PullToRefreshStateImpl(
|
PullToRefreshStateImpl(
|
||||||
initialRefreshing = false,
|
initialRefreshing = initialRefreshing,
|
||||||
extraVerticalOffset = extraVerticalOffsetPx,
|
extraVerticalOffset = extraVerticalOffsetPx,
|
||||||
positionalThreshold = positionalThresholdPx,
|
positionalThreshold = positionalThresholdPx,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
@ -133,18 +138,21 @@ private class PullToRefreshStateImpl(
|
|||||||
) : PullToRefreshState {
|
) : PullToRefreshState {
|
||||||
|
|
||||||
override val progress get() = adjustedDistancePulled / positionalThreshold
|
override val progress get() = adjustedDistancePulled / positionalThreshold
|
||||||
override var verticalOffset by mutableFloatStateOf(0f)
|
override var verticalOffset by mutableFloatStateOf(if (initialRefreshing) refreshingVerticalOffset else 0f)
|
||||||
|
|
||||||
override var isRefreshing by mutableStateOf(initialRefreshing)
|
override var isRefreshing by mutableStateOf(initialRefreshing)
|
||||||
|
|
||||||
|
private val refreshingVerticalOffset: Float
|
||||||
|
get() = positionalThreshold + extraVerticalOffset
|
||||||
|
|
||||||
override fun startRefresh() {
|
override fun startRefresh() {
|
||||||
isRefreshing = true
|
isRefreshing = true
|
||||||
verticalOffset = positionalThreshold + extraVerticalOffset
|
verticalOffset = refreshingVerticalOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun startRefreshAnimated() {
|
suspend fun startRefreshAnimated() {
|
||||||
isRefreshing = true
|
isRefreshing = true
|
||||||
animateTo(positionalThreshold + extraVerticalOffset)
|
animateTo(refreshingVerticalOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun endRefresh() {
|
override fun endRefresh() {
|
||||||
@ -196,7 +204,7 @@ private class PullToRefreshStateImpl(
|
|||||||
val newOffset = (distancePulled + available.y).coerceAtLeast(0f)
|
val newOffset = (distancePulled + available.y).coerceAtLeast(0f)
|
||||||
val dragConsumed = newOffset - distancePulled
|
val dragConsumed = newOffset - distancePulled
|
||||||
distancePulled = newOffset
|
distancePulled = newOffset
|
||||||
verticalOffset = calculateVerticalOffset() + (extraVerticalOffset * progress)
|
verticalOffset = calculateVerticalOffset() + (extraVerticalOffset * progress.coerceIn(0f, 1f))
|
||||||
dragConsumed
|
dragConsumed
|
||||||
}
|
}
|
||||||
return Offset(0f, y)
|
return Offset(0f, y)
|
||||||
|
@ -11,6 +11,8 @@ 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.EpubFile
|
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
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
|
||||||
@ -22,6 +24,9 @@ import tachiyomi.core.metadata.comicinfo.ComicInfo
|
|||||||
import tachiyomi.core.metadata.comicinfo.copyFromComicInfo
|
import tachiyomi.core.metadata.comicinfo.copyFromComicInfo
|
||||||
import tachiyomi.core.metadata.comicinfo.getComicInfo
|
import tachiyomi.core.metadata.comicinfo.getComicInfo
|
||||||
import tachiyomi.core.metadata.tachiyomi.MangaDetails
|
import tachiyomi.core.metadata.tachiyomi.MangaDetails
|
||||||
|
import tachiyomi.core.storage.extension
|
||||||
|
import tachiyomi.core.storage.nameWithoutExtension
|
||||||
|
import tachiyomi.core.storage.toTempFile
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import tachiyomi.core.util.system.ImageUtil
|
import tachiyomi.core.util.system.ImageUtil
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
@ -33,11 +38,9 @@ import tachiyomi.source.local.image.LocalCoverManager
|
|||||||
import tachiyomi.source.local.io.Archive
|
import tachiyomi.source.local.io.Archive
|
||||||
import tachiyomi.source.local.io.Format
|
import tachiyomi.source.local.io.Format
|
||||||
import tachiyomi.source.local.io.LocalSourceFileSystem
|
import tachiyomi.source.local.io.LocalSourceFileSystem
|
||||||
import tachiyomi.source.local.metadata.fillChapterMetadata
|
import tachiyomi.source.local.metadata.fillMetadata
|
||||||
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.InputStream
|
import java.io.InputStream
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
@ -72,22 +75,22 @@ actual class LocalSource(
|
|||||||
|
|
||||||
override suspend fun getLatestUpdates(page: Int) = getSearchManga(page, "", LATEST_FILTERS)
|
override suspend fun getLatestUpdates(page: Int) = getSearchManga(page, "", LATEST_FILTERS)
|
||||||
|
|
||||||
override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage {
|
override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage = withIOContext {
|
||||||
val baseDirsFiles = fileSystem.getFilesInBaseDirectories()
|
val lastModifiedLimit = if (filters === LATEST_FILTERS) {
|
||||||
val lastModifiedLimit by lazy {
|
|
||||||
if (filters === LATEST_FILTERS) {
|
|
||||||
System.currentTimeMillis() - LATEST_THRESHOLD
|
System.currentTimeMillis() - LATEST_THRESHOLD
|
||||||
} else {
|
} else {
|
||||||
0L
|
0L
|
||||||
}
|
}
|
||||||
}
|
|
||||||
var mangaDirs = baseDirsFiles
|
var mangaDirs = fileSystem.getFilesInBaseDirectory()
|
||||||
// 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.orEmpty().startsWith('.') }
|
||||||
.distinctBy { it.name }
|
.distinctBy { it.name }
|
||||||
.filter { // Filter by query or last modified
|
.filter {
|
||||||
if (lastModifiedLimit == 0L) {
|
if (lastModifiedLimit == 0L && query.isBlank()) {
|
||||||
it.name.contains(query, ignoreCase = true)
|
true
|
||||||
|
} else if (lastModifiedLimit == 0L) {
|
||||||
|
it.name.orEmpty().contains(query, ignoreCase = true)
|
||||||
} else {
|
} else {
|
||||||
it.lastModified() >= lastModifiedLimit
|
it.lastModified() >= lastModifiedLimit
|
||||||
}
|
}
|
||||||
@ -97,71 +100,53 @@ actual class LocalSource(
|
|||||||
when (filter) {
|
when (filter) {
|
||||||
is OrderBy.Popular -> {
|
is OrderBy.Popular -> {
|
||||||
mangaDirs = if (filter.state!!.ascending) {
|
mangaDirs = if (filter.state!!.ascending) {
|
||||||
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name.orEmpty() })
|
||||||
} else {
|
} else {
|
||||||
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
|
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name.orEmpty() })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is OrderBy.Latest -> {
|
is OrderBy.Latest -> {
|
||||||
mangaDirs = if (filter.state!!.ascending) {
|
mangaDirs = if (filter.state!!.ascending) {
|
||||||
mangaDirs.sortedBy(File::lastModified)
|
mangaDirs.sortedBy(UniFile::lastModified)
|
||||||
} else {
|
} else {
|
||||||
mangaDirs.sortedByDescending(File::lastModified)
|
mangaDirs.sortedByDescending(UniFile::lastModified)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
/* Do nothing */
|
/* Do nothing */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform mangaDirs to list of SManga
|
val mangas = mangaDirs
|
||||||
val mangas = mangaDirs.map { mangaDir ->
|
.map { mangaDir ->
|
||||||
|
async {
|
||||||
SManga.create().apply {
|
SManga.create().apply {
|
||||||
title = mangaDir.name
|
title = mangaDir.name.orEmpty()
|
||||||
url = mangaDir.name
|
url = mangaDir.name.orEmpty()
|
||||||
|
|
||||||
// Try to find the cover
|
// Try to find the cover
|
||||||
coverManager.find(mangaDir.name)
|
coverManager.find(mangaDir.name.orEmpty())?.let {
|
||||||
?.takeIf(File::exists)
|
thumbnail_url = it.filePath
|
||||||
?.let { thumbnail_url = it.absolutePath }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.awaitAll()
|
||||||
|
|
||||||
// Fetch chapters of all the manga
|
MangasPage(mangas, false)
|
||||||
mangas.forEach { manga ->
|
|
||||||
val chapters = getChapterList(manga)
|
|
||||||
if (chapters.isNotEmpty()) {
|
|
||||||
val chapter = chapters.last()
|
|
||||||
val format = getFormat(chapter)
|
|
||||||
|
|
||||||
if (format is Format.Epub) {
|
|
||||||
EpubFile(format.file).use { epub ->
|
|
||||||
epub.fillMangaMetadata(manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the cover from the first chapter found if not available
|
|
||||||
if (manga.thumbnail_url == null) {
|
|
||||||
updateCover(chapter, manga)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return MangasPage(mangas.toList(), false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manga details related
|
// Manga details related
|
||||||
override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext {
|
override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext {
|
||||||
coverManager.find(manga.url)?.let {
|
coverManager.find(manga.url)?.let {
|
||||||
manga.thumbnail_url = it.absolutePath
|
manga.thumbnail_url = it.filePath
|
||||||
}
|
}
|
||||||
|
|
||||||
// Augment manga details based on metadata files
|
// Augment manga details based on metadata files
|
||||||
try {
|
try {
|
||||||
val mangaDir = fileSystem.getMangaDirectory(manga.url)
|
val mangaDir by lazy { fileSystem.getMangaDirectory(manga.url) }
|
||||||
val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url).toList()
|
val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url)
|
||||||
|
|
||||||
val comicInfoFile = mangaDirFiles
|
val comicInfoFile = mangaDirFiles
|
||||||
.firstOrNull { it.name == COMIC_INFO_FILE }
|
.firstOrNull { it.name == COMIC_INFO_FILE }
|
||||||
@ -174,13 +159,13 @@ actual class LocalSource(
|
|||||||
// Top level ComicInfo.xml
|
// Top level ComicInfo.xml
|
||||||
comicInfoFile != null -> {
|
comicInfoFile != null -> {
|
||||||
noXmlFile?.delete()
|
noXmlFile?.delete()
|
||||||
setMangaDetailsFromComicInfoFile(comicInfoFile.inputStream(), manga)
|
setMangaDetailsFromComicInfoFile(comicInfoFile.openInputStream(), manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Old custom JSON format
|
// Old custom JSON format
|
||||||
// TODO: remove support for this entirely after a while
|
// TODO: remove support for this entirely after a while
|
||||||
legacyJsonDetailsFile != null -> {
|
legacyJsonDetailsFile != null -> {
|
||||||
json.decodeFromStream<MangaDetails>(legacyJsonDetailsFile.inputStream()).run {
|
json.decodeFromStream<MangaDetails>(legacyJsonDetailsFile.openInputStream()).run {
|
||||||
title?.let { manga.title = it }
|
title?.let { manga.title = it }
|
||||||
author?.let { manga.author = it }
|
author?.let { manga.author = it }
|
||||||
artist?.let { manga.artist = it }
|
artist?.let { manga.artist = it }
|
||||||
@ -190,7 +175,7 @@ actual class LocalSource(
|
|||||||
}
|
}
|
||||||
// Replace with ComicInfo.xml file
|
// Replace with ComicInfo.xml file
|
||||||
val comicInfo = manga.getComicInfo()
|
val comicInfo = manga.getComicInfo()
|
||||||
UniFile.fromFile(mangaDir)
|
mangaDir
|
||||||
?.createFile(COMIC_INFO_FILE)
|
?.createFile(COMIC_INFO_FILE)
|
||||||
?.openOutputStream()
|
?.openOutputStream()
|
||||||
?.use {
|
?.use {
|
||||||
@ -206,14 +191,14 @@ actual class LocalSource(
|
|||||||
.filter(Archive::isSupported)
|
.filter(Archive::isSupported)
|
||||||
.toList()
|
.toList()
|
||||||
|
|
||||||
val folderPath = mangaDir?.absolutePath
|
val folderPath = mangaDir?.filePath
|
||||||
|
|
||||||
val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)
|
val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)
|
||||||
if (copiedFile != null) {
|
if (copiedFile != null) {
|
||||||
setMangaDetailsFromComicInfoFile(copiedFile.inputStream(), manga)
|
setMangaDetailsFromComicInfoFile(copiedFile.inputStream(), manga)
|
||||||
} else {
|
} else {
|
||||||
// Avoid re-scanning
|
// Avoid re-scanning
|
||||||
File("$folderPath/.noxml").createNewFile()
|
mangaDir?.createFile(".noxml")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -224,11 +209,11 @@ actual class LocalSource(
|
|||||||
return@withIOContext manga
|
return@withIOContext manga
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copyComicInfoFileFromArchive(chapterArchives: List<File>, folderPath: String?): File? {
|
private fun copyComicInfoFileFromArchive(chapterArchives: List<UniFile>, folderPath: String?): File? {
|
||||||
for (chapter in chapterArchives) {
|
for (chapter in chapterArchives) {
|
||||||
when (Format.valueOf(chapter)) {
|
when (Format.valueOf(chapter)) {
|
||||||
is Format.Zip -> {
|
is Format.Zip -> {
|
||||||
ZipFile(chapter).use { zip: ZipFile ->
|
ZipFile(chapter.toTempFile(context)).use { zip: ZipFile ->
|
||||||
zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile ->
|
zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile ->
|
||||||
zip.getInputStream(comicInfoFile).buffered().use { stream ->
|
zip.getInputStream(comicInfoFile).buffered().use { stream ->
|
||||||
return copyComicInfoFile(stream, folderPath)
|
return copyComicInfoFile(stream, folderPath)
|
||||||
@ -237,7 +222,7 @@ actual class LocalSource(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Format.Rar -> {
|
is Format.Rar -> {
|
||||||
JunrarArchive(chapter).use { rar ->
|
JunrarArchive(chapter.toTempFile(context)).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)
|
||||||
@ -268,8 +253,8 @@ actual class LocalSource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Chapters
|
// Chapters
|
||||||
override suspend fun getChapterList(manga: SManga): List<SChapter> {
|
override suspend fun getChapterList(manga: SManga): List<SChapter> = withIOContext {
|
||||||
return fileSystem.getFilesInMangaDirectory(manga.url)
|
val chapters = fileSystem.getFilesInMangaDirectory(manga.url)
|
||||||
// Only keep supported formats
|
// Only keep supported formats
|
||||||
.filter { it.isDirectory || Archive.isSupported(it) }
|
.filter { it.isDirectory || Archive.isSupported(it) }
|
||||||
.map { chapterFile ->
|
.map { chapterFile ->
|
||||||
@ -279,7 +264,7 @@ actual class LocalSource(
|
|||||||
chapterFile.name
|
chapterFile.name
|
||||||
} else {
|
} else {
|
||||||
chapterFile.nameWithoutExtension
|
chapterFile.nameWithoutExtension
|
||||||
}
|
}.orEmpty()
|
||||||
date_upload = chapterFile.lastModified()
|
date_upload = chapterFile.lastModified()
|
||||||
chapter_number = ChapterRecognition
|
chapter_number = ChapterRecognition
|
||||||
.parseChapterNumber(manga.title, this.name, this.chapter_number.toDouble())
|
.parseChapterNumber(manga.title, this.name, this.chapter_number.toDouble())
|
||||||
@ -287,8 +272,8 @@ actual class LocalSource(
|
|||||||
|
|
||||||
val format = Format.valueOf(chapterFile)
|
val format = Format.valueOf(chapterFile)
|
||||||
if (format is Format.Epub) {
|
if (format is Format.Epub) {
|
||||||
EpubFile(format.file).use { epub ->
|
EpubFile(format.file.toTempFile(context)).use { epub ->
|
||||||
epub.fillChapterMetadata(this)
|
epub.fillMetadata(manga, this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -297,7 +282,15 @@ actual class LocalSource(
|
|||||||
val c = c2.chapter_number.compareTo(c1.chapter_number)
|
val c = c2.chapter_number.compareTo(c1.chapter_number)
|
||||||
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
|
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
|
||||||
}
|
}
|
||||||
.toList()
|
|
||||||
|
// Copy the cover from the first chapter found if not available
|
||||||
|
if (manga.thumbnail_url.isNullOrBlank()) {
|
||||||
|
chapters.lastOrNull()?.let { chapter ->
|
||||||
|
updateCover(chapter, manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chapters
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
@ -308,9 +301,10 @@ actual class LocalSource(
|
|||||||
|
|
||||||
fun getFormat(chapter: SChapter): Format {
|
fun getFormat(chapter: SChapter): Format {
|
||||||
try {
|
try {
|
||||||
return fileSystem.getBaseDirectories()
|
val (mangaDirName, chapterName) = chapter.url.split('/', limit = 2)
|
||||||
.map { dir -> File(dir, chapter.url) }
|
return fileSystem.getBaseDirectory()
|
||||||
.find { it.exists() }
|
?.findFile(mangaDirName, true)
|
||||||
|
?.findFile(chapterName, true)
|
||||||
?.let(Format.Companion::valueOf)
|
?.let(Format.Companion::valueOf)
|
||||||
?: throw Exception(context.stringResource(MR.strings.chapter_not_found))
|
?: throw Exception(context.stringResource(MR.strings.chapter_not_found))
|
||||||
} catch (e: Format.UnknownFormatException) {
|
} catch (e: Format.UnknownFormatException) {
|
||||||
@ -320,18 +314,24 @@ actual class LocalSource(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateCover(chapter: SChapter, manga: SManga): File? {
|
private fun updateCover(chapter: SChapter, manga: SManga): UniFile? {
|
||||||
return try {
|
return try {
|
||||||
when (val format = getFormat(chapter)) {
|
when (val format = getFormat(chapter)) {
|
||||||
is Format.Directory -> {
|
is Format.Directory -> {
|
||||||
val entry = format.file.listFiles()
|
val entry = format.file.listFiles()
|
||||||
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
?.sortedWith { f1, f2 ->
|
||||||
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
f1.name.orEmpty().compareToCaseInsensitiveNaturalOrder(
|
||||||
|
f2.name.orEmpty(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
?.find {
|
||||||
|
!it.isDirectory && ImageUtil.isImage(it.name) { it.openInputStream() }
|
||||||
|
}
|
||||||
|
|
||||||
entry?.let { coverManager.update(manga, it.inputStream()) }
|
entry?.let { coverManager.update(manga, it.openInputStream()) }
|
||||||
}
|
}
|
||||||
is Format.Zip -> {
|
is Format.Zip -> {
|
||||||
ZipFile(format.file).use { zip ->
|
ZipFile(format.file.toTempFile(context)).use { zip ->
|
||||||
val entry = zip.entries().toList()
|
val entry = zip.entries().toList()
|
||||||
.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) } }
|
||||||
@ -340,7 +340,7 @@ actual class LocalSource(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Format.Rar -> {
|
is Format.Rar -> {
|
||||||
JunrarArchive(format.file).use { archive ->
|
JunrarArchive(format.file.toTempFile(context)).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) } }
|
||||||
@ -349,7 +349,7 @@ actual class LocalSource(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Format.Epub -> {
|
is Format.Epub -> {
|
||||||
EpubFile(format.file).use { epub ->
|
EpubFile(format.file.toTempFile(context)).use { epub ->
|
||||||
val entry = epub.getImagesFromPages()
|
val entry = epub.getImagesFromPages()
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
?.let { epub.getEntry(it) }
|
?.let { epub.getEntry(it) }
|
||||||
|
@ -4,9 +4,9 @@ import android.content.Context
|
|||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
|
import tachiyomi.core.storage.nameWithoutExtension
|
||||||
import tachiyomi.core.util.system.ImageUtil
|
import tachiyomi.core.util.system.ImageUtil
|
||||||
import tachiyomi.source.local.io.LocalSourceFileSystem
|
import tachiyomi.source.local.io.LocalSourceFileSystem
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
private const val DEFAULT_COVER_NAME = "cover.jpg"
|
private const val DEFAULT_COVER_NAME = "cover.jpg"
|
||||||
@ -16,43 +16,35 @@ actual class LocalCoverManager(
|
|||||||
private val fileSystem: LocalSourceFileSystem,
|
private val fileSystem: LocalSourceFileSystem,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
actual fun find(mangaUrl: String): File? {
|
actual fun find(mangaUrl: String): UniFile? {
|
||||||
return fileSystem.getFilesInMangaDirectory(mangaUrl)
|
return fileSystem.getFilesInMangaDirectory(mangaUrl)
|
||||||
// Get all file whose names start with "cover"
|
// Get all file whose names start with "cover"
|
||||||
.filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
|
.filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
|
||||||
// Get the first actual image
|
// Get the first actual image
|
||||||
.firstOrNull {
|
.firstOrNull { ImageUtil.isImage(it.name) { it.openInputStream() } }
|
||||||
ImageUtil.isImage(it.name) { it.inputStream() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun update(
|
actual fun update(
|
||||||
manga: SManga,
|
manga: SManga,
|
||||||
inputStream: InputStream,
|
inputStream: InputStream,
|
||||||
): File? {
|
): UniFile? {
|
||||||
val directory = fileSystem.getMangaDirectory(manga.url)
|
val directory = fileSystem.getMangaDirectory(manga.url)
|
||||||
if (directory == null) {
|
if (directory == null) {
|
||||||
inputStream.close()
|
inputStream.close()
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
var targetFile = find(manga.url)
|
val targetFile = find(manga.url) ?: directory.createFile(DEFAULT_COVER_NAME)!!
|
||||||
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 ->
|
inputStream.use { input ->
|
||||||
targetFile.outputStream().use { output ->
|
targetFile.openOutputStream().use { output ->
|
||||||
input.copyTo(output)
|
input.copyTo(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DiskUtil.createNoMediaFile(UniFile.fromFile(directory), context)
|
DiskUtil.createNoMediaFile(directory, context)
|
||||||
|
|
||||||
manga.thumbnail_url = targetFile.absolutePath
|
manga.thumbnail_url = targetFile.uri.toString()
|
||||||
return targetFile
|
return targetFile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,40 +1,30 @@
|
|||||||
package tachiyomi.source.local.io
|
package tachiyomi.source.local.io
|
||||||
|
|
||||||
import android.content.Context
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import tachiyomi.domain.storage.service.StorageManager
|
||||||
import tachiyomi.core.i18n.stringResource
|
|
||||||
import tachiyomi.i18n.MR
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
actual class LocalSourceFileSystem(
|
actual class LocalSourceFileSystem(
|
||||||
private val context: Context,
|
private val storageManager: StorageManager,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val baseFolderLocation = "${context.stringResource(MR.strings.app_name)}${File.separator}local"
|
actual fun getBaseDirectory(): UniFile? {
|
||||||
|
return storageManager.getLocalSourceDirectory()
|
||||||
actual fun getBaseDirectories(): Sequence<File> {
|
|
||||||
return DiskUtil.getExternalStorages(context)
|
|
||||||
.map { File(it.absolutePath, baseFolderLocation) }
|
|
||||||
.asSequence()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun getFilesInBaseDirectories(): Sequence<File> {
|
actual fun getFilesInBaseDirectory(): List<UniFile> {
|
||||||
return getBaseDirectories()
|
return getBaseDirectory()?.listFiles().orEmpty().toList()
|
||||||
// Get all the files inside all baseDir
|
|
||||||
.flatMap { it.listFiles().orEmpty().toList() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun getMangaDirectory(name: String): File? {
|
actual fun getMangaDirectory(name: String): UniFile? {
|
||||||
return getFilesInBaseDirectories()
|
return getBaseDirectory()
|
||||||
// Get the first mangaDir or null
|
?.findFile(name, true)
|
||||||
.firstOrNull { it.isDirectory && it.name == name }
|
?.takeIf { it.isDirectory }
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun getFilesInMangaDirectory(name: String): Sequence<File> {
|
actual fun getFilesInMangaDirectory(name: String): List<UniFile> {
|
||||||
return getFilesInBaseDirectories()
|
return getBaseDirectory()
|
||||||
// Filter out ones that are not related to the manga and is not a directory
|
?.findFile(name, true)
|
||||||
.filter { it.isDirectory && it.name == name }
|
?.takeIf { it.isDirectory }
|
||||||
// Get all the files inside the filtered folders
|
?.listFiles().orEmpty().toList()
|
||||||
.flatMap { it.listFiles().orEmpty().toList() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,37 +8,25 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fills manga metadata using this epub file's metadata.
|
* Fills manga and chapter metadata using this epub file's metadata.
|
||||||
*/
|
*/
|
||||||
fun EpubFile.fillMangaMetadata(manga: SManga) {
|
fun EpubFile.fillMetadata(manga: SManga, chapter: SChapter) {
|
||||||
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 ref = getPackageHref()
|
||||||
val doc = getPackageDocument(ref)
|
val doc = getPackageDocument(ref)
|
||||||
|
|
||||||
val title = doc.getElementsByTag("dc:title").first()
|
val title = doc.getElementsByTag("dc:title").first()
|
||||||
val publisher = doc.getElementsByTag("dc:publisher").first()
|
val publisher = doc.getElementsByTag("dc:publisher").first()
|
||||||
val creator = doc.getElementsByTag("dc:creator").first()
|
val creator = doc.getElementsByTag("dc:creator").first()
|
||||||
|
val description = doc.getElementsByTag("dc:description").first()
|
||||||
var date = doc.getElementsByTag("dc:date").first()
|
var date = doc.getElementsByTag("dc:date").first()
|
||||||
if (date == null) {
|
if (date == null) {
|
||||||
date = doc.select("meta[property=dcterms:modified]").first()
|
date = doc.select("meta[property=dcterms:modified]").first()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (title != null) {
|
creator?.text()?.let { manga.author = it }
|
||||||
chapter.name = title.text()
|
description?.text()?.let { manga.description = it }
|
||||||
}
|
|
||||||
|
title?.text()?.let { chapter.name = it }
|
||||||
|
|
||||||
if (publisher != null) {
|
if (publisher != null) {
|
||||||
chapter.scanlator = publisher.text()
|
chapter.scanlator = publisher.text()
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
package tachiyomi.source.local.image
|
package tachiyomi.source.local.image
|
||||||
|
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
expect class LocalCoverManager {
|
expect class LocalCoverManager {
|
||||||
|
|
||||||
fun find(mangaUrl: String): File?
|
fun find(mangaUrl: String): UniFile?
|
||||||
|
|
||||||
fun update(manga: SManga, inputStream: InputStream): File?
|
fun update(manga: SManga, inputStream: InputStream): UniFile?
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
package tachiyomi.source.local.io
|
package tachiyomi.source.local.io
|
||||||
|
|
||||||
import java.io.File
|
import com.hippo.unifile.UniFile
|
||||||
|
import tachiyomi.core.storage.extension
|
||||||
|
|
||||||
object Archive {
|
object Archive {
|
||||||
|
|
||||||
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
|
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
|
||||||
|
|
||||||
fun isSupported(file: File): Boolean = with(file) {
|
fun isSupported(file: UniFile): Boolean {
|
||||||
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
|
return file.extension in SUPPORTED_ARCHIVE_TYPES
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
package tachiyomi.source.local.io
|
package tachiyomi.source.local.io
|
||||||
|
|
||||||
import java.io.File
|
import com.hippo.unifile.UniFile
|
||||||
|
import tachiyomi.core.storage.extension
|
||||||
|
|
||||||
sealed interface Format {
|
sealed interface Format {
|
||||||
data class Directory(val file: File) : Format
|
data class Directory(val file: UniFile) : Format
|
||||||
data class Zip(val file: File) : Format
|
data class Zip(val file: UniFile) : Format
|
||||||
data class Rar(val file: File) : Format
|
data class Rar(val file: UniFile) : Format
|
||||||
data class Epub(val file: File) : Format
|
data class Epub(val file: UniFile) : Format
|
||||||
|
|
||||||
class UnknownFormatException : Exception()
|
class UnknownFormatException : Exception()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun valueOf(file: File) = with(file) {
|
fun valueOf(file: UniFile) = with(file) {
|
||||||
when {
|
when {
|
||||||
isDirectory -> Directory(this)
|
isDirectory -> Directory(this)
|
||||||
extension.equals("zip", true) || extension.equals("cbz", true) -> Zip(this)
|
extension.equals("zip", true) || extension.equals("cbz", true) -> Zip(this)
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
package tachiyomi.source.local.io
|
package tachiyomi.source.local.io
|
||||||
|
|
||||||
import java.io.File
|
import com.hippo.unifile.UniFile
|
||||||
|
|
||||||
expect class LocalSourceFileSystem {
|
expect class LocalSourceFileSystem {
|
||||||
|
|
||||||
fun getBaseDirectories(): Sequence<File>
|
fun getBaseDirectory(): UniFile?
|
||||||
|
|
||||||
fun getFilesInBaseDirectories(): Sequence<File>
|
fun getFilesInBaseDirectory(): List<UniFile>
|
||||||
|
|
||||||
fun getMangaDirectory(name: String): File?
|
fun getMangaDirectory(name: String): UniFile?
|
||||||
|
|
||||||
fun getFilesInMangaDirectory(name: String): Sequence<File>
|
fun getFilesInMangaDirectory(name: String): List<UniFile>
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user