diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index f62ce9cbc..a48438b57 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -164,7 +164,6 @@ dependencies {
implementation(compose.ui.tooling.preview)
implementation(compose.ui.util)
implementation(compose.accompanist.webview)
- implementation(compose.accompanist.permissions)
implementation(compose.accompanist.systemuicontroller)
lintChecks(compose.lintchecks)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f6cc6ed32..fe6f4a6c7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -7,9 +7,6 @@
-
-
-
@@ -39,7 +36,6 @@
android:largeHeap="true"
android:localeConfig="@xml/locales_config"
android:networkSecurityConfig="@xml/network_security_config"
- android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Tachiyomi">
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt
index 631a68949..04d1137a0 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt
@@ -27,7 +27,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringResource
import androidx.core.net.toUri
import cafe.adriel.voyager.navigator.LocalNavigator
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.widget.BasePreferenceWidget
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
-import eu.kanade.presentation.permissions.PermissionRequestHelper
import eu.kanade.presentation.util.relativeTimeSpanString
import eu.kanade.tachiyomi.data.backup.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.BackupRestoreJob
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.SyncManager
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
@@ -77,8 +76,6 @@ object SettingsDataScreen : SearchableSettings {
val backupPreferences = Injekt.get()
val storagePreferences = Injekt.get()
- PermissionRequestHelper.requestStoragePermission()
-
val syncPreferences = remember { Injekt.get() }
val syncService by syncPreferences.syncService().collectAsState()
@@ -107,8 +104,10 @@ object SettingsDataScreen : SearchableSettings {
context.contentResolver.takePersistableUriPermission(uri, flags)
- val file = UniFile.fromUri(context, uri)
- storageDirPref.set(file.uri.toString())
+ UniFile.fromUri(context, uri)?.let {
+ storageDirPref.set(it.uri.toString())
+ }
+ Injekt.get().invalidateCache()
}
}
diff --git a/app/src/main/java/eu/kanade/presentation/more/stats/StatsScreenContent.kt b/app/src/main/java/eu/kanade/presentation/more/stats/StatsScreenContent.kt
index f35336d50..64f59280c 100644
--- a/app/src/main/java/eu/kanade/presentation/more/stats/StatsScreenContent.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/stats/StatsScreenContent.kt
@@ -1,8 +1,10 @@
package eu.kanade.presentation.more.stats
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
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.runtime.Composable
import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import eu.kanade.presentation.more.stats.components.StatsItem
import eu.kanade.presentation.more.stats.components.StatsOverviewItem
@@ -63,22 +66,24 @@ private fun OverviewSection(
.toDurationString(context, fallback = none)
}
StatsSection(MR.strings.label_overview_section) {
- Row {
+ Row(
+ modifier = Modifier.height(IntrinsicSize.Min),
+ ) {
StatsOverviewItem(
title = data.libraryMangaCount.toString(),
subtitle = stringResource(MR.strings.in_library),
icon = Icons.Outlined.CollectionsBookmark,
)
- StatsOverviewItem(
- title = data.completedMangaCount.toString(),
- subtitle = stringResource(MR.strings.label_completed_titles),
- icon = Icons.Outlined.LocalLibrary,
- )
StatsOverviewItem(
title = readDurationString,
subtitle = stringResource(MR.strings.label_read_duration),
icon = Icons.Outlined.Schedule,
)
+ StatsOverviewItem(
+ title = data.completedMangaCount.toString(),
+ subtitle = stringResource(MR.strings.label_completed_titles),
+ icon = Icons.Outlined.LocalLibrary,
+ )
}
}
}
diff --git a/app/src/main/java/eu/kanade/presentation/more/stats/components/StatsItem.kt b/app/src/main/java/eu/kanade/presentation/more/stats/components/StatsItem.kt
index 32f4fcd28..8002b3d04 100644
--- a/app/src/main/java/eu/kanade/presentation/more/stats/components/StatsItem.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/stats/components/StatsItem.kt
@@ -3,6 +3,8 @@ package eu.kanade.presentation.more.stats.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.MaterialTheme
import androidx.compose.material3.Text
@@ -53,7 +55,9 @@ private fun RowScope.BaseStatsItem(
icon: ImageVector? = null,
) {
Column(
- modifier = Modifier.weight(1f),
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxHeight(),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
horizontalAlignment = Alignment.CenterHorizontally,
) {
@@ -74,6 +78,7 @@ private fun RowScope.BaseStatsItem(
textAlign = TextAlign.Center,
)
if (icon != null) {
+ Spacer(modifier = Modifier.weight(1f))
Icon(
imageVector = icon,
contentDescription = null,
diff --git a/app/src/main/java/eu/kanade/presentation/permissions/PermissionRequestHelper.kt b/app/src/main/java/eu/kanade/presentation/permissions/PermissionRequestHelper.kt
deleted file mode 100644
index 7ce28f9da..000000000
--- a/app/src/main/java/eu/kanade/presentation/permissions/PermissionRequestHelper.kt
+++ /dev/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()
- }
- }
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt
index 67ed7146e..27235ba64 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/App.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt
@@ -87,8 +87,8 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
if (packageName != process) WebView.setDataDirectorySuffix(process)
}
- Injekt.importModule(AppModule(this))
Injekt.importModule(PreferenceModule(this))
+ Injekt.importModule(AppModule(this))
Injekt.importModule(DomainModule())
setupAcra()
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt
index c28d12f26..bc37b8c75 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt
@@ -21,7 +21,7 @@ import eu.kanade.tachiyomi.util.system.workManager
import logcat.LogPriority
import tachiyomi.core.util.system.logcat
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.api.get
import java.util.Date
@@ -43,6 +43,8 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
?: getAutomaticBackupLocation()
+ ?: return Result.failure()
+
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupCreateFlags.AutomaticDefaults)
try {
@@ -56,7 +58,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
if (isAutoBackup) {
backupPreferences.lastAutoBackupTimestamp().set(Date().time)
} else {
- notifier.showBackupComplete(UniFile.fromUri(context, location.toUri()))
+ notifier.showBackupComplete(UniFile.fromUri(context, location.toUri())!!)
}
Result.success()
} catch (e: Exception) {
@@ -75,13 +77,9 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
)
}
- private fun getAutomaticBackupLocation(): Uri {
- val storagePreferences = Injekt.get()
- return storagePreferences.baseStorageDirectory().get().let {
- val dir = UniFile.fromUri(context, it.toUri())
- .createDirectory(StoragePreferences.BACKUP_DIR)
- dir.uri
- }
+ private fun getAutomaticBackupLocation(): Uri? {
+ val storageManager = Injekt.get()
+ return storageManager.getAutomaticBackupsDirectory()?.uri
}
companion object {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt
index 415900488..1c335ae6e 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt
@@ -93,17 +93,16 @@ class BackupCreator(
if (isAutoBackup) {
// Get dir of file and create
val dir = UniFile.fromUri(context, uri)
- .createDirectory("automatic")
// Delete older backups
- dir.listFiles { _, filename -> Backup.filenameRegex.matches(filename) }
+ dir?.listFiles { _, filename -> Backup.filenameRegex.matches(filename) }
.orEmpty()
.sortedByDescending { it.name }
.drop(MAX_AUTO_BACKUPS - 1)
.forEach { it.delete() }
// Create new file to place backup
- dir.createFile(Backup.getFilename())
+ dir?.createFile(Backup.getFilename())
} else {
UniFile.fromUri(context, uri)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt
index 407d2d505..9fe7dbfee 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.download
import android.app.Application
import android.content.Context
import android.net.Uri
-import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.Source
@@ -19,6 +18,7 @@ import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
@@ -40,6 +40,8 @@ import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.protobuf.ProtoBuf
import logcat.LogPriority
+import tachiyomi.core.storage.extension
+import tachiyomi.core.storage.nameWithoutExtension
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.system.logcat
@@ -64,7 +66,7 @@ class DownloadCache(
private val provider: DownloadProvider = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(),
- private val storagePreferences: StoragePreferences = Injekt.get(),
+ storagePreferences: StoragePreferences = Injekt.get(),
) {
private val scope = CoroutineScope(Dispatchers.IO)
@@ -95,16 +97,9 @@ class DownloadCache(
get() = File(context.cacheDir, "dl_index_cache")
private val rootDownloadsDirLock = Mutex()
- private var rootDownloadsDir = RootDirectory(getDirectoryFromPreference())
+ private var rootDownloadsDir = RootDirectory(provider.downloadsDir)
init {
- storagePreferences.baseStorageDirectory().changes()
- .onEach {
- rootDownloadsDir = RootDirectory(getDirectoryFromPreference())
- invalidateCache()
- }
- .launchIn(scope)
-
// Attempt to read cache file
scope.launch {
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()
}
- /**
- * 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.
*/
@@ -332,7 +327,7 @@ class DownloadCache(
val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id }
rootDownloadsDirLock.withLock {
- val sourceDirs = rootDownloadsDir.dir.listFiles().orEmpty()
+ val sourceDirs = rootDownloadsDir.dir?.listFiles().orEmpty()
.filter { it.isDirectory && !it.name.isNullOrBlank() }
.mapNotNull { dir ->
val sourceId = sourceMap[dir.name!!.lowercase()]
@@ -345,12 +340,12 @@ class DownloadCache(
sourceDirs.values
.map { sourceDir ->
async {
- sourceDir.mangaDirs = sourceDir.dir.listFiles().orEmpty()
+ sourceDir.mangaDirs = sourceDir.dir?.listFiles().orEmpty()
.filter { it.isDirectory && !it.name.isNullOrBlank() }
.associate { it.name!! to MangaDirectory(it) }
sourceDir.mangaDirs.values.forEach { mangaDir ->
- val chapterDirs = mangaDir.dir.listFiles().orEmpty()
+ val chapterDirs = mangaDir.dir?.listFiles().orEmpty()
.mapNotNull {
when {
// Ignore incomplete downloads
@@ -358,8 +353,7 @@ class DownloadCache(
// Folder of images
it.isDirectory -> it.name
// CBZ files
- it.isFile && it.name?.endsWith(".cbz") == true ->
- it.name!!.substringBeforeLast(".cbz")
+ it.isFile && it.extension == "cbz" -> it.nameWithoutExtension
// Anything else is irrelevant
else -> null
}
@@ -427,7 +421,7 @@ class DownloadCache(
@Serializable
private class RootDirectory(
@Serializable(with = UniFileAsStringSerializer::class)
- val dir: UniFile,
+ val dir: UniFile?,
var sourceDirs: Map = mapOf(),
)
@@ -437,7 +431,7 @@ private class RootDirectory(
@Serializable
private class SourceDirectory(
@Serializable(with = UniFileAsStringSerializer::class)
- val dir: UniFile,
+ val dir: UniFile?,
var mangaDirs: Map = mapOf(),
)
@@ -447,17 +441,26 @@ private class SourceDirectory(
@Serializable
private class MangaDirectory(
@Serializable(with = UniFileAsStringSerializer::class)
- val dir: UniFile,
+ val dir: UniFile?,
var chapterDirs: MutableSet = mutableSetOf(),
)
-private object UniFileAsStringSerializer : KSerializer {
+private object UniFileAsStringSerializer : KSerializer {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UniFile", PrimitiveKind.STRING)
- override fun serialize(encoder: Encoder, value: UniFile) {
- return encoder.encodeString(value.uri.toString())
+ override fun serialize(encoder: Encoder, value: UniFile?) {
+ return if (value == null) {
+ encoder.encodeNull()
+ } else {
+ encoder.encodeString(value.uri.toString())
+ }
}
- override fun deserialize(decoder: Decoder): UniFile {
- return UniFile.fromUri(Injekt.get(), Uri.parse(decoder.decodeString()))
+
+ override fun deserialize(decoder: Decoder): UniFile? {
+ return if (decoder.decodeNotNullMark()) {
+ UniFile.fromUri(Injekt.get(), Uri.parse(decoder.decodeString()))
+ } else {
+ decoder.decodeNull()
+ }
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt
index 0b404165f..ba5c4d81a 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt
@@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.runBlocking
import logcat.LogPriority
import tachiyomi.core.i18n.stringResource
+import tachiyomi.core.storage.extension
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.category.interactor.GetCategories
@@ -340,7 +341,7 @@ class DownloadManager(
.firstOrNull() ?: return
var newName = provider.getChapterDirName(newChapter.name, newChapter.scanlator)
- if (oldDownload.isFile && oldDownload.name?.endsWith(".cbz") == true) {
+ if (oldDownload.isFile && oldDownload.extension == "cbz") {
newName += ".cbz"
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt
index eb444398a..e29628878 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt
@@ -1,19 +1,15 @@
package eu.kanade.tachiyomi.data.download
import android.content.Context
-import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.Source
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 tachiyomi.core.i18n.stringResource
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga
-import tachiyomi.domain.storage.service.StoragePreferences
+import tachiyomi.domain.storage.service.StorageManager
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -26,30 +22,11 @@ import uy.kohesive.injekt.api.get
*/
class DownloadProvider(
private val context: Context,
- private val storagePreferences: StoragePreferences = Injekt.get(),
+ private val storageManager: StorageManager = Injekt.get(),
) {
- private val scope = MainScope()
-
- /**
- * 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
- }
- }
+ val downloadsDir: UniFile?
+ get() = storageManager.getDownloadsDirectory()
/**
* 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 {
try {
- return downloadsDir
- .createDirectory(getSourceDirName(source))
- .createDirectory(getMangaDirName(mangaTitle))
+ return downloadsDir!!
+ .createDirectory(getSourceDirName(source))!!
+ .createDirectory(getMangaDirName(mangaTitle))!!
} catch (e: Throwable) {
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.
*/
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()
return mangaDir to chapters.mapNotNull { chapter ->
getValidChapterDirNames(chapter.name, chapter.scanlator).asSequence()
- .mapNotNull { mangaDir.findFile(it) }
+ .mapNotNull { mangaDir.findFile(it, true) }
.firstOrNull()
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt
index f89146e3a..5e58d4f6a 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt
@@ -43,6 +43,7 @@ import okhttp3.Response
import tachiyomi.core.i18n.stringResource
import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
import tachiyomi.core.metadata.comicinfo.ComicInfo
+import tachiyomi.core.storage.extension
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.launchNow
import tachiyomi.core.util.lang.withIOContext
@@ -334,7 +335,7 @@ class Downloader(
}
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 {
// If the page list already exists, start from the file
@@ -353,7 +354,7 @@ class Downloader(
// Delete all temporary (unfinished) files
tmpDir.listFiles()
- ?.filter { it.name!!.endsWith(".tmp") }
+ ?.filter { it.extension == "tmp" }
?.forEach { it.delete() }
download.status = Download.State.DOWNLOADING
@@ -479,7 +480,7 @@ class Downloader(
page.progress = 0
return flow {
val response = source.getImage(page)
- val file = tmpDir.createFile("$filename.tmp")
+ val file = tmpDir.createFile("$filename.tmp")!!
try {
response.body.source().saveTo(file.openOutputStream())
val extension = getImageExtension(response, file)
@@ -511,7 +512,7 @@ class Downloader(
* @param filename the filename of the image.
*/
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 ->
tmpFile.openOutputStream().use { output ->
input.copyTo(output)
@@ -602,7 +603,7 @@ class Downloader(
dirname: String,
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 ->
zipOut.setMethod(ZipEntry.STORED)
@@ -641,8 +642,8 @@ class Downloader(
val categories = getCategories.await(manga.id).map { it.name.trim() }.takeUnless { it.isEmpty() }
val comicInfo = getComicInfo(manga, chapter, chapterUrl, categories)
// Remove the old file
- dir.findFile(COMIC_INFO_FILE)?.delete()
- dir.createFile(COMIC_INFO_FILE).openOutputStream().use {
+ dir.findFile(COMIC_INFO_FILE, true)?.delete()
+ dir.createFile(COMIC_INFO_FILE)!!.openOutputStream().use {
val comicInfoString = xml.encodeToString(ComicInfo.serializer(), comicInfo)
it.write(comicInfoString.toByteArray())
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt
index 4a21b9bc0..d9e8dc54a 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt
@@ -33,12 +33,14 @@ import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy
+import java.math.RoundingMode
import java.text.NumberFormat
class LibraryUpdateNotifier(private val context: Context) {
private val preferences: SecurityPreferences by injectLazy()
private val percentFormatter = NumberFormat.getPercentInstance().apply {
+ roundingMode = RoundingMode.DOWN
maximumFractionDigits = 0
}
@@ -78,20 +80,17 @@ class LibraryUpdateNotifier(private val context: Context) {
* @param total the total progress.
*/
fun showProgressNotification(manga: List, current: Int, total: Int) {
- if (preferences.hideNotificationContent().get()) {
- progressNotificationBuilder
- .setContentTitle(context.stringResource(MR.strings.notification_check_updates))
- .setContentText("($current/$total)")
- } else {
+ progressNotificationBuilder
+ .setContentTitle(
+ context.stringResource(
+ MR.strings.notification_updating_progress,
+ percentFormatter.format(current.toFloat() / total),
+ ),
+ )
+
+ if (!preferences.hideNotificationContent().get()) {
val updatingText = manga.joinToString("\n") { it.title.chop(40) }
- progressNotificationBuilder
- .setContentTitle(
- context.stringResource(
- MR.strings.notification_updating_progress,
- percentFormatter.format(current.toFloat() / total),
- ),
- )
- .setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
+ progressNotificationBuilder.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
}
context.notify(
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt
index 46de5e735..e0103371e 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt
@@ -4,6 +4,7 @@ import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch
+import eu.kanade.tachiyomi.util.lang.htmlDecode
import kotlinx.serialization.Serializable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
@@ -25,7 +26,7 @@ data class ALManga(
title = title_user_pref
total_chapters = this@ALManga.total_chapters
cover_url = image_url_lge
- summary = description ?: ""
+ summary = description?.htmlDecode() ?: ""
tracking_url = AnilistApi.mangaUrl(media_id)
publishing_status = this@ALManga.publishing_status
publishing_type = format
diff --git a/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt
index 903f13566..c95e73017 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt
@@ -26,6 +26,7 @@ import kotlinx.serialization.json.Json
import nl.adaptivity.xmlutil.XmlDeclMode
import nl.adaptivity.xmlutil.core.XmlVersion
import nl.adaptivity.xmlutil.serialization.XML
+import tachiyomi.core.storage.AndroidStorageFolderProvider
import tachiyomi.data.AndroidDatabaseHandler
import tachiyomi.data.Database
import tachiyomi.data.DatabaseHandler
@@ -35,6 +36,7 @@ import tachiyomi.data.Mangas
import tachiyomi.data.StringListColumnAdapter
import tachiyomi.data.UpdateStrategyColumnAdapter
import tachiyomi.domain.source.service.SourceManager
+import tachiyomi.domain.storage.service.StorageManager
import tachiyomi.source.local.image.LocalCoverManager
import tachiyomi.source.local.io.LocalSourceFileSystem
import uy.kohesive.injekt.api.InjektModule
@@ -124,8 +126,10 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { ImageSaver(app) }
- addSingletonFactory { LocalSourceFileSystem(app) }
+ addSingletonFactory { AndroidStorageFolderProvider(app) }
+ addSingletonFactory { LocalSourceFileSystem(get()) }
addSingletonFactory { LocalCoverManager(app, get()) }
+ addSingletonFactory { StorageManager(app, get()) }
// Asynchronously init expensive components for a faster cold start
ContextCompat.getMainExecutor(app).execute {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt
index ce5d7eedd..0981ead0f 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt
@@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.system.isDevFlavor
import tachiyomi.core.preference.AndroidPreferenceStore
import tachiyomi.core.preference.PreferenceStore
-import tachiyomi.core.provider.AndroidStorageFolderProvider
+import tachiyomi.core.storage.AndroidStorageFolderProvider
import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.library.service.LibraryPreferences
@@ -55,9 +55,6 @@ class PreferenceModule(val app: Application) : InjektModule {
addSingletonFactory {
BackupPreferences(get())
}
- addSingletonFactory {
- AndroidStorageFolderProvider(app)
- }
addSingletonFactory {
StoragePreferences(
folderProvider = get(),
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/SecureActivityDelegate.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/SecureActivityDelegate.kt
index 02589cec9..dc017c328 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/SecureActivityDelegate.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/SecureActivityDelegate.kt
@@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.ui.security.UnlockActivity
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
+import eu.kanade.tachiyomi.util.system.overridePendingTransitionCompat
import eu.kanade.tachiyomi.util.view.setSecureScreen
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
@@ -106,7 +107,7 @@ class SecureActivityDelegateImpl : SecureActivityDelegate, DefaultLifecycleObser
if (activity.isAuthenticationSupported()) {
if (!SecureActivityDelegate.requireUnlock) return
activity.startActivity(Intent(activity, UnlockActivity::class.java))
- activity.overridePendingTransition(0, 0)
+ activity.overridePendingTransitionCompat(0, 0)
} else {
securityPreferences.useAuthenticator().set(false)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt
index a05cba620..2ce4685bb 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt
@@ -13,7 +13,6 @@ import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import eu.kanade.presentation.components.TabbedScreen
-import eu.kanade.presentation.permissions.PermissionRequestHelper
import eu.kanade.presentation.util.Tab
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
@@ -66,9 +65,6 @@ data class BrowseTab(
onChangeSearchQuery = extensionsScreenModel::search,
)
- // For local source
- PermissionRequestHelper.requestStoragePermission()
-
LaunchedEffect(Unit) {
(context as? MainActivity)?.ready = true
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt
index 236e80f83..13319c948 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt
@@ -66,6 +66,7 @@ object HomeScreen : Screen() {
private val showBottomNavEvent = Channel()
private const val TabFadeDuration = 200
+ private const val TabNavigatorKey = "HomeTabs"
private val tabs = listOf(
LibraryTab,
@@ -80,6 +81,7 @@ object HomeScreen : Screen() {
val navigator = LocalNavigator.currentOrThrow
TabNavigator(
tab = LibraryTab,
+ key = TabNavigatorKey,
) { tabNavigator ->
// Provide usable navigator to content screen
CompositionLocalProvider(LocalNavigator provides navigator) {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
index f5b647e85..7900b377c 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
@@ -70,6 +70,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
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.toast
import eu.kanade.tachiyomi.util.view.setComposeContent
@@ -138,7 +139,7 @@ class ReaderActivity : BaseActivity() {
*/
override fun onCreate(savedInstanceState: Bundle?) {
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)
@@ -269,7 +270,7 @@ class ReaderActivity : BaseActivity() {
override fun finish() {
viewModel.onActivityFinish()
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 {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt
index 4989b4704..6a31ed029 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import tachiyomi.core.i18n.stringResource
+import tachiyomi.core.storage.toTempFile
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.manga.model.Manga
@@ -88,13 +89,13 @@ class ChapterLoader(
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
when (format) {
is Format.Directory -> DirectoryPageLoader(format.file)
- is Format.Zip -> ZipPageLoader(format.file)
+ is Format.Zip -> ZipPageLoader(format.file.toTempFile(context))
is Format.Rar -> try {
- RarPageLoader(format.file)
+ RarPageLoader(format.file.toTempFile(context))
} catch (e: UnsupportedRarV5Exception) {
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)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt
index 837986b28..2a11f74e3 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt
@@ -1,25 +1,24 @@
package eu.kanade.tachiyomi.ui.reader.loader
+import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
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].
*/
-internal class DirectoryPageLoader(val file: File) : PageLoader() {
+internal class DirectoryPageLoader(val file: UniFile) : PageLoader() {
override var isLocal: Boolean = true
override suspend fun getPages(): List {
return file.listFiles()
- ?.filter { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
- ?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
+ ?.filter { !it.isDirectory && ImageUtil.isImage(it.name) { it.openInputStream() } }
+ ?.sortedWith { f1, f2 -> f1.name.orEmpty().compareToCaseInsensitiveNaturalOrder(f2.name.orEmpty()) }
?.mapIndexed { i, file ->
- val streamFn = { FileInputStream(file) }
+ val streamFn = { file.openInputStream() }
ReaderPage(i).apply {
stream = streamFn
status = Page.State.READY
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt
index 64b6a73f5..3d385551d 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt
@@ -10,9 +10,9 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
+import tachiyomi.core.storage.toTempFile
import tachiyomi.domain.manga.model.Manga
import uy.kohesive.injekt.injectLazy
-import java.io.File
/**
* Loader used to load a chapter from the downloaded chapters.
@@ -47,7 +47,7 @@ internal class DownloadPageLoader(
}
private suspend fun getPagesFromArchive(chapterPath: UniFile): List {
- val loader = ZipPageLoader(File(chapterPath.filePath!!)).also { zipPageLoader = it }
+ val loader = ZipPageLoader(chapterPath.toTempFile(context)).also { zipPageLoader = it }
return loader.getPages()
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt
index 61d09dc79..5afd8364c 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt
@@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.util.system.WebViewUtil
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.toast
import eu.kanade.tachiyomi.util.view.setComposeContent
@@ -35,7 +36,7 @@ class WebViewActivity : BaseActivity() {
}
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)
if (!WebViewUtil.supportsWebView(this)) {
@@ -77,7 +78,7 @@ class WebViewActivity : BaseActivity() {
override fun 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) {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ActivityExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ActivityExtensions.kt
new file mode 100644
index 000000000..c145f0e28
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ActivityExtensions.kt
@@ -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)
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt
index 14f637366..5f91bfbac 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt
@@ -165,7 +165,7 @@ fun Context.createReaderThemeContext(): Context {
* @return document size of [uri] or null if size can't be obtained
*/
fun Context.getUriSize(uri: Uri): Long? {
- return UniFile.fromUri(this, uri).length().takeIf { it >= 0 }
+ return UniFile.fromUri(this, uri)?.length()?.takeIf { it >= 0 }
}
/**
diff --git a/buildSrc/src/main/kotlin/AndroidConfig.kt b/buildSrc/src/main/kotlin/AndroidConfig.kt
index f63c07729..2127d0657 100644
--- a/buildSrc/src/main/kotlin/AndroidConfig.kt
+++ b/buildSrc/src/main/kotlin/AndroidConfig.kt
@@ -1,6 +1,6 @@
object AndroidConfig {
const val compileSdk = 34
const val minSdk = 23
- const val targetSdk = 29
+ const val targetSdk = 30
const val ndk = "22.1.7171670"
}
diff --git a/core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt b/core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt
index 5ea61b5a5..fc0e81e65 100644
--- a/core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt
+++ b/core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt
@@ -3,9 +3,7 @@ package eu.kanade.tachiyomi.util.storage
import android.content.Context
import android.media.MediaScannerConnection
import android.net.Uri
-import android.os.Environment
import android.os.StatFs
-import androidx.core.content.ContextCompat
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.util.lang.Hash
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 {
- 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`.
*/
diff --git a/core/src/main/java/tachiyomi/core/provider/AndroidStorageFolderProvider.kt b/core/src/main/java/tachiyomi/core/storage/AndroidStorageFolderProvider.kt
similarity index 94%
rename from core/src/main/java/tachiyomi/core/provider/AndroidStorageFolderProvider.kt
rename to core/src/main/java/tachiyomi/core/storage/AndroidStorageFolderProvider.kt
index fc2859868..a5d48a49d 100644
--- a/core/src/main/java/tachiyomi/core/provider/AndroidStorageFolderProvider.kt
+++ b/core/src/main/java/tachiyomi/core/storage/AndroidStorageFolderProvider.kt
@@ -1,4 +1,4 @@
-package tachiyomi.core.provider
+package tachiyomi.core.storage
import android.content.Context
import android.os.Environment
diff --git a/core/src/main/java/tachiyomi/core/provider/FolderProvider.kt b/core/src/main/java/tachiyomi/core/storage/FolderProvider.kt
similarity index 76%
rename from core/src/main/java/tachiyomi/core/provider/FolderProvider.kt
rename to core/src/main/java/tachiyomi/core/storage/FolderProvider.kt
index b4e124cee..decd1c378 100644
--- a/core/src/main/java/tachiyomi/core/provider/FolderProvider.kt
+++ b/core/src/main/java/tachiyomi/core/storage/FolderProvider.kt
@@ -1,4 +1,4 @@
-package tachiyomi.core.provider
+package tachiyomi.core.storage
import java.io.File
diff --git a/core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt b/core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt
new file mode 100644
index 000000000..c5c2bbbc8
--- /dev/null
+++ b/core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt
@@ -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
+}
diff --git a/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt b/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt
index b6cbc45e6..0aa7f9f59 100644
--- a/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt
+++ b/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt
@@ -37,7 +37,9 @@ import kotlin.math.min
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 {
URLConnection.guessContentTypeFromName(name)
} catch (e: Exception) {
@@ -243,7 +245,7 @@ object ImageUtil {
// Remove pre-existing split if exists (this split shouldn't exist under normal circumstances)
tmpDir.findFile(splitImageName)?.delete()
- val splitFile = tmpDir.createFile(splitImageName)
+ val splitFile = tmpDir.createFile(splitImageName)!!
val region = Rect(0, splitData.topOffset, splitData.splitWidth, splitData.bottomOffset)
diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts
index 2ce1a4d55..4df15c79a 100644
--- a/domain/build.gradle.kts
+++ b/domain/build.gradle.kts
@@ -19,6 +19,8 @@ dependencies {
implementation(platform(kotlinx.coroutines.bom))
implementation(kotlinx.bundles.coroutines)
+ implementation(libs.unifile)
+
api(libs.sqldelight.android.paging)
testImplementation(libs.bundles.test)
diff --git a/domain/src/main/java/tachiyomi/domain/storage/service/StorageManager.kt b/domain/src/main/java/tachiyomi/domain/storage/service/StorageManager.kt
new file mode 100644
index 000000000..a1fff4269
--- /dev/null
+++ b/domain/src/main/java/tachiyomi/domain/storage/service/StorageManager.kt
@@ -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"
diff --git a/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt b/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt
index c336825b8..8f7c3fcc6 100644
--- a/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt
+++ b/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt
@@ -1,7 +1,7 @@
package tachiyomi.domain.storage.service
import tachiyomi.core.preference.PreferenceStore
-import tachiyomi.core.provider.FolderProvider
+import tachiyomi.core.storage.FolderProvider
class StoragePreferences(
private val folderProvider: FolderProvider,
@@ -9,9 +9,4 @@ class StoragePreferences(
) {
fun baseStorageDirectory() = preferenceStore.getString("storage_dir", folderProvider.path())
-
- companion object {
- const val BACKUP_DIR = "backup"
- const val DOWNLOADS_DIR = "downloads"
- }
}
diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml
index 8d586064e..7ef758874 100644
--- a/gradle/androidx.versions.toml
+++ b/gradle/androidx.versions.toml
@@ -1,5 +1,5 @@
[versions]
-agp_version = "8.1.3"
+agp_version = "8.1.4"
lifecycle_version = "2.6.2"
paging_version = "3.2.1"
diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml
index bdafa0fe2..ce7b074bf 100644
--- a/gradle/compose.versions.toml
+++ b/gradle/compose.versions.toml
@@ -1,6 +1,6 @@
[versions]
compiler = "1.5.4"
-compose-bom = "2023.12.00-alpha01"
+compose-bom = "2023.12.00-alpha02"
accompanist = "0.33.2-alpha"
[libraries]
@@ -22,7 +22,6 @@ material-core = { module = "androidx.compose.material:material" }
glance = "androidx.glance:glance-appwidget:1.0.0"
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" }
lintchecks = { module = "com.slack.lint.compose:compose-lint-checks", version = "1.2.0" }
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 1f016060f..c556a1674 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -30,7 +30,7 @@ quickjs-android = "app.cash.quickjs:quickjs-android:0.9.2"
jsoup = "org.jsoup:jsoup:1.16.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"
sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" }
diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml
index 079136828..b9ec6edf0 100644
--- a/i18n/src/commonMain/resources/MR/base/strings.xml
+++ b/i18n/src/commonMain/resources/MR/base/strings.xml
@@ -839,7 +839,6 @@
Warning: large bulk downloads may lead to sources becoming slower and/or blocking Tachiyomi. Tap to learn more.
- Checking for new chapters
Updating library… (%s)
Large updates harm sources and may lead to slower updates and also increased battery usage. Tap to learn more.
New chapters found
diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt
index c68dd300f..b24baec43 100644
--- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt
+++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt
@@ -4,6 +4,7 @@ import androidx.compose.animation.core.animate
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
import androidx.compose.material3.pulltorefresh.PullToRefreshState
import androidx.compose.runtime.Composable
@@ -46,6 +47,7 @@ fun PullRefresh(
content: @Composable () -> Unit,
) {
val state = rememberPullToRefreshState(
+ initialRefreshing = refreshing,
extraVerticalOffset = indicatorPadding.calculateTopPadding(),
enabled = enabled,
)
@@ -84,12 +86,15 @@ fun PullRefresh(
modifier = Modifier
.align(Alignment.TopCenter)
.padding(contentPadding),
+ containerColor = MaterialTheme.colorScheme.surfaceVariant,
+ contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@Composable
private fun rememberPullToRefreshState(
+ initialRefreshing: Boolean,
extraVerticalOffset: Dp,
positionalThreshold: Dp = 64.dp,
enabled: () -> Boolean = { true },
@@ -108,7 +113,7 @@ private fun rememberPullToRefreshState(
),
) {
PullToRefreshStateImpl(
- initialRefreshing = false,
+ initialRefreshing = initialRefreshing,
extraVerticalOffset = extraVerticalOffsetPx,
positionalThreshold = positionalThresholdPx,
enabled = enabled,
@@ -133,18 +138,21 @@ private class PullToRefreshStateImpl(
) : PullToRefreshState {
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)
+ private val refreshingVerticalOffset: Float
+ get() = positionalThreshold + extraVerticalOffset
+
override fun startRefresh() {
isRefreshing = true
- verticalOffset = positionalThreshold + extraVerticalOffset
+ verticalOffset = refreshingVerticalOffset
}
suspend fun startRefreshAnimated() {
isRefreshing = true
- animateTo(positionalThreshold + extraVerticalOffset)
+ animateTo(refreshingVerticalOffset)
}
override fun endRefresh() {
@@ -196,7 +204,7 @@ private class PullToRefreshStateImpl(
val newOffset = (distancePulled + available.y).coerceAtLeast(0f)
val dragConsumed = newOffset - distancePulled
distancePulled = newOffset
- verticalOffset = calculateVerticalOffset() + (extraVerticalOffset * progress)
+ verticalOffset = calculateVerticalOffset() + (extraVerticalOffset * progress.coerceIn(0f, 1f))
dragConsumed
}
return Offset(0f, y)
diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt
index c3be92d2c..1305001f5 100644
--- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt
+++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt
@@ -11,6 +11,8 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.EpubFile
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import logcat.LogPriority
@@ -22,6 +24,9 @@ import tachiyomi.core.metadata.comicinfo.ComicInfo
import tachiyomi.core.metadata.comicinfo.copyFromComicInfo
import tachiyomi.core.metadata.comicinfo.getComicInfo
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.system.ImageUtil
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.Format
import tachiyomi.source.local.io.LocalSourceFileSystem
-import tachiyomi.source.local.metadata.fillChapterMetadata
-import tachiyomi.source.local.metadata.fillMangaMetadata
+import tachiyomi.source.local.metadata.fillMetadata
import uy.kohesive.injekt.injectLazy
import java.io.File
-import java.io.FileInputStream
import java.io.InputStream
import java.nio.charset.StandardCharsets
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 getSearchManga(page: Int, query: String, filters: FilterList): MangasPage {
- val baseDirsFiles = fileSystem.getFilesInBaseDirectories()
- val lastModifiedLimit by lazy {
- if (filters === LATEST_FILTERS) {
- System.currentTimeMillis() - LATEST_THRESHOLD
- } else {
- 0L
- }
+ override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage = withIOContext {
+ val lastModifiedLimit = if (filters === LATEST_FILTERS) {
+ System.currentTimeMillis() - LATEST_THRESHOLD
+ } else {
+ 0L
}
- var mangaDirs = baseDirsFiles
+
+ var mangaDirs = fileSystem.getFilesInBaseDirectory()
// 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 }
- .filter { // Filter by query or last modified
- if (lastModifiedLimit == 0L) {
- it.name.contains(query, ignoreCase = true)
+ .filter {
+ if (lastModifiedLimit == 0L && query.isBlank()) {
+ true
+ } else if (lastModifiedLimit == 0L) {
+ it.name.orEmpty().contains(query, ignoreCase = true)
} else {
it.lastModified() >= lastModifiedLimit
}
@@ -97,71 +100,53 @@ actual class LocalSource(
when (filter) {
is OrderBy.Popular -> {
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 {
- mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
+ mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name.orEmpty() })
}
}
is OrderBy.Latest -> {
mangaDirs = if (filter.state!!.ascending) {
- mangaDirs.sortedBy(File::lastModified)
+ mangaDirs.sortedBy(UniFile::lastModified)
} else {
- mangaDirs.sortedByDescending(File::lastModified)
+ mangaDirs.sortedByDescending(UniFile::lastModified)
}
}
-
else -> {
/* Do nothing */
}
}
}
- // Transform mangaDirs to list of SManga
- val mangas = mangaDirs.map { mangaDir ->
- SManga.create().apply {
- title = mangaDir.name
- url = mangaDir.name
+ val mangas = mangaDirs
+ .map { mangaDir ->
+ async {
+ SManga.create().apply {
+ title = mangaDir.name.orEmpty()
+ url = mangaDir.name.orEmpty()
- // Try to find the cover
- coverManager.find(mangaDir.name)
- ?.takeIf(File::exists)
- ?.let { thumbnail_url = it.absolutePath }
- }
- }
-
- // Fetch chapters of all the manga
- 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)
+ // Try to find the cover
+ coverManager.find(mangaDir.name.orEmpty())?.let {
+ thumbnail_url = it.filePath
+ }
}
}
-
- // Copy the cover from the first chapter found if not available
- if (manga.thumbnail_url == null) {
- updateCover(chapter, manga)
- }
}
- }
+ .awaitAll()
- return MangasPage(mangas.toList(), false)
+ MangasPage(mangas, false)
}
// Manga details related
override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext {
coverManager.find(manga.url)?.let {
- manga.thumbnail_url = it.absolutePath
+ manga.thumbnail_url = it.filePath
}
// Augment manga details based on metadata files
try {
- val mangaDir = fileSystem.getMangaDirectory(manga.url)
- val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url).toList()
+ val mangaDir by lazy { fileSystem.getMangaDirectory(manga.url) }
+ val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url)
val comicInfoFile = mangaDirFiles
.firstOrNull { it.name == COMIC_INFO_FILE }
@@ -174,13 +159,13 @@ actual class LocalSource(
// Top level ComicInfo.xml
comicInfoFile != null -> {
noXmlFile?.delete()
- setMangaDetailsFromComicInfoFile(comicInfoFile.inputStream(), manga)
+ setMangaDetailsFromComicInfoFile(comicInfoFile.openInputStream(), manga)
}
// Old custom JSON format
// TODO: remove support for this entirely after a while
legacyJsonDetailsFile != null -> {
- json.decodeFromStream(legacyJsonDetailsFile.inputStream()).run {
+ json.decodeFromStream(legacyJsonDetailsFile.openInputStream()).run {
title?.let { manga.title = it }
author?.let { manga.author = it }
artist?.let { manga.artist = it }
@@ -190,7 +175,7 @@ actual class LocalSource(
}
// Replace with ComicInfo.xml file
val comicInfo = manga.getComicInfo()
- UniFile.fromFile(mangaDir)
+ mangaDir
?.createFile(COMIC_INFO_FILE)
?.openOutputStream()
?.use {
@@ -206,14 +191,14 @@ actual class LocalSource(
.filter(Archive::isSupported)
.toList()
- val folderPath = mangaDir?.absolutePath
+ val folderPath = mangaDir?.filePath
val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)
if (copiedFile != null) {
setMangaDetailsFromComicInfoFile(copiedFile.inputStream(), manga)
} else {
// Avoid re-scanning
- File("$folderPath/.noxml").createNewFile()
+ mangaDir?.createFile(".noxml")
}
}
}
@@ -224,11 +209,11 @@ actual class LocalSource(
return@withIOContext manga
}
- private fun copyComicInfoFileFromArchive(chapterArchives: List, folderPath: String?): File? {
+ private fun copyComicInfoFileFromArchive(chapterArchives: List, folderPath: String?): File? {
for (chapter in chapterArchives) {
when (Format.valueOf(chapter)) {
is Format.Zip -> {
- ZipFile(chapter).use { zip: ZipFile ->
+ ZipFile(chapter.toTempFile(context)).use { zip: ZipFile ->
zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile ->
zip.getInputStream(comicInfoFile).buffered().use { stream ->
return copyComicInfoFile(stream, folderPath)
@@ -237,7 +222,7 @@ actual class LocalSource(
}
}
is Format.Rar -> {
- JunrarArchive(chapter).use { rar ->
+ JunrarArchive(chapter.toTempFile(context)).use { rar ->
rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile ->
rar.getInputStream(comicInfoFile).buffered().use { stream ->
return copyComicInfoFile(stream, folderPath)
@@ -268,8 +253,8 @@ actual class LocalSource(
}
// Chapters
- override suspend fun getChapterList(manga: SManga): List {
- return fileSystem.getFilesInMangaDirectory(manga.url)
+ override suspend fun getChapterList(manga: SManga): List = withIOContext {
+ val chapters = fileSystem.getFilesInMangaDirectory(manga.url)
// Only keep supported formats
.filter { it.isDirectory || Archive.isSupported(it) }
.map { chapterFile ->
@@ -279,7 +264,7 @@ actual class LocalSource(
chapterFile.name
} else {
chapterFile.nameWithoutExtension
- }
+ }.orEmpty()
date_upload = chapterFile.lastModified()
chapter_number = ChapterRecognition
.parseChapterNumber(manga.title, this.name, this.chapter_number.toDouble())
@@ -287,8 +272,8 @@ actual class LocalSource(
val format = Format.valueOf(chapterFile)
if (format is Format.Epub) {
- EpubFile(format.file).use { epub ->
- epub.fillChapterMetadata(this)
+ EpubFile(format.file.toTempFile(context)).use { epub ->
+ epub.fillMetadata(manga, this)
}
}
}
@@ -297,7 +282,15 @@ actual class LocalSource(
val c = c2.chapter_number.compareTo(c1.chapter_number)
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
@@ -308,9 +301,10 @@ actual class LocalSource(
fun getFormat(chapter: SChapter): Format {
try {
- return fileSystem.getBaseDirectories()
- .map { dir -> File(dir, chapter.url) }
- .find { it.exists() }
+ val (mangaDirName, chapterName) = chapter.url.split('/', limit = 2)
+ return fileSystem.getBaseDirectory()
+ ?.findFile(mangaDirName, true)
+ ?.findFile(chapterName, true)
?.let(Format.Companion::valueOf)
?: throw Exception(context.stringResource(MR.strings.chapter_not_found))
} 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 {
when (val format = getFormat(chapter)) {
is Format.Directory -> {
val entry = format.file.listFiles()
- ?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
- ?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
+ ?.sortedWith { f1, f2 ->
+ 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 -> {
- ZipFile(format.file).use { zip ->
+ ZipFile(format.file.toTempFile(context)).use { zip ->
val entry = zip.entries().toList()
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
@@ -340,7 +340,7 @@ actual class LocalSource(
}
}
is Format.Rar -> {
- JunrarArchive(format.file).use { archive ->
+ JunrarArchive(format.file.toTempFile(context)).use { archive ->
val entry = archive.fileHeaders
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
@@ -349,7 +349,7 @@ actual class LocalSource(
}
}
is Format.Epub -> {
- EpubFile(format.file).use { epub ->
+ EpubFile(format.file.toTempFile(context)).use { epub ->
val entry = epub.getImagesFromPages()
.firstOrNull()
?.let { epub.getEntry(it) }
diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt
index 7683756e3..0f5a4c343 100644
--- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt
+++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt
@@ -4,9 +4,9 @@ import android.content.Context
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.storage.DiskUtil
+import tachiyomi.core.storage.nameWithoutExtension
import tachiyomi.core.util.system.ImageUtil
import tachiyomi.source.local.io.LocalSourceFileSystem
-import java.io.File
import java.io.InputStream
private const val DEFAULT_COVER_NAME = "cover.jpg"
@@ -16,43 +16,35 @@ actual class LocalCoverManager(
private val fileSystem: LocalSourceFileSystem,
) {
- actual fun find(mangaUrl: String): File? {
+ actual fun find(mangaUrl: String): UniFile? {
return fileSystem.getFilesInMangaDirectory(mangaUrl)
// Get all file whose names start with "cover"
.filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
// Get the first actual image
- .firstOrNull {
- ImageUtil.isImage(it.name) { it.inputStream() }
- }
+ .firstOrNull { ImageUtil.isImage(it.name) { it.openInputStream() } }
}
actual fun update(
manga: SManga,
inputStream: InputStream,
- ): File? {
+ ): UniFile? {
val directory = fileSystem.getMangaDirectory(manga.url)
if (directory == null) {
inputStream.close()
return null
}
- var targetFile = find(manga.url)
- if (targetFile == null) {
- targetFile = File(directory.absolutePath, DEFAULT_COVER_NAME)
- targetFile.createNewFile()
- }
+ val targetFile = find(manga.url) ?: directory.createFile(DEFAULT_COVER_NAME)!!
- // It might not exist at this point
- targetFile.parentFile?.mkdirs()
inputStream.use { input ->
- targetFile.outputStream().use { output ->
+ targetFile.openOutputStream().use { 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
}
}
diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt
index f645f8b75..ad95b39ce 100644
--- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt
+++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt
@@ -1,40 +1,30 @@
package tachiyomi.source.local.io
-import android.content.Context
-import eu.kanade.tachiyomi.util.storage.DiskUtil
-import tachiyomi.core.i18n.stringResource
-import tachiyomi.i18n.MR
-import java.io.File
+import com.hippo.unifile.UniFile
+import tachiyomi.domain.storage.service.StorageManager
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 getBaseDirectories(): Sequence {
- return DiskUtil.getExternalStorages(context)
- .map { File(it.absolutePath, baseFolderLocation) }
- .asSequence()
+ actual fun getBaseDirectory(): UniFile? {
+ return storageManager.getLocalSourceDirectory()
}
- actual fun getFilesInBaseDirectories(): Sequence {
- return getBaseDirectories()
- // Get all the files inside all baseDir
- .flatMap { it.listFiles().orEmpty().toList() }
+ actual fun getFilesInBaseDirectory(): List {
+ return getBaseDirectory()?.listFiles().orEmpty().toList()
}
- actual fun getMangaDirectory(name: String): File? {
- return getFilesInBaseDirectories()
- // Get the first mangaDir or null
- .firstOrNull { it.isDirectory && it.name == name }
+ actual fun getMangaDirectory(name: String): UniFile? {
+ return getBaseDirectory()
+ ?.findFile(name, true)
+ ?.takeIf { it.isDirectory }
}
- actual fun getFilesInMangaDirectory(name: String): Sequence {
- return getFilesInBaseDirectories()
- // Filter out ones that are not related to the manga and is not a directory
- .filter { it.isDirectory && it.name == name }
- // Get all the files inside the filtered folders
- .flatMap { it.listFiles().orEmpty().toList() }
+ actual fun getFilesInMangaDirectory(name: String): List {
+ return getBaseDirectory()
+ ?.findFile(name, true)
+ ?.takeIf { it.isDirectory }
+ ?.listFiles().orEmpty().toList()
}
}
diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/metadata/EpubFile.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/metadata/EpubFile.kt
index d9ce323d5..6bade530b 100644
--- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/metadata/EpubFile.kt
+++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/metadata/EpubFile.kt
@@ -8,37 +8,25 @@ import java.text.SimpleDateFormat
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) {
- 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) {
+fun EpubFile.fillMetadata(manga: SManga, chapter: SChapter) {
val ref = getPackageHref()
val doc = getPackageDocument(ref)
val title = doc.getElementsByTag("dc:title").first()
val publisher = doc.getElementsByTag("dc:publisher").first()
val creator = doc.getElementsByTag("dc:creator").first()
+ val description = doc.getElementsByTag("dc:description").first()
var date = doc.getElementsByTag("dc:date").first()
if (date == null) {
date = doc.select("meta[property=dcterms:modified]").first()
}
- if (title != null) {
- chapter.name = title.text()
- }
+ creator?.text()?.let { manga.author = it }
+ description?.text()?.let { manga.description = it }
+
+ title?.text()?.let { chapter.name = it }
if (publisher != null) {
chapter.scanlator = publisher.text()
diff --git a/source-local/src/commonMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt b/source-local/src/commonMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt
index fd31299c2..037d9f1dc 100644
--- a/source-local/src/commonMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt
+++ b/source-local/src/commonMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt
@@ -1,12 +1,12 @@
package tachiyomi.source.local.image
+import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.SManga
-import java.io.File
import java.io.InputStream
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?
}
diff --git a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt
index b28ee60b5..a8f5a0740 100644
--- a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt
+++ b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt
@@ -1,12 +1,13 @@
package tachiyomi.source.local.io
-import java.io.File
+import com.hippo.unifile.UniFile
+import tachiyomi.core.storage.extension
object Archive {
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
- fun isSupported(file: File): Boolean = with(file) {
- return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
+ fun isSupported(file: UniFile): Boolean {
+ return file.extension in SUPPORTED_ARCHIVE_TYPES
}
}
diff --git a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Format.kt b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Format.kt
index 53406b5de..0f29ae8ab 100644
--- a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Format.kt
+++ b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Format.kt
@@ -1,18 +1,19 @@
package tachiyomi.source.local.io
-import java.io.File
+import com.hippo.unifile.UniFile
+import tachiyomi.core.storage.extension
sealed interface Format {
- data class Directory(val file: File) : Format
- data class Zip(val file: File) : Format
- data class Rar(val file: File) : Format
- data class Epub(val file: File) : Format
+ data class Directory(val file: UniFile) : Format
+ data class Zip(val file: UniFile) : Format
+ data class Rar(val file: UniFile) : Format
+ data class Epub(val file: UniFile) : Format
class UnknownFormatException : Exception()
companion object {
- fun valueOf(file: File) = with(file) {
+ fun valueOf(file: UniFile) = with(file) {
when {
isDirectory -> Directory(this)
extension.equals("zip", true) || extension.equals("cbz", true) -> Zip(this)
diff --git a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt
index 0440df26e..5aa74d851 100644
--- a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt
+++ b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt
@@ -1,14 +1,14 @@
package tachiyomi.source.local.io
-import java.io.File
+import com.hippo.unifile.UniFile
expect class LocalSourceFileSystem {
- fun getBaseDirectories(): Sequence
+ fun getBaseDirectory(): UniFile?
- fun getFilesInBaseDirectories(): Sequence
+ fun getFilesInBaseDirectory(): List
- fun getMangaDirectory(name: String): File?
+ fun getMangaDirectory(name: String): UniFile?
- fun getFilesInMangaDirectory(name: String): Sequence
+ fun getFilesInMangaDirectory(name: String): List
}