mirror of
https://github.com/mihonapp/mihon.git
synced 2025-10-09 12:59:34 +02:00
chore: merge upstream changes.
Signed-off-by: KaiserBh <kaiserbh@proton.me>
This commit is contained in:
@@ -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)
|
||||
|
||||
|
@@ -7,9 +7,6 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
|
||||
<!-- Storage -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<!-- For background jobs -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
@@ -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">
|
||||
|
@@ -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<BackupPreferences>()
|
||||
val storagePreferences = Injekt.get<StoragePreferences>()
|
||||
|
||||
PermissionRequestHelper.requestStoragePermission()
|
||||
|
||||
val syncPreferences = remember { Injekt.get<SyncPreferences>() }
|
||||
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<DownloadCache>().invalidateCache()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
Injekt.importModule(AppModule(this))
|
||||
Injekt.importModule(PreferenceModule(this))
|
||||
Injekt.importModule(AppModule(this))
|
||||
Injekt.importModule(DomainModule())
|
||||
|
||||
setupAcra()
|
||||
|
@@ -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<StoragePreferences>()
|
||||
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<StorageManager>()
|
||||
return storageManager.getAutomaticBackupsDirectory()?.uri
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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<Long, SourceDirectory> = 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<String, MangaDirectory> = 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<String> = mutableSetOf(),
|
||||
)
|
||||
|
||||
private object UniFileAsStringSerializer : KSerializer<UniFile> {
|
||||
private object UniFileAsStringSerializer : KSerializer<UniFile?> {
|
||||
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<Application>(), Uri.parse(decoder.decodeString()))
|
||||
|
||||
override fun deserialize(decoder: Decoder): UniFile? {
|
||||
return if (decoder.decodeNotNullMark()) {
|
||||
UniFile.fromUri(Injekt.get<Application>(), Uri.parse(decoder.decodeString()))
|
||||
} else {
|
||||
decoder.decodeNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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"
|
||||
}
|
||||
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
||||
|
@@ -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())
|
||||
}
|
||||
|
@@ -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<Manga>, 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(
|
||||
|
@@ -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
|
||||
|
@@ -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 {
|
||||
|
@@ -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<AndroidStorageFolderProvider>(),
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -66,6 +66,7 @@ object HomeScreen : Screen() {
|
||||
private val showBottomNavEvent = Channel<Boolean>()
|
||||
|
||||
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) {
|
||||
|
@@ -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 {
|
||||
|
@@ -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)
|
||||
|
@@ -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<ReaderPage> {
|
||||
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
|
||||
|
@@ -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<ReaderPage> {
|
||||
val loader = ZipPageLoader(File(chapterPath.filePath!!)).also { zipPageLoader = it }
|
||||
val loader = ZipPageLoader(chapterPath.toTempFile(context)).also { zipPageLoader = it }
|
||||
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.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) {
|
||||
|
@@ -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
|
||||
*/
|
||||
fun Context.getUriSize(uri: Uri): Long? {
|
||||
return UniFile.fromUri(this, uri).length().takeIf { it >= 0 }
|
||||
return UniFile.fromUri(this, uri)?.length()?.takeIf { it >= 0 }
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user