mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 10:44:42 +00:00
feat: The actual unified storage
This commit is contained in:
parent
6887d779ef
commit
64d6879893
25 changed files with 256 additions and 380 deletions
|
@ -7,11 +7,6 @@
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
|
||||||
<!-- Storage -->
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
|
||||||
tools:ignore="ScopedStorage" />
|
|
||||||
|
|
||||||
<!-- For background jobs -->
|
<!-- For background jobs -->
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
@ -37,12 +32,10 @@
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:preserveLegacyExternalStorage="true"
|
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
android:enableOnBackInvokedCallback="true"
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:requestLegacyExternalStorage="true"
|
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
|
|
@ -40,6 +40,8 @@ class StorageManager(
|
||||||
parent.createDirectory(DOWNLOADS_PATH).also {
|
parent.createDirectory(DOWNLOADS_PATH).also {
|
||||||
DiskUtil.createNoMediaFile(it, context)
|
DiskUtil.createNoMediaFile(it, context)
|
||||||
}
|
}
|
||||||
|
parent.createDirectory(COVERS_PATH)
|
||||||
|
parent.createDirectory(PAGES_PATH)
|
||||||
}
|
}
|
||||||
_changes.send(Unit)
|
_changes.send(Unit)
|
||||||
}
|
}
|
||||||
|
@ -66,9 +68,19 @@ class StorageManager(
|
||||||
fun getLocalSourceDirectory(): UniFile? {
|
fun getLocalSourceDirectory(): UniFile? {
|
||||||
return baseDir?.createDirectory(LOCAL_SOURCE_PATH)
|
return baseDir?.createDirectory(LOCAL_SOURCE_PATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getCoversDirectory(): UniFile? {
|
||||||
|
return baseDir?.createDirectory(COVERS_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPagesDirectory(): UniFile? {
|
||||||
|
return baseDir?.createDirectory(PAGES_PATH)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val BACKUPS_PATH = "autobackup"
|
private const val BACKUPS_PATH = "autobackup"
|
||||||
private const val AUTOMATIC_BACKUPS_PATH = "autobackup"
|
private const val AUTOMATIC_BACKUPS_PATH = "autobackup"
|
||||||
private const val DOWNLOADS_PATH = "downloads"
|
private const val DOWNLOADS_PATH = "downloads"
|
||||||
private const val LOCAL_SOURCE_PATH = "local"
|
private const val LOCAL_SOURCE_PATH = "local"
|
||||||
|
private const val COVERS_PATH = "covers"
|
||||||
|
private const val PAGES_PATH = "pages"
|
||||||
|
|
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.backup
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
|
import dev.yokai.domain.storage.StorageManager
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS_MASK
|
||||||
|
@ -54,6 +55,7 @@ import okio.sink
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
class BackupCreator(val context: Context) {
|
class BackupCreator(val context: Context) {
|
||||||
|
@ -64,6 +66,7 @@ class BackupCreator(val context: Context) {
|
||||||
private val sourceManager: SourceManager = Injekt.get()
|
private val sourceManager: SourceManager = Injekt.get()
|
||||||
private val preferences: PreferencesHelper = Injekt.get()
|
private val preferences: PreferencesHelper = Injekt.get()
|
||||||
private val customMangaManager: CustomMangaManager = Injekt.get()
|
private val customMangaManager: CustomMangaManager = Injekt.get()
|
||||||
|
internal val storageManager: StorageManager by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create backup Json file from database
|
* Create backup Json file from database
|
||||||
|
@ -98,8 +101,7 @@ class BackupCreator(val context: Context) {
|
||||||
file = (
|
file = (
|
||||||
if (isAutoBackup) {
|
if (isAutoBackup) {
|
||||||
// Get dir of file and create
|
// Get dir of file and create
|
||||||
// TODO: Unified Storage
|
val dir = storageManager.getAutomaticBackupsDirectory()!!
|
||||||
val dir = UniFile.fromUri(context, uri)!!.createDirectory("automatic")!!
|
|
||||||
|
|
||||||
// Delete older backups
|
// Delete older backups
|
||||||
val numberOfBackups = preferences.numberOfBackups().get()
|
val numberOfBackups = preferences.numberOfBackups().get()
|
||||||
|
|
|
@ -13,6 +13,8 @@ import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import androidx.work.workDataOf
|
import androidx.work.workDataOf
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
|
import dev.yokai.domain.storage.StorageManager
|
||||||
|
import dev.yokai.domain.storage.StoragePreferences
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.util.system.localeContext
|
import eu.kanade.tachiyomi.util.system.localeContext
|
||||||
|
@ -20,16 +22,16 @@ import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :
|
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :
|
||||||
Worker(context, workerParams) {
|
Worker(context, workerParams) {
|
||||||
|
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val storageManager: StorageManager by injectLazy()
|
||||||
val notifier = BackupNotifier(context.localeContext)
|
val notifier = BackupNotifier(context.localeContext)
|
||||||
val uri = inputData.getString(LOCATION_URI_KEY)?.let { Uri.parse(it) }
|
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri() ?: storageManager.getAutomaticBackupsDirectory()?.uri!!
|
||||||
?: preferences.backupsDirectory().get().toUri()
|
|
||||||
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupConst.BACKUP_ALL)
|
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupConst.BACKUP_ALL)
|
||||||
val isAutoBackup = inputData.getBoolean(IS_AUTO_BACKUP_KEY, true)
|
val isAutoBackup = inputData.getBoolean(IS_AUTO_BACKUP_KEY, true)
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
package eu.kanade.tachiyomi.data.download
|
package eu.kanade.tachiyomi.data.download
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.net.toUri
|
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
|
import dev.yokai.domain.storage.StorageManager
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.drop
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
|
@ -35,7 +33,7 @@ class DownloadCache(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val provider: DownloadProvider,
|
private val provider: DownloadProvider,
|
||||||
private val sourceManager: SourceManager,
|
private val sourceManager: SourceManager,
|
||||||
private val preferences: PreferencesHelper = Injekt.get(),
|
private val storageManager: StorageManager = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,21 +52,11 @@ class DownloadCache(
|
||||||
val scope = CoroutineScope(Job() + Dispatchers.IO)
|
val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
preferences.downloadsDirectory().changes()
|
storageManager.changes
|
||||||
.drop(1)
|
.onEach { forceRenewCache() } // invalidate cache
|
||||||
.onEach { lastRenew = 0L } // invalidate cache
|
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the downloads directory from the user's preferences.
|
|
||||||
*/
|
|
||||||
private fun getDirectoryFromPreference(): UniFile {
|
|
||||||
// TODO: Unified Storage
|
|
||||||
val dir = preferences.downloadsDirectory().get()
|
|
||||||
return UniFile.fromUri(context, dir.toUri())!!
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the chapter is downloaded.
|
* Returns true if the chapter is downloaded.
|
||||||
*
|
*
|
||||||
|
@ -138,7 +126,7 @@ class DownloadCache(
|
||||||
private fun renew() {
|
private fun renew() {
|
||||||
val onlineSources = sourceManager.getOnlineSources()
|
val onlineSources = sourceManager.getOnlineSources()
|
||||||
|
|
||||||
val sourceDirs = getDirectoryFromPreference().listFiles().orEmpty()
|
val sourceDirs = storageManager.getDownloadsDirectory()!!.listFiles().orEmpty()
|
||||||
.associate { it.name to SourceDirectory(it) }.mapNotNullKeys { entry ->
|
.associate { it.name to SourceDirectory(it) }.mapNotNullKeys { entry ->
|
||||||
onlineSources.find { provider.getSourceDirName(it).equals(entry.key, ignoreCase = true) }?.id
|
onlineSources.find { provider.getSourceDirName(it).equals(entry.key, ignoreCase = true) }?.id
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.download
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
|
import dev.yokai.domain.storage.StorageManager
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
@ -31,6 +32,7 @@ class DownloadProvider(private val context: Context) {
|
||||||
* Preferences helper.
|
* Preferences helper.
|
||||||
*/
|
*/
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
private val storageManager: StorageManager by injectLazy()
|
||||||
|
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
|
@ -38,15 +40,11 @@ class DownloadProvider(private val context: Context) {
|
||||||
* The root directory for downloads.
|
* The root directory for downloads.
|
||||||
*/
|
*/
|
||||||
// TODO: Unified Storage
|
// TODO: Unified Storage
|
||||||
private var downloadsDir = preferences.downloadsDirectory().get().let {
|
private var downloadsDir = storageManager.getDownloadsDirectory()
|
||||||
val dir = UniFile.fromUri(context, it.toUri())
|
|
||||||
DiskUtil.createNoMediaFile(dir, context)
|
|
||||||
dir!!
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
preferences.downloadsDirectory().changes().drop(1).onEach {
|
storageManager.changes.onEach {
|
||||||
downloadsDir = UniFile.fromUri(context, it.toUri())!!
|
downloadsDir = storageManager.getDownloadsDirectory()
|
||||||
}.launchIn(scope)
|
}.launchIn(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +56,7 @@ class DownloadProvider(private val context: Context) {
|
||||||
*/
|
*/
|
||||||
internal fun getMangaDir(manga: Manga, source: Source): UniFile {
|
internal fun getMangaDir(manga: Manga, source: Source): UniFile {
|
||||||
try {
|
try {
|
||||||
return downloadsDir.createDirectory(getSourceDirName(source))!!
|
return downloadsDir!!.createDirectory(getSourceDirName(source))!!
|
||||||
.createDirectory(getMangaDirName(manga))!!
|
.createDirectory(getMangaDirName(manga))!!
|
||||||
} catch (e: NullPointerException) {
|
} catch (e: NullPointerException) {
|
||||||
throw Exception(context.getString(R.string.invalid_download_location))
|
throw Exception(context.getString(R.string.invalid_download_location))
|
||||||
|
@ -71,7 +69,7 @@ class DownloadProvider(private val context: Context) {
|
||||||
* @param source the source to query.
|
* @param source the source to query.
|
||||||
*/
|
*/
|
||||||
fun findSourceDir(source: Source): UniFile? {
|
fun findSourceDir(source: Source): UniFile? {
|
||||||
return downloadsDir.findFile(getSourceDirName(source), true)
|
return downloadsDir!!.findFile(getSourceDirName(source), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,20 +1,16 @@
|
||||||
package eu.kanade.tachiyomi.data.preference
|
package eu.kanade.tachiyomi.data.preference
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Environment
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.material.color.DynamicColors
|
import com.google.android.material.color.DynamicColors
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.core.preference.Preference
|
import eu.kanade.tachiyomi.core.preference.Preference
|
||||||
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||||
import eu.kanade.tachiyomi.core.preference.getEnum
|
import eu.kanade.tachiyomi.core.preference.getEnum
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
|
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
|
||||||
import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder
|
import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
import eu.kanade.tachiyomi.ui.library.LibraryItem
|
||||||
import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet
|
import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet
|
||||||
|
@ -23,14 +19,12 @@ import eu.kanade.tachiyomi.ui.reader.settings.PageLayout
|
||||||
import eu.kanade.tachiyomi.ui.reader.settings.ReaderBottomButton
|
import eu.kanade.tachiyomi.ui.reader.settings.ReaderBottomButton
|
||||||
import eu.kanade.tachiyomi.ui.reader.settings.ReadingModeType
|
import eu.kanade.tachiyomi.ui.reader.settings.ReadingModeType
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
||||||
import eu.kanade.tachiyomi.ui.recents.RecentMangaAdapter
|
|
||||||
import eu.kanade.tachiyomi.ui.recents.RecentsPresenter
|
import eu.kanade.tachiyomi.ui.recents.RecentsPresenter
|
||||||
import eu.kanade.tachiyomi.util.system.Themes
|
import eu.kanade.tachiyomi.util.system.Themes
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import java.io.File
|
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -66,22 +60,6 @@ class PreferencesHelper(val context: Context, val preferenceStore: PreferenceSto
|
||||||
|
|
||||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
private val defaultDownloadsDir = Uri.fromFile(
|
|
||||||
File(
|
|
||||||
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
|
||||||
context.getString(R.string.app_normalized_name),
|
|
||||||
"downloads",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
private val defaultBackupDir = Uri.fromFile(
|
|
||||||
File(
|
|
||||||
Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
|
||||||
context.getString(R.string.app_normalized_name),
|
|
||||||
"backup",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
fun getInt(key: String, default: Int) = preferenceStore.getInt(key, default)
|
fun getInt(key: String, default: Int) = preferenceStore.getInt(key, default)
|
||||||
fun getStringPref(key: String, default: String = "") = preferenceStore.getString(key, default)
|
fun getStringPref(key: String, default: String = "") = preferenceStore.getString(key, default)
|
||||||
fun getStringSet(key: String, default: Set<String>) = preferenceStore.getStringSet(key, default)
|
fun getStringSet(key: String, default: Set<String>) = preferenceStore.getStringSet(key, default)
|
||||||
|
@ -215,8 +193,6 @@ class PreferencesHelper(val context: Context, val preferenceStore: PreferenceSto
|
||||||
|
|
||||||
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", "POINT_10")
|
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", "POINT_10")
|
||||||
|
|
||||||
fun backupsDirectory() = preferenceStore.getString(Keys.backupDirectory, defaultBackupDir.toString())
|
|
||||||
|
|
||||||
fun dateFormat(format: String = preferenceStore.getString(Keys.dateFormat, "").get()): DateFormat = when (format) {
|
fun dateFormat(format: String = preferenceStore.getString(Keys.dateFormat, "").get()): DateFormat = when (format) {
|
||||||
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
||||||
else -> SimpleDateFormat(format, Locale.getDefault())
|
else -> SimpleDateFormat(format, Locale.getDefault())
|
||||||
|
@ -224,8 +200,6 @@ class PreferencesHelper(val context: Context, val preferenceStore: PreferenceSto
|
||||||
|
|
||||||
fun appLanguage() = preferenceStore.getString("app_language", "")
|
fun appLanguage() = preferenceStore.getString("app_language", "")
|
||||||
|
|
||||||
fun downloadsDirectory() = preferenceStore.getString(Keys.downloadsDirectory, defaultDownloadsDir.toString())
|
|
||||||
|
|
||||||
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
||||||
|
|
||||||
fun folderPerManga() = preferenceStore.getBoolean("create_folder_per_manga", false)
|
fun folderPerManga() = preferenceStore.getBoolean("create_folder_per_manga", false)
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package eu.kanade.tachiyomi.source
|
package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.core.net.toFile
|
||||||
import com.github.junrar.Archive
|
import com.github.junrar.Archive
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import dev.yokai.domain.storage.StorageManager
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
@ -10,9 +13,11 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
||||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
|
||||||
import eu.kanade.tachiyomi.util.storage.EpubFile
|
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.extension
|
||||||
|
import eu.kanade.tachiyomi.util.system.nameWithoutExtension
|
||||||
|
import eu.kanade.tachiyomi.util.system.writeText
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
|
@ -20,7 +25,6 @@ import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
@ -35,16 +39,16 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
||||||
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
|
||||||
private val langMap = hashMapOf<String, String>()
|
private val langMap = hashMapOf<String, String>()
|
||||||
|
|
||||||
fun getMangaLang(manga: SManga, context: Context): String {
|
fun getMangaLang(manga: SManga): String {
|
||||||
return langMap.getOrPut(manga.url) {
|
return langMap.getOrPut(manga.url) {
|
||||||
val localDetails = getBaseDirectories(context)
|
val localDetails = getBaseDirectories()
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
.mapNotNull { it.findFile(manga.url)?.listFiles()?.toList() }
|
||||||
.flatten()
|
.flatten()
|
||||||
.firstOrNull { it.extension.equals("json", ignoreCase = true) }
|
.firstOrNull { it.extension.equals("json", ignoreCase = true) }
|
||||||
|
|
||||||
return if (localDetails != null) {
|
return if (localDetails != null) {
|
||||||
val obj = Json.decodeFromStream<MangaJson>(localDetails.inputStream())
|
val obj = Json.decodeFromStream<MangaJson>(localDetails.openInputStream())
|
||||||
obj.lang ?: "other"
|
obj.lang ?: "other"
|
||||||
} else {
|
} else {
|
||||||
"other"
|
"other"
|
||||||
|
@ -52,49 +56,39 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
|
fun updateCover(manga: SManga, input: InputStream): UniFile? {
|
||||||
val dir = getBaseDirectories(context).firstOrNull()
|
val dir = getBaseDirectories().firstOrNull()
|
||||||
if (dir == null) {
|
if (dir == null) {
|
||||||
input.close()
|
input.close()
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
var cover = getCoverFile(File("${dir.absolutePath}/${manga.url}"))
|
var cover = getCoverFile(dir.findFile(manga.url))
|
||||||
if (cover == null) {
|
if (cover == null) {
|
||||||
cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
|
cover = dir.findFile(manga.url)?.findFile(COVER_NAME)!!
|
||||||
}
|
}
|
||||||
// It might not exist if using the external SD card
|
// It might not exist if using the external SD card
|
||||||
cover.parentFile?.mkdirs()
|
cover.parentFile?.parentFile?.createDirectory(cover.parentFile?.name)
|
||||||
input.use {
|
input.use {
|
||||||
cover.outputStream().use {
|
cover.openOutputStream().use {
|
||||||
input.copyTo(it)
|
input.copyTo(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
manga.thumbnail_url = cover.absolutePath
|
manga.thumbnail_url = cover.filePath
|
||||||
return cover
|
return cover
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns valid cover file inside [parent] directory.
|
* Returns valid cover file inside [parent] directory.
|
||||||
*/
|
*/
|
||||||
private fun getCoverFile(parent: File): File? {
|
private fun getCoverFile(parent: UniFile?): UniFile? {
|
||||||
return parent.listFiles()?.find { it.nameWithoutExtension == "cover" }?.takeIf {
|
return parent?.listFiles()?.find { it.nameWithoutExtension == "cover" }?.takeIf {
|
||||||
it.isFile && ImageUtil.isImage(it.name) { it.inputStream() }
|
it.isFile && ImageUtil.isImage(it.name.orEmpty()) { it.openInputStream() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getBaseDirectories(context: Context): List<File> {
|
private fun getBaseDirectories(): List<UniFile> {
|
||||||
val library = context.getString(R.string.app_short_name) + File.separator + "local"
|
val storageManager: StorageManager by injectLazy()
|
||||||
val normalized = context.getString(R.string.app_normalized_name) + File.separator + "local"
|
return listOf(storageManager.getLocalSourceDirectory()!!)
|
||||||
val j2k = "TachiyomiJ2K" + File.separator + "local"
|
|
||||||
val tachi = "Tachiyomi" + File.separator + "local"
|
|
||||||
return DiskUtil.getExternalStorages(context).map {
|
|
||||||
listOf(
|
|
||||||
File(it.absolutePath, library),
|
|
||||||
File(it.absolutePath, normalized),
|
|
||||||
File(it.absolutePath, j2k),
|
|
||||||
File(it.absolutePath, tachi),
|
|
||||||
)
|
|
||||||
}.flatten()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,7 +108,7 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
||||||
query: String,
|
query: String,
|
||||||
filters: FilterList,
|
filters: FilterList,
|
||||||
): MangasPage {
|
): MangasPage {
|
||||||
val baseDirs = getBaseDirectories(context)
|
val baseDirs = getBaseDirectories()
|
||||||
|
|
||||||
val time =
|
val time =
|
||||||
if (filters === latestFilters) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
if (filters === latestFilters) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
|
||||||
|
@ -123,38 +117,38 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
||||||
.mapNotNull { it.listFiles()?.toList() }
|
.mapNotNull { it.listFiles()?.toList() }
|
||||||
.flatten()
|
.flatten()
|
||||||
.filter { it.isDirectory }
|
.filter { it.isDirectory }
|
||||||
.filterNot { it.name.startsWith('.') }
|
.filterNot { it.name.orEmpty().startsWith('.') }
|
||||||
.filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
|
.filter { if (time == 0L) it.name.orEmpty().contains(query, ignoreCase = true) else it.lastModified() >= time }
|
||||||
.distinctBy { it.name }
|
.distinctBy { it.name }
|
||||||
|
|
||||||
val state = ((if (filters.isEmpty()) popularFilters else filters)[0] as OrderBy).state
|
val state = ((if (filters.isEmpty()) popularFilters else filters)[0] as OrderBy).state
|
||||||
when (state?.index) {
|
when (state?.index) {
|
||||||
0 -> {
|
0 -> {
|
||||||
mangaDirs = if (state.ascending) {
|
mangaDirs = if (state.ascending) {
|
||||||
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name.orEmpty() })
|
||||||
} else {
|
} else {
|
||||||
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
|
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name.orEmpty()})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1 -> {
|
1 -> {
|
||||||
mangaDirs = if (state.ascending) {
|
mangaDirs = if (state.ascending) {
|
||||||
mangaDirs.sortedBy(File::lastModified)
|
mangaDirs.sortedBy(UniFile::lastModified)
|
||||||
} else {
|
} else {
|
||||||
mangaDirs.sortedByDescending(File::lastModified)
|
mangaDirs.sortedByDescending(UniFile::lastModified)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val mangas = mangaDirs.map { mangaDir ->
|
val mangas = mangaDirs.map { mangaDir ->
|
||||||
SManga.create().apply {
|
SManga.create().apply {
|
||||||
title = mangaDir.name
|
title = mangaDir.name.orEmpty()
|
||||||
url = mangaDir.name
|
url = mangaDir.name.orEmpty()
|
||||||
|
|
||||||
// Try to find the cover
|
// Try to find the cover
|
||||||
for (dir in baseDirs) {
|
for (dir in baseDirs) {
|
||||||
val cover = getCoverFile(File("${dir.absolutePath}/$url"))
|
val cover = getCoverFile(mangaDir.findFile(url))
|
||||||
if (cover != null && cover.exists()) {
|
if (cover != null && cover.exists()) {
|
||||||
thumbnail_url = cover.absolutePath
|
thumbnail_url = cover.filePath
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -166,7 +160,7 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
||||||
val chapter = chapters.last()
|
val chapter = chapters.last()
|
||||||
val format = getFormat(chapter)
|
val format = getFormat(chapter)
|
||||||
if (format is Format.Epub) {
|
if (format is Format.Epub) {
|
||||||
EpubFile(format.file).use { epub ->
|
EpubFile(format.file.uri.toFile()).use { epub ->
|
||||||
epub.fillMangaMetadata(manga)
|
epub.fillMangaMetadata(manga)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -175,7 +169,7 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
||||||
if (thumbnail_url == null) {
|
if (thumbnail_url == null) {
|
||||||
try {
|
try {
|
||||||
val dest = updateCover(chapter, manga)
|
val dest = updateCover(chapter, manga)
|
||||||
thumbnail_url = dest?.absolutePath
|
thumbnail_url = dest?.filePath
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
}
|
}
|
||||||
|
@ -191,14 +185,14 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
||||||
override suspend fun getLatestUpdates(page: Int) = getSearchManga(page, "", latestFilters)
|
override suspend fun getLatestUpdates(page: Int) = getSearchManga(page, "", latestFilters)
|
||||||
|
|
||||||
override suspend fun getMangaDetails(manga: SManga): SManga {
|
override suspend fun getMangaDetails(manga: SManga): SManga {
|
||||||
val localDetails = getBaseDirectories(context)
|
val localDetails = getBaseDirectories()
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
.mapNotNull { it.findFile(manga.url)?.listFiles()?.toList() }
|
||||||
.flatten()
|
.flatten()
|
||||||
.firstOrNull { it.extension.equals("json", ignoreCase = true) }
|
.firstOrNull { it.extension.equals("json", ignoreCase = true) }
|
||||||
|
|
||||||
return if (localDetails != null) {
|
return if (localDetails != null) {
|
||||||
val obj = json.decodeFromStream<MangaJson>(localDetails.inputStream())
|
val obj = json.decodeFromStream<MangaJson>(localDetails.openInputStream())
|
||||||
|
|
||||||
obj.lang?.let { langMap[manga.url] = it }
|
obj.lang?.let { langMap[manga.url] = it }
|
||||||
SManga.create().apply {
|
SManga.create().apply {
|
||||||
|
@ -215,13 +209,13 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateMangaInfo(manga: SManga, lang: String?) {
|
fun updateMangaInfo(manga: SManga, lang: String?) {
|
||||||
val directory = getBaseDirectories(context).map { File(it, manga.url) }.find {
|
val directory = getBaseDirectories().map { it.findFile(manga.url) }.find {
|
||||||
it.exists()
|
it?.exists() == true
|
||||||
} ?: return
|
} ?: return
|
||||||
lang?.let { langMap[manga.url] = it }
|
lang?.let { langMap[manga.url] = it }
|
||||||
val json = Json { prettyPrint = true }
|
val json = Json { prettyPrint = true }
|
||||||
val existingFileName = directory.listFiles()?.find { it.extension == "json" }?.name
|
val existingFileName = directory.listFiles()?.find { it.name.orEmpty().endsWith("json", true) }?.name
|
||||||
val file = File(directory, existingFileName ?: "info.json")
|
val file = directory.findFile(existingFileName ?: "info.json")!!
|
||||||
file.writeText(json.encodeToString(manga.toJson(lang)))
|
file.writeText(json.encodeToString(manga.toJson(lang)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,24 +250,24 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getChapterList(manga: SManga): List<SChapter> {
|
override suspend fun getChapterList(manga: SManga): List<SChapter> {
|
||||||
val chapters = getBaseDirectories(context)
|
val chapters = getBaseDirectories()
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
|
.mapNotNull { it.findFile(manga.url)?.listFiles()?.toList() }
|
||||||
.flatten()
|
.flatten()
|
||||||
.filter { it.isDirectory || isSupportedFile(it.extension) }
|
.filter { it.isDirectory || isSupportedFile(it.extension.orEmpty()) }
|
||||||
.map { chapterFile ->
|
.map { chapterFile ->
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
url = "${manga.url}/${chapterFile.name}"
|
url = "${manga.url}/${chapterFile.name}"
|
||||||
name = if (chapterFile.isDirectory) {
|
name = if (chapterFile.isDirectory) {
|
||||||
chapterFile.name
|
chapterFile.name.orEmpty()
|
||||||
} else {
|
} else {
|
||||||
chapterFile.nameWithoutExtension
|
chapterFile.nameWithoutExtension.orEmpty()
|
||||||
}
|
}
|
||||||
date_upload = chapterFile.lastModified()
|
date_upload = chapterFile.lastModified()
|
||||||
|
|
||||||
val format = getFormat(chapterFile)
|
val format = getFormat(chapterFile)
|
||||||
if (format is Format.Epub) {
|
if (format is Format.Epub) {
|
||||||
EpubFile(format.file).use { epub ->
|
EpubFile(format.file.uri.toFile()).use { epub ->
|
||||||
epub.fillChapterMetadata(this)
|
epub.fillChapterMetadata(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -297,18 +291,18 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFormat(chapter: SChapter): Format {
|
fun getFormat(chapter: SChapter): Format {
|
||||||
val baseDirs = getBaseDirectories(context)
|
val baseDirs = getBaseDirectories()
|
||||||
|
|
||||||
for (dir in baseDirs) {
|
for (dir in baseDirs) {
|
||||||
val chapFile = File(dir, chapter.url)
|
val chapFile = dir.findFile(chapter.url)
|
||||||
if (!chapFile.exists()) continue
|
if (chapFile == null || !chapFile.exists()) continue
|
||||||
|
|
||||||
return getFormat(chapFile)
|
return getFormat(chapFile)
|
||||||
}
|
}
|
||||||
throw Exception(context.getString(R.string.chapter_not_found))
|
throw Exception(context.getString(R.string.chapter_not_found))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFormat(file: File) = with(file) {
|
private fun getFormat(file: UniFile) = with(file) {
|
||||||
when {
|
when {
|
||||||
isDirectory -> Format.Directory(this)
|
isDirectory -> Format.Directory(this)
|
||||||
extension.equals("zip", true) || extension.equals("cbz", true) -> Format.Zip(this)
|
extension.equals("zip", true) || extension.equals("cbz", true) -> Format.Zip(this)
|
||||||
|
@ -318,41 +312,41 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateCover(chapter: SChapter, manga: SManga): File? {
|
private fun updateCover(chapter: SChapter, manga: SManga): UniFile? {
|
||||||
return try {
|
return try {
|
||||||
when (val format = getFormat(chapter)) {
|
when (val format = getFormat(chapter)) {
|
||||||
is Format.Directory -> {
|
is Format.Directory -> {
|
||||||
val entry = format.file.listFiles()
|
val entry = format.file.listFiles()
|
||||||
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
?.sortedWith { f1, f2 -> f1.name.orEmpty().compareToCaseInsensitiveNaturalOrder(f2.name.orEmpty()) }
|
||||||
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
|
?.find { !it.isDirectory && ImageUtil.isImage(it.name.orEmpty()) { FileInputStream(it.uri.toFile()) } }
|
||||||
|
|
||||||
entry?.let { updateCover(context, manga, it.inputStream()) }
|
entry?.let { updateCover(manga, it.openInputStream()) }
|
||||||
}
|
}
|
||||||
is Format.Zip -> {
|
is Format.Zip -> {
|
||||||
ZipFile(format.file).use { zip ->
|
ZipFile(format.file.uri.toFile()).use { zip ->
|
||||||
val entry = zip.entries().toList()
|
val entry = zip.entries().toList()
|
||||||
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
|
||||||
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
|
||||||
|
|
||||||
entry?.let { updateCover(context, manga, zip.getInputStream(it)) }
|
entry?.let { updateCover(manga, zip.getInputStream(it)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Format.Rar -> {
|
is Format.Rar -> {
|
||||||
Archive(format.file).use { archive ->
|
Archive(format.file.uri.toFile()).use { archive ->
|
||||||
val entry = archive.fileHeaders
|
val entry = archive.fileHeaders
|
||||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||||
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
||||||
|
|
||||||
entry?.let { updateCover(context, manga, archive.getInputStream(it)) }
|
entry?.let { updateCover(manga, archive.getInputStream(it)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Format.Epub -> {
|
is Format.Epub -> {
|
||||||
EpubFile(format.file).use { epub ->
|
EpubFile(format.file.uri.toFile()).use { epub ->
|
||||||
val entry = epub.getImagesFromPages()
|
val entry = epub.getImagesFromPages()
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
?.let { epub.getEntry(it) }
|
?.let { epub.getEntry(it) }
|
||||||
|
|
||||||
entry?.let { updateCover(context, manga, epub.getInputStream(it)) }
|
entry?.let { updateCover(manga, epub.getInputStream(it)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -374,10 +368,10 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed class Format {
|
sealed class Format {
|
||||||
data class Directory(val file: File) : Format()
|
data class Directory(val file: UniFile) : Format()
|
||||||
data class Zip(val file: File) : Format()
|
data class Zip(val file: UniFile) : Format()
|
||||||
data class Rar(val file: File) : Format()
|
data class Rar(val file: UniFile) : Format()
|
||||||
data class Epub(val file: File) : Format()
|
data class Epub(val file: UniFile) : Format()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -578,7 +578,7 @@ class LibraryPresenter(
|
||||||
|
|
||||||
private fun getLanguage(manga: Manga): String? {
|
private fun getLanguage(manga: Manga): String? {
|
||||||
return if (manga.isLocal()) {
|
return if (manga.isLocal()) {
|
||||||
LocalSource.getMangaLang(manga, context)
|
LocalSource.getMangaLang(manga)
|
||||||
} else {
|
} else {
|
||||||
sourceManager.get(manga.source)?.lang
|
sourceManager.get(manga.source)?.lang
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,7 +124,7 @@ class EditMangaDialog : DialogController {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
binding.mangaLang.setSelection(
|
binding.mangaLang.setSelection(
|
||||||
languages.indexOf(LocalSource.getMangaLang(manga, binding.root.context))
|
languages.indexOf(LocalSource.getMangaLang(manga))
|
||||||
.takeIf { it > -1 } ?: 0,
|
.takeIf { it > -1 } ?: 0,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -32,6 +32,7 @@ import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
|
import androidx.core.net.toFile
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsCompat.Type.systemBars
|
import androidx.core.view.WindowInsetsCompat.Type.systemBars
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
@ -118,7 +119,6 @@ import eu.kanade.tachiyomi.util.view.findChild
|
||||||
import eu.kanade.tachiyomi.util.view.getText
|
import eu.kanade.tachiyomi.util.view.getText
|
||||||
import eu.kanade.tachiyomi.util.view.isControllerVisible
|
import eu.kanade.tachiyomi.util.view.isControllerVisible
|
||||||
import eu.kanade.tachiyomi.util.view.previousController
|
import eu.kanade.tachiyomi.util.view.previousController
|
||||||
import eu.kanade.tachiyomi.util.view.requestFilePermissionsSafe
|
|
||||||
import eu.kanade.tachiyomi.util.view.scrollViewWith
|
import eu.kanade.tachiyomi.util.view.scrollViewWith
|
||||||
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
|
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
|
||||||
import eu.kanade.tachiyomi.util.view.setStyle
|
import eu.kanade.tachiyomi.util.view.setStyle
|
||||||
|
@ -249,7 +249,6 @@ class MangaDetailsController :
|
||||||
if (presenter.preferences.themeMangaDetails()) {
|
if (presenter.preferences.themeMangaDetails()) {
|
||||||
setItemColors()
|
setItemColors()
|
||||||
}
|
}
|
||||||
requestFilePermissionsSafe(301, presenter.preferences, presenter.manga.isLocal())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setAccentColorValue(colorToUse: Int? = null) {
|
private fun setAccentColorValue(colorToUse: Int? = null) {
|
||||||
|
@ -1193,7 +1192,7 @@ class MangaDetailsController :
|
||||||
fun shareCover() {
|
fun shareCover() {
|
||||||
val cover = presenter.shareCover()
|
val cover = presenter.shareCover()
|
||||||
if (cover != null) {
|
if (cover != null) {
|
||||||
val stream = cover.getUriCompat(activity!!)
|
val stream = cover.toFile().getUriCompat(activity!!)
|
||||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
putExtra(Intent.EXTRA_STREAM, stream)
|
putExtra(Intent.EXTRA_STREAM, stream)
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
|
|
@ -3,12 +3,14 @@ package eu.kanade.tachiyomi.ui.manga
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import androidx.core.net.toFile
|
||||||
import coil3.imageLoader
|
import coil3.imageLoader
|
||||||
import coil3.memory.MemoryCache
|
import coil3.memory.MemoryCache
|
||||||
import coil3.request.CachePolicy
|
import coil3.request.CachePolicy
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import coil3.request.SuccessResult
|
import coil3.request.SuccessResult
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import dev.yokai.domain.storage.StorageManager
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
@ -81,6 +83,7 @@ class MangaDetailsPresenter(
|
||||||
val db: DatabaseHelper = Injekt.get(),
|
val db: DatabaseHelper = Injekt.get(),
|
||||||
private val downloadManager: DownloadManager = Injekt.get(),
|
private val downloadManager: DownloadManager = Injekt.get(),
|
||||||
chapterFilter: ChapterFilter = Injekt.get(),
|
chapterFilter: ChapterFilter = Injekt.get(),
|
||||||
|
internal val storageManager: StorageManager = Injekt.get(),
|
||||||
) : BaseCoroutinePresenter<MangaDetailsController>(), DownloadQueue.DownloadListener {
|
) : BaseCoroutinePresenter<MangaDetailsController>(), DownloadQueue.DownloadListener {
|
||||||
|
|
||||||
private val customMangaManager: CustomMangaManager by injectLazy()
|
private val customMangaManager: CustomMangaManager by injectLazy()
|
||||||
|
@ -719,14 +722,13 @@ class MangaDetailsPresenter(
|
||||||
fun shareManga() {
|
fun shareManga() {
|
||||||
val context = Injekt.get<Application>()
|
val context = Injekt.get<Application>()
|
||||||
|
|
||||||
val destDir = File(context.cacheDir, "shared_image")
|
val destDir = UniFile.fromFile(context.cacheDir)!!.createDirectory("shared_image")!!
|
||||||
|
|
||||||
presenterScope.launchIO {
|
presenterScope.launchIO {
|
||||||
destDir.deleteRecursively()
|
|
||||||
try {
|
try {
|
||||||
val file = saveCover(destDir)
|
val uri = saveCover(destDir)
|
||||||
withUIContext {
|
withUIContext {
|
||||||
view?.shareManga(file)
|
view?.shareManga(uri.toFile())
|
||||||
}
|
}
|
||||||
} catch (_: java.lang.Exception) {
|
} catch (_: java.lang.Exception) {
|
||||||
}
|
}
|
||||||
|
@ -831,7 +833,7 @@ class MangaDetailsPresenter(
|
||||||
val inputStream =
|
val inputStream =
|
||||||
downloadManager.context.contentResolver.openInputStream(uri) ?: return false
|
downloadManager.context.contentResolver.openInputStream(uri) ?: return false
|
||||||
if (manga.isLocal()) {
|
if (manga.isLocal()) {
|
||||||
LocalSource.updateCover(downloadManager.context, manga, inputStream)
|
LocalSource.updateCover(manga, inputStream)
|
||||||
view?.setPaletteColor()
|
view?.setPaletteColor()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -844,9 +846,9 @@ class MangaDetailsPresenter(
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shareCover(): File? {
|
fun shareCover(): Uri? {
|
||||||
return try {
|
return try {
|
||||||
val destDir = File(coverCache.context.cacheDir, "shared_image")
|
val destDir = UniFile.fromFile(coverCache.context.cacheDir)!!.createDirectory("shared_image")!!
|
||||||
val file = saveCover(destDir)
|
val file = saveCover(destDir)
|
||||||
file
|
file
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -857,43 +859,33 @@ class MangaDetailsPresenter(
|
||||||
fun saveCover(): Boolean {
|
fun saveCover(): Boolean {
|
||||||
return try {
|
return try {
|
||||||
val directory = if (preferences.folderPerManga().get()) {
|
val directory = if (preferences.folderPerManga().get()) {
|
||||||
val baseDir = Environment.getExternalStorageDirectory().absolutePath +
|
storageManager.getCoversDirectory()!!.createDirectory(DiskUtil.buildValidFilename(manga.title))!!
|
||||||
File.separator + Environment.DIRECTORY_PICTURES +
|
|
||||||
File.separator + preferences.context.getString(R.string.app_normalized_name)
|
|
||||||
|
|
||||||
File(baseDir + File.separator + DiskUtil.buildValidFilename(manga.title))
|
|
||||||
} else {
|
} else {
|
||||||
File(
|
storageManager.getCoversDirectory()!!
|
||||||
Environment.getExternalStorageDirectory().absolutePath +
|
|
||||||
File.separator + Environment.DIRECTORY_PICTURES +
|
|
||||||
File.separator + preferences.context.getString(R.string.app_normalized_name),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
val file = saveCover(directory)
|
val uri = saveCover(directory)
|
||||||
DiskUtil.scanMedia(preferences.context, file)
|
DiskUtil.scanMedia(preferences.context, uri.toFile())
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveCover(directory: File): File {
|
private fun saveCover(directory: UniFile): Uri {
|
||||||
val cover = coverCache.getCustomCoverFile(manga).takeIf { it.exists() } ?: coverCache.getCoverFile(manga)
|
val cover = coverCache.getCustomCoverFile(manga).takeIf { it.exists() } ?: coverCache.getCoverFile(manga)
|
||||||
val type = ImageUtil.findImageType(cover.inputStream())
|
val type = ImageUtil.findImageType(cover.inputStream())
|
||||||
?: throw Exception("Not an image")
|
?: throw Exception("Not an image")
|
||||||
|
|
||||||
directory.mkdirs()
|
|
||||||
|
|
||||||
// Build destination file.
|
// Build destination file.
|
||||||
val filename = DiskUtil.buildValidFilename("${manga.title}.${type.extension}")
|
val filename = DiskUtil.buildValidFilename("${manga.title}.${type.extension}")
|
||||||
|
|
||||||
val destFile = File(directory, filename)
|
val destFile = directory.createFile(filename)!!
|
||||||
cover.inputStream().use { input ->
|
cover.inputStream().use { input ->
|
||||||
destFile.outputStream().use { output ->
|
destFile.openOutputStream().use { output ->
|
||||||
input.copyTo(output)
|
input.copyTo(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return destFile
|
return destFile.uri
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isTracked(): Boolean =
|
fun isTracked(): Boolean =
|
||||||
|
|
|
@ -483,7 +483,7 @@ class StatsDetailsPresenter(
|
||||||
*/
|
*/
|
||||||
private fun LibraryManga.getLanguage(): String {
|
private fun LibraryManga.getLanguage(): String {
|
||||||
val code = if (isLocal()) {
|
val code = if (isLocal()) {
|
||||||
LocalSource.getMangaLang(this, context)
|
LocalSource.getMangaLang(this)
|
||||||
} else {
|
} else {
|
||||||
sourceManager.get(source)?.lang
|
sourceManager.get(source)?.lang
|
||||||
} ?: return context.getString(R.string.unknown)
|
} ?: return context.getString(R.string.unknown)
|
||||||
|
|
|
@ -5,9 +5,12 @@ import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.core.net.toFile
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import dev.yokai.domain.storage.StorageManager
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
@ -86,6 +89,7 @@ class ReaderViewModel(
|
||||||
private val coverCache: CoverCache = Injekt.get(),
|
private val coverCache: CoverCache = Injekt.get(),
|
||||||
private val preferences: PreferencesHelper = Injekt.get(),
|
private val preferences: PreferencesHelper = Injekt.get(),
|
||||||
private val chapterFilter: ChapterFilter = Injekt.get(),
|
private val chapterFilter: ChapterFilter = Injekt.get(),
|
||||||
|
private val storageManager: StorageManager = Injekt.get(),
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val mutableState = MutableStateFlow(State())
|
private val mutableState = MutableStateFlow(State())
|
||||||
|
@ -743,13 +747,11 @@ class ReaderViewModel(
|
||||||
/**
|
/**
|
||||||
* Saves the image of this [page] in the given [directory] and returns the file location.
|
* Saves the image of this [page] in the given [directory] and returns the file location.
|
||||||
*/
|
*/
|
||||||
private fun saveImage(page: ReaderPage, directory: File, manga: Manga): File {
|
private fun saveImage(page: ReaderPage, directory: UniFile, manga: Manga): Uri {
|
||||||
val stream = page.stream!!
|
val stream = page.stream!!
|
||||||
val type = ImageUtil.findImageType(stream) ?: throw Exception("Not an image")
|
val type = ImageUtil.findImageType(stream) ?: throw Exception("Not an image")
|
||||||
val context = Injekt.get<Application>()
|
val context = Injekt.get<Application>()
|
||||||
|
|
||||||
directory.mkdirs()
|
|
||||||
|
|
||||||
val chapter = page.chapter.chapter
|
val chapter = page.chapter.chapter
|
||||||
|
|
||||||
// Build destination file.
|
// Build destination file.
|
||||||
|
@ -757,13 +759,13 @@ class ReaderViewModel(
|
||||||
"${manga.title} - ${chapter.preferredChapterName(context, manga, preferences)}".take(225),
|
"${manga.title} - ${chapter.preferredChapterName(context, manga, preferences)}".take(225),
|
||||||
) + " - ${page.number}.${type.extension}"
|
) + " - ${page.number}.${type.extension}"
|
||||||
|
|
||||||
val destFile = File(directory, filename)
|
val destFile = directory.createFile(filename)!!
|
||||||
stream().use { input ->
|
stream().use { input ->
|
||||||
destFile.outputStream().use { output ->
|
destFile.openOutputStream().use { output ->
|
||||||
input.copyTo(output)
|
input.copyTo(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return destFile
|
return destFile.uri
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -814,22 +816,20 @@ class ReaderViewModel(
|
||||||
notifier.onClear()
|
notifier.onClear()
|
||||||
|
|
||||||
// Pictures directory.
|
// Pictures directory.
|
||||||
val baseDir = Environment.getExternalStorageDirectory().absolutePath +
|
val baseDir = storageManager.getPagesDirectory()!!
|
||||||
File.separator + Environment.DIRECTORY_PICTURES +
|
|
||||||
File.separator + context.getString(R.string.app_normalized_name)
|
|
||||||
val destDir = if (preferences.folderPerManga().get()) {
|
val destDir = if (preferences.folderPerManga().get()) {
|
||||||
File(baseDir + File.separator + DiskUtil.buildValidFilename(manga.title))
|
baseDir.createDirectory(DiskUtil.buildValidFilename(manga.title))!!
|
||||||
} else {
|
} else {
|
||||||
File(baseDir)
|
baseDir
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy file in background.
|
// Copy file in background.
|
||||||
viewModelScope.launchNonCancellable {
|
viewModelScope.launchNonCancellable {
|
||||||
try {
|
try {
|
||||||
val file = saveImage(page, destDir, manga)
|
val uri = saveImage(page, destDir, manga)
|
||||||
DiskUtil.scanMedia(context, file)
|
DiskUtil.scanMedia(context, uri.toFile())
|
||||||
notifier.onComplete(file)
|
notifier.onComplete(uri.toFile())
|
||||||
eventChannel.send(Event.SavedImage(SaveImageResult.Success(file)))
|
eventChannel.send(Event.SavedImage(SaveImageResult.Success(uri.toFile())))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
notifier.onError(e.message)
|
notifier.onError(e.message)
|
||||||
eventChannel.send(Event.SavedImage(SaveImageResult.Error(e)))
|
eventChannel.send(Event.SavedImage(SaveImageResult.Error(e)))
|
||||||
|
@ -880,12 +880,11 @@ class ReaderViewModel(
|
||||||
val manga = manga ?: return
|
val manga = manga ?: return
|
||||||
val context = Injekt.get<Application>()
|
val context = Injekt.get<Application>()
|
||||||
|
|
||||||
val destDir = File(context.cacheDir, "shared_image")
|
val destDir = UniFile.fromFile(context.cacheDir)!!.createDirectory("shared_image")!!
|
||||||
|
|
||||||
viewModelScope.launchNonCancellable {
|
viewModelScope.launchNonCancellable {
|
||||||
destDir.deleteRecursively() // Keep only the last shared file
|
val uri = saveImage(page, destDir, manga)
|
||||||
val file = saveImage(page, destDir, manga)
|
eventChannel.send(Event.ShareImage(uri.toFile(), page))
|
||||||
eventChannel.send(Event.ShareImage(file, page))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -919,7 +918,7 @@ class ReaderViewModel(
|
||||||
if (manga.isLocal()) {
|
if (manga.isLocal()) {
|
||||||
val context = Injekt.get<Application>()
|
val context = Injekt.get<Application>()
|
||||||
coverCache.deleteFromCache(manga)
|
coverCache.deleteFromCache(manga)
|
||||||
LocalSource.updateCover(context, manga, stream())
|
LocalSource.updateCover(manga, stream())
|
||||||
R.string.cover_updated
|
R.string.cover_updated
|
||||||
SetAsCoverResult.Success
|
SetAsCoverResult.Success
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
|
import eu.kanade.tachiyomi.util.system.toTempFile
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
@ -48,7 +49,7 @@ class DownloadPageLoader(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getPagesFromArchive(chapterPath: UniFile): List<ReaderPage> {
|
private suspend fun getPagesFromArchive(chapterPath: UniFile): List<ReaderPage> {
|
||||||
val loader = ZipPageLoader(File(chapterPath.filePath!!)).also { zipPageLoader = it }
|
val loader = ZipPageLoader(chapterPath.toTempFile(context)).also { zipPageLoader = it }
|
||||||
return loader.getPages()
|
return loader.getPages()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,6 @@ import eu.kanade.tachiyomi.util.view.isExpanded
|
||||||
import eu.kanade.tachiyomi.util.view.isHidden
|
import eu.kanade.tachiyomi.util.view.isHidden
|
||||||
import eu.kanade.tachiyomi.util.view.moveRecyclerViewUp
|
import eu.kanade.tachiyomi.util.view.moveRecyclerViewUp
|
||||||
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
|
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
|
||||||
import eu.kanade.tachiyomi.util.view.requestFilePermissionsSafe
|
|
||||||
import eu.kanade.tachiyomi.util.view.scrollViewWith
|
import eu.kanade.tachiyomi.util.view.scrollViewWith
|
||||||
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
|
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
|
||||||
import eu.kanade.tachiyomi.util.view.setStyle
|
import eu.kanade.tachiyomi.util.view.setStyle
|
||||||
|
@ -421,7 +420,6 @@ class RecentsController(bundle: Bundle? = null) :
|
||||||
binding.downloadBottomSheet.dlBottomSheet.sheetBehavior?.expand()
|
binding.downloadBottomSheet.dlBottomSheet.sheetBehavior?.expand()
|
||||||
}
|
}
|
||||||
setPadding(binding.downloadBottomSheet.dlBottomSheet.sheetBehavior?.isHideable == true)
|
setPadding(binding.downloadBottomSheet.dlBottomSheet.sheetBehavior?.isHideable == true)
|
||||||
requestFilePermissionsSafe(301, presenter.preferences)
|
|
||||||
|
|
||||||
binding.downloadBottomSheet.root.sheetBehavior?.isGestureInsetBottomIgnored = true
|
binding.downloadBottomSheet.root.sheetBehavior?.isGestureInsetBottomIgnored = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,16 +4,16 @@ import android.app.Activity
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
|
import dev.yokai.domain.storage.StorageManager
|
||||||
|
import dev.yokai.domain.storage.StoragePreferences
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst
|
import eu.kanade.tachiyomi.data.backup.BackupConst
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||||
|
@ -26,23 +26,43 @@ import eu.kanade.tachiyomi.util.system.disableItems
|
||||||
import eu.kanade.tachiyomi.util.system.materialAlertDialog
|
import eu.kanade.tachiyomi.util.system.materialAlertDialog
|
||||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.util.view.requestFilePermissionsSafe
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class SettingsBackupController : SettingsController() {
|
class SettingsDataController : SettingsController() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flags containing information of what to backup.
|
* Flags containing information of what to backup.
|
||||||
*/
|
*/
|
||||||
private var backupFlags = 0
|
private var backupFlags = 0
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
internal val storagePreferences: StoragePreferences by injectLazy()
|
||||||
super.onViewCreated(view, savedInstanceState)
|
internal val storageManager: StorageManager by injectLazy()
|
||||||
requestFilePermissionsSafe(500, preferences)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
|
||||||
titleRes = R.string.backup_and_restore
|
titleRes = R.string.data_and_storage
|
||||||
|
|
||||||
|
preference {
|
||||||
|
bindTo(storagePreferences.baseStorageDirectory())
|
||||||
|
titleRes = R.string.storage_location
|
||||||
|
|
||||||
|
onClick {
|
||||||
|
try {
|
||||||
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||||
|
startActivityForResult(intent, CODE_DATA_DIR)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
activity?.toast(R.string.file_picker_error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
storagePreferences.baseStorageDirectory().changes()
|
||||||
|
.onEach { path ->
|
||||||
|
summary = UniFile.fromUri(context, path.toUri())!!.let { dir ->
|
||||||
|
dir.filePath ?: context.getString(R.string.invalid_location, dir.uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(viewScope)
|
||||||
|
}
|
||||||
|
|
||||||
preference {
|
preference {
|
||||||
key = "pref_create_backup"
|
key = "pref_create_backup"
|
||||||
|
@ -75,7 +95,7 @@ class SettingsBackupController : SettingsController() {
|
||||||
(activity as? MainActivity)?.getExtensionUpdates(true)
|
(activity as? MainActivity)?.getExtensionUpdates(true)
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
intent.type = "*/*"
|
intent.setDataAndType(storageManager.getBackupsDirectory()!!.uri, "*/*")
|
||||||
val title = resources?.getString(R.string.select_backup_file)
|
val title = resources?.getString(R.string.select_backup_file)
|
||||||
val chooser = Intent.createChooser(intent, title)
|
val chooser = Intent.createChooser(intent, title)
|
||||||
startActivityForResult(chooser, CODE_BACKUP_RESTORE)
|
startActivityForResult(chooser, CODE_BACKUP_RESTORE)
|
||||||
|
@ -107,29 +127,6 @@ class SettingsBackupController : SettingsController() {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
preference {
|
|
||||||
bindTo(preferences.backupsDirectory())
|
|
||||||
titleRes = R.string.backup_location
|
|
||||||
|
|
||||||
onClick {
|
|
||||||
try {
|
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
|
||||||
startActivityForResult(intent, CODE_BACKUP_DIR)
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
activity?.toast(R.string.file_picker_error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
visibleIf(preferences.backupInterval()) { it > 0 }
|
|
||||||
|
|
||||||
preferences.backupsDirectory().changes()
|
|
||||||
.onEach { path ->
|
|
||||||
val dir = UniFile.fromUri(context, path.toUri())!!
|
|
||||||
val filePath = dir.filePath
|
|
||||||
summary = if (filePath != null) "$filePath/automatic" else "Invalid directory: ${dir.uri}"
|
|
||||||
}
|
|
||||||
.launchIn(viewScope)
|
|
||||||
}
|
|
||||||
intListPreference(activity) {
|
intListPreference(activity) {
|
||||||
bindTo(preferences.numberOfBackups())
|
bindTo(preferences.numberOfBackups())
|
||||||
titleRes = R.string.max_auto_backups
|
titleRes = R.string.max_auto_backups
|
||||||
|
@ -165,22 +162,18 @@ class SettingsBackupController : SettingsController() {
|
||||||
}
|
}
|
||||||
|
|
||||||
when (requestCode) {
|
when (requestCode) {
|
||||||
CODE_BACKUP_DIR -> {
|
CODE_DATA_DIR -> {
|
||||||
// Get UriPermission so it's possible to write files
|
// Get UriPermission so it's possible to write files
|
||||||
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
|
||||||
activity.contentResolver.takePersistableUriPermission(uri, flags)
|
activity.contentResolver.takePersistableUriPermission(uri, flags)
|
||||||
preferences.backupsDirectory().set(uri.toString())
|
val file = UniFile.fromUri(activity, uri)!!
|
||||||
|
storagePreferences.baseStorageDirectory().set(file.uri.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
CODE_BACKUP_CREATE -> {
|
CODE_BACKUP_CREATE -> {
|
||||||
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
doBackup(backupFlags, uri)
|
||||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
|
||||||
|
|
||||||
activity.contentResolver.takePersistableUriPermission(uri, flags)
|
|
||||||
activity.toast(R.string.creating_backup)
|
|
||||||
BackupCreatorJob.startNow(activity, uri, backupFlags)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CODE_BACKUP_RESTORE -> {
|
CODE_BACKUP_RESTORE -> {
|
||||||
|
@ -191,8 +184,25 @@ class SettingsBackupController : SettingsController() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createBackup(flags: Int) {
|
private fun doBackup(flags: Int, uri: Uri) {
|
||||||
|
val activity = activity ?: return
|
||||||
|
|
||||||
|
val intentFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||||
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
|
||||||
|
activity.contentResolver.takePersistableUriPermission(uri, intentFlags)
|
||||||
|
activity.toast(R.string.creating_backup)
|
||||||
|
BackupCreatorJob.startNow(activity, uri, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createBackup(flags: Int, picker: Boolean = false) {
|
||||||
backupFlags = flags
|
backupFlags = flags
|
||||||
|
|
||||||
|
if (!picker) {
|
||||||
|
doBackup(backupFlags, storageManager.getBackupsDirectory()!!.uri)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use Android's built-in file creator
|
// Use Android's built-in file creator
|
||||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||||
|
@ -299,7 +309,7 @@ class SettingsBackupController : SettingsController() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val CODE_BACKUP_DIR = 503
|
private const val CODE_DATA_DIR = 104
|
||||||
private const val CODE_BACKUP_CREATE = 504
|
private const val CODE_BACKUP_CREATE = 504
|
||||||
private const val CODE_BACKUP_RESTORE = 505
|
private const val CODE_BACKUP_RESTORE = 505
|
||||||
|
|
|
@ -1,24 +1,14 @@
|
||||||
package eu.kanade.tachiyomi.ui.setting
|
package eu.kanade.tachiyomi.ui.setting
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Environment
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.bluelinelabs.conductor.Controller
|
||||||
import com.hippo.unifile.UniFile
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.data.preference.changesIn
|
import eu.kanade.tachiyomi.data.preference.changesIn
|
||||||
import eu.kanade.tachiyomi.util.system.withOriginalWidth
|
import eu.kanade.tachiyomi.util.view.withFadeTransaction
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||||
|
|
||||||
class SettingsDownloadController : SettingsController() {
|
class SettingsDownloadController : SettingsController() {
|
||||||
|
@ -31,14 +21,9 @@ class SettingsDownloadController : SettingsController() {
|
||||||
preference {
|
preference {
|
||||||
key = Keys.downloadsDirectory
|
key = Keys.downloadsDirectory
|
||||||
titleRes = R.string.download_location
|
titleRes = R.string.download_location
|
||||||
onClick {
|
onClick { navigateTo(SettingsDataController()) }
|
||||||
DownloadDirectoriesDialog(this@SettingsDownloadController).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
preferences.downloadsDirectory().changesIn(viewScope) { path ->
|
summary = "Moved to Data and Storage!"
|
||||||
val dir = UniFile.fromUri(context, path.toUri())!!
|
|
||||||
summary = dir.filePath ?: path
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
switchPreference {
|
switchPreference {
|
||||||
key = Keys.downloadOnlyOverWifi
|
key = Keys.downloadOnlyOverWifi
|
||||||
|
@ -150,70 +135,9 @@ class SettingsDownloadController : SettingsController() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
when (requestCode) {
|
|
||||||
DOWNLOAD_DIR -> if (data != null && resultCode == Activity.RESULT_OK) {
|
|
||||||
val context = applicationContext ?: return
|
|
||||||
val uri = data.data
|
|
||||||
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
|
||||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
|
||||||
|
|
||||||
if (uri != null) {
|
|
||||||
@Suppress("NewApi")
|
|
||||||
context.contentResolver.takePersistableUriPermission(uri, flags)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val file = UniFile.fromUri(context, uri)!!
|
private fun navigateTo(controller: Controller) {
|
||||||
preferences.downloadsDirectory().set(file.uri.toString())
|
router.pushController(controller.withFadeTransaction())
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun predefinedDirectorySelected(selectedDir: String) {
|
|
||||||
val path = Uri.fromFile(File(selectedDir))
|
|
||||||
preferences.downloadsDirectory().set(path.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun customDirectorySelected() {
|
|
||||||
startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE), DOWNLOAD_DIR)
|
|
||||||
}
|
|
||||||
|
|
||||||
class DownloadDirectoriesDialog(val controller: SettingsDownloadController) :
|
|
||||||
MaterialAlertDialogBuilder(controller.activity!!.withOriginalWidth()) {
|
|
||||||
|
|
||||||
private val preferences: PreferencesHelper = Injekt.get()
|
|
||||||
|
|
||||||
val activity = controller.activity!!
|
|
||||||
|
|
||||||
init {
|
|
||||||
val currentDir = preferences.downloadsDirectory().get()
|
|
||||||
val externalDirs =
|
|
||||||
getExternalDirs() + File(activity.getString(R.string.custom_location))
|
|
||||||
val selectedIndex = externalDirs.map(File::toString).indexOfFirst { it in currentDir }
|
|
||||||
val items = externalDirs.map { it.path }
|
|
||||||
|
|
||||||
setTitle(R.string.download_location)
|
|
||||||
setSingleChoiceItems(items.toTypedArray(), selectedIndex) { dialog, position ->
|
|
||||||
if (position == externalDirs.lastIndex) {
|
|
||||||
controller.customDirectorySelected()
|
|
||||||
} else {
|
|
||||||
controller.predefinedDirectorySelected(items[position])
|
|
||||||
}
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
|
||||||
setNegativeButton(android.R.string.cancel, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getExternalDirs(): List<File> {
|
|
||||||
val defaultDir = Environment.getExternalStorageDirectory().absolutePath +
|
|
||||||
File.separator + activity.resources?.getString(R.string.app_normalized_name) +
|
|
||||||
File.separator + "downloads"
|
|
||||||
|
|
||||||
return mutableListOf(File(defaultDir)) +
|
|
||||||
ContextCompat.getExternalFilesDirs(activity, "").filterNotNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
const val DOWNLOAD_DIR = 104
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,8 +75,8 @@ class SettingsMainController : SettingsController(), FloatingSearchInterface {
|
||||||
preference {
|
preference {
|
||||||
iconRes = R.drawable.ic_backup_restore_24dp
|
iconRes = R.drawable.ic_backup_restore_24dp
|
||||||
iconTint = tintColor
|
iconTint = tintColor
|
||||||
titleRes = R.string.backup_and_restore
|
titleRes = R.string.data_and_storage
|
||||||
onClick { navigateTo(SettingsBackupController()) }
|
onClick { navigateTo(SettingsDataController()) }
|
||||||
}
|
}
|
||||||
preference {
|
preference {
|
||||||
iconRes = R.drawable.ic_security_24dp
|
iconRes = R.drawable.ic_security_24dp
|
||||||
|
|
|
@ -9,7 +9,7 @@ import androidx.preference.PreferenceGroup
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsAdvancedController
|
import eu.kanade.tachiyomi.ui.setting.SettingsAdvancedController
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsAppearanceController
|
import eu.kanade.tachiyomi.ui.setting.SettingsAppearanceController
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsBackupController
|
import eu.kanade.tachiyomi.ui.setting.SettingsDataController
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsBrowseController
|
import eu.kanade.tachiyomi.ui.setting.SettingsBrowseController
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsController
|
import eu.kanade.tachiyomi.ui.setting.SettingsController
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsDownloadController
|
import eu.kanade.tachiyomi.ui.setting.SettingsDownloadController
|
||||||
|
@ -31,7 +31,7 @@ object SettingsSearchHelper {
|
||||||
*/
|
*/
|
||||||
private val settingControllersList: List<KClass<out SettingsController>> = listOf(
|
private val settingControllersList: List<KClass<out SettingsController>> = listOf(
|
||||||
SettingsAdvancedController::class,
|
SettingsAdvancedController::class,
|
||||||
SettingsBackupController::class,
|
SettingsDataController::class,
|
||||||
SettingsBrowseController::class,
|
SettingsBrowseController::class,
|
||||||
SettingsDownloadController::class,
|
SettingsDownloadController::class,
|
||||||
SettingsGeneralController::class,
|
SettingsGeneralController::class,
|
||||||
|
|
|
@ -60,7 +60,6 @@ import eu.kanade.tachiyomi.util.view.isCollapsed
|
||||||
import eu.kanade.tachiyomi.util.view.isCompose
|
import eu.kanade.tachiyomi.util.view.isCompose
|
||||||
import eu.kanade.tachiyomi.util.view.isControllerVisible
|
import eu.kanade.tachiyomi.util.view.isControllerVisible
|
||||||
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
|
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
|
||||||
import eu.kanade.tachiyomi.util.view.requestFilePermissionsSafe
|
|
||||||
import eu.kanade.tachiyomi.util.view.scrollViewWith
|
import eu.kanade.tachiyomi.util.view.scrollViewWith
|
||||||
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
|
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
|
||||||
import eu.kanade.tachiyomi.util.view.snack
|
import eu.kanade.tachiyomi.util.view.snack
|
||||||
|
@ -182,7 +181,6 @@ class BrowseController :
|
||||||
updateTitleAndMenu()
|
updateTitleAndMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
requestFilePermissionsSafe(301, preferences)
|
|
||||||
binding.bottomSheet.root.onCreate(this)
|
binding.bottomSheet.root.onCreate(this)
|
||||||
|
|
||||||
basePreferences.extensionInstaller().changes()
|
basePreferences.extensionInstaller().changes()
|
||||||
|
|
|
@ -48,7 +48,6 @@ import eu.kanade.tachiyomi.util.view.applyBottomAnimatedInsets
|
||||||
import eu.kanade.tachiyomi.util.view.fullAppBarHeight
|
import eu.kanade.tachiyomi.util.view.fullAppBarHeight
|
||||||
import eu.kanade.tachiyomi.util.view.inflate
|
import eu.kanade.tachiyomi.util.view.inflate
|
||||||
import eu.kanade.tachiyomi.util.view.isControllerVisible
|
import eu.kanade.tachiyomi.util.view.isControllerVisible
|
||||||
import eu.kanade.tachiyomi.util.view.requestFilePermissionsSafe
|
|
||||||
import eu.kanade.tachiyomi.util.view.scrollViewWith
|
import eu.kanade.tachiyomi.util.view.scrollViewWith
|
||||||
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
|
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
|
||||||
import eu.kanade.tachiyomi.util.view.snack
|
import eu.kanade.tachiyomi.util.view.snack
|
||||||
|
@ -182,7 +181,6 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||||
} else {
|
} else {
|
||||||
binding.progress.isVisible = true
|
binding.progress.isVisible = true
|
||||||
}
|
}
|
||||||
requestFilePermissionsSafe(301, preferences, presenter.source is LocalSource)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView(view: View) {
|
override fun onDestroyView(view: View) {
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
package eu.kanade.tachiyomi.util.system
|
||||||
|
|
||||||
|
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.nameWithoutExtension: String?
|
||||||
|
get() = name?.substringBeforeLast('.')
|
||||||
|
|
||||||
|
val UniFile.extension: String?
|
||||||
|
get() = name?.replace(nameWithoutExtension.orEmpty(), "")
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
fun UniFile.writeText(string: String) {
|
||||||
|
this.openOutputStream().use {
|
||||||
|
it.write(string.toByteArray())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
package eu.kanade.tachiyomi.util.view
|
package eu.kanade.tachiyomi.util.view
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.animation.Animator
|
import android.animation.Animator
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
import android.app.ActivityManager
|
import android.app.ActivityManager
|
||||||
|
@ -8,12 +7,9 @@ import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
|
||||||
import android.provider.Settings
|
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
@ -26,7 +22,6 @@ import androidx.annotation.CallSuper
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.core.math.MathUtils
|
import androidx.core.math.MathUtils
|
||||||
|
@ -53,7 +48,6 @@ import com.bluelinelabs.conductor.Router
|
||||||
import com.bluelinelabs.conductor.RouterTransaction
|
import com.bluelinelabs.conductor.RouterTransaction
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.databinding.MainActivityBinding
|
import eu.kanade.tachiyomi.databinding.MainActivityBinding
|
||||||
import eu.kanade.tachiyomi.ui.base.SmallToolbarInterface
|
import eu.kanade.tachiyomi.ui.base.SmallToolbarInterface
|
||||||
|
@ -73,7 +67,6 @@ import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
import eu.kanade.tachiyomi.util.system.ignoredSystemInsets
|
import eu.kanade.tachiyomi.util.system.ignoredSystemInsets
|
||||||
import eu.kanade.tachiyomi.util.system.materialAlertDialog
|
|
||||||
import eu.kanade.tachiyomi.util.system.rootWindowInsetsCompat
|
import eu.kanade.tachiyomi.util.system.rootWindowInsetsCompat
|
||||||
import eu.kanade.tachiyomi.util.system.toInt
|
import eu.kanade.tachiyomi.util.system.toInt
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
@ -780,54 +773,6 @@ fun Controller.setAppBarBG(value: Float, includeTabView: Boolean = false) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Controller.requestFilePermissionsSafe(
|
|
||||||
requestCode: Int,
|
|
||||||
preferences: PreferencesHelper,
|
|
||||||
showA11PermissionAnyway: Boolean = false,
|
|
||||||
) {
|
|
||||||
val activity = activity ?: return
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
|
||||||
val permissions = mutableListOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
|
||||||
permissions.forEach { permission ->
|
|
||||||
if (ContextCompat.checkSelfPermission(
|
|
||||||
activity,
|
|
||||||
permission,
|
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
|
||||||
) {
|
|
||||||
requestPermissions(arrayOf(permission), requestCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
|
||||||
!Environment.isExternalStorageManager() &&
|
|
||||||
(!preferences.hasDeniedA11FilePermission().get() || showA11PermissionAnyway)
|
|
||||||
) {
|
|
||||||
preferences.hasDeniedA11FilePermission().set(true)
|
|
||||||
activity.materialAlertDialog()
|
|
||||||
.setTitle(R.string.all_files_permission_required)
|
|
||||||
.setMessage(R.string.external_storage_permission_notice)
|
|
||||||
.setCancelable(false)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
val intent = Intent(
|
|
||||||
Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
|
|
||||||
"package:${activity.packageName}".toUri(),
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
activity.startActivity(intent)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
val intent2 = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
|
|
||||||
activity.startActivity(intent2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
} else if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.R || Environment.isExternalStorageManager()) && !preferences.backupInterval().isSet()) {
|
|
||||||
preferences.backupInterval().set(24)
|
|
||||||
BackupCreatorJob.setupTask(activity, 24)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Controller.withFadeTransaction(): RouterTransaction {
|
fun Controller.withFadeTransaction(): RouterTransaction {
|
||||||
return RouterTransaction.with(this)
|
return RouterTransaction.with(this)
|
||||||
.pushChangeHandler(fadeTransactionHandler())
|
.pushChangeHandler(fadeTransactionHandler())
|
||||||
|
|
|
@ -793,6 +793,10 @@
|
||||||
<string name="series_opens_new_chapters">Series shortcuts opens new chapters</string>
|
<string name="series_opens_new_chapters">Series shortcuts opens new chapters</string>
|
||||||
<string name="no_new_chapters_open_details">When there\'s no new chapters, the series\' details will open instead</string>
|
<string name="no_new_chapters_open_details">When there\'s no new chapters, the series\' details will open instead</string>
|
||||||
|
|
||||||
|
<!-- Storage -->
|
||||||
|
<string name="data_and_storage">Data and storage</string>
|
||||||
|
<string name="storage_location">Storage location</string>
|
||||||
|
|
||||||
<!-- Backup -->
|
<!-- Backup -->
|
||||||
<string name="backup">Backup</string>
|
<string name="backup">Backup</string>
|
||||||
<string name="backup_and_restore">Backup and restore</string>
|
<string name="backup_and_restore">Backup and restore</string>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue