Configurable dir for silent pages saving
This commit is contained in:
@@ -11,6 +11,7 @@ import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.collection.ArraySet
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.preference.PreferenceManager
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.json.JSONArray
|
||||
@@ -412,6 +413,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
val isReadingTimeEstimationEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_READING_TIME, true)
|
||||
|
||||
val isPagesSavingAskEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_PAGES_SAVE_ASK, true)
|
||||
|
||||
fun isTipEnabled(tip: String): Boolean {
|
||||
return prefs.getStringSet(KEY_TIPS_CLOSED, emptySet())?.contains(tip) != true
|
||||
}
|
||||
@@ -424,6 +428,15 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
prefs.edit { putStringSet(KEY_TIPS_CLOSED, closedTips + tip) }
|
||||
}
|
||||
|
||||
fun getPagesSaveDir(context: Context): DocumentFile? =
|
||||
prefs.getString(KEY_PAGES_SAVE_DIR, null)?.toUriOrNull()?.let {
|
||||
DocumentFile.fromTreeUri(context, it)
|
||||
}
|
||||
|
||||
fun setPagesSaveDir(uri: Uri?) {
|
||||
prefs.edit { putString(KEY_PAGES_SAVE_DIR, uri?.toString()) }
|
||||
}
|
||||
|
||||
fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
|
||||
prefs.registerOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
@@ -591,6 +604,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_IGNORE_DOZE = "ignore_dose"
|
||||
const val KEY_DETAILS_TAB = "details_tab"
|
||||
const val KEY_READING_TIME = "reading_time"
|
||||
const val KEY_PAGES_SAVE_DIR = "pages_dir"
|
||||
const val KEY_PAGES_SAVE_ASK = "pages_dir_ask"
|
||||
|
||||
// About
|
||||
const val KEY_APP_UPDATE = "app_update"
|
||||
|
||||
@@ -8,19 +8,26 @@ import android.provider.DocumentsContract
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.net.toUri
|
||||
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
|
||||
import java.io.File
|
||||
|
||||
class PageSaveContract : ActivityResultContracts.CreateDocument("image/*") {
|
||||
|
||||
override fun createIntent(context: Context, input: String): Intent {
|
||||
val intent = super.createIntent(context, input)
|
||||
val intent = super.createIntent(context, input.substringAfterLast(File.separatorChar))
|
||||
intent.type = MimeTypeMap.getSingleton()
|
||||
.getMimeTypeFromExtension(input.substringAfterLast('.')) ?: "image/*"
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val defaultUri = input.toUriOrNull()?.run {
|
||||
path?.let { p ->
|
||||
buildUpon().path(p.substringBeforeLast('/')).build()
|
||||
}
|
||||
}
|
||||
intent.putExtra(
|
||||
DocumentsContract.EXTRA_INITIAL_URI,
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toUri(),
|
||||
defaultUri ?: Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toUri(),
|
||||
)
|
||||
}
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okio.IOException
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.source
|
||||
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
|
||||
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
|
||||
@@ -30,7 +31,8 @@ private const val MAX_FILENAME_LENGTH = 10
|
||||
private const val EXTENSION_FALLBACK = "png"
|
||||
|
||||
class PageSaveHelper @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
@ApplicationContext private val context: Context,
|
||||
private val settings: AppSettings,
|
||||
) {
|
||||
|
||||
private var continuation: Continuation<Uri>? = null
|
||||
@@ -44,14 +46,7 @@ class PageSaveHelper @Inject constructor(
|
||||
val pageUrl = pageLoader.getPageUrl(page)
|
||||
val pageUri = pageLoader.loadPage(page, force = false)
|
||||
val proposedName = getProposedFileName(pageUrl, pageUri)
|
||||
val destination = withContext(Dispatchers.Main) {
|
||||
suspendCancellableCoroutine { cont ->
|
||||
continuation = cont
|
||||
saveLauncher.launch(proposedName)
|
||||
}.also {
|
||||
continuation = null
|
||||
}
|
||||
}
|
||||
val destination = getDefaultFileUri(proposedName) ?: pickFileUri(saveLauncher, proposedName)
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
contentResolver.openOutputStream(destination)?.sink()?.buffer()
|
||||
}?.use { output ->
|
||||
@@ -62,12 +57,35 @@ class PageSaveHelper @Inject constructor(
|
||||
return destination
|
||||
}
|
||||
|
||||
private fun getDefaultFileUri(proposedName: String): Uri? {
|
||||
if (settings.isPagesSavingAskEnabled) {
|
||||
return null
|
||||
}
|
||||
return settings.getPagesSaveDir(context)?.let {
|
||||
val ext = proposedName.substringAfterLast('.', "")
|
||||
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return null
|
||||
it.createFile(mime, proposedName.substringBeforeLast('.'))?.uri
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun pickFileUri(saveLauncher: ActivityResultLauncher<String>, proposedName: String): Uri {
|
||||
val defaultUri = settings.getPagesSaveDir(context)?.uri?.buildUpon()?.appendPath(proposedName)?.toString()
|
||||
return withContext(Dispatchers.Main) {
|
||||
suspendCancellableCoroutine { cont ->
|
||||
continuation = cont
|
||||
saveLauncher.launch(defaultUri ?: proposedName)
|
||||
}.also {
|
||||
continuation = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onActivityResult(uri: Uri): Boolean = continuation?.apply {
|
||||
resume(uri)
|
||||
} != null
|
||||
|
||||
private suspend fun getProposedFileName(url: String, fileUri: Uri): String {
|
||||
var name = if (url.startsWith("cbz://")) {
|
||||
var name = if (url.startsWith("cbz:")) {
|
||||
requireNotNull(url.toUri().fragment)
|
||||
} else {
|
||||
url.toHttpUrl().pathSegments.last()
|
||||
|
||||
@@ -117,6 +117,7 @@ class ReaderConfigSheet :
|
||||
R.id.button_save_page -> {
|
||||
val page = viewModel.getCurrentPage() ?: return
|
||||
viewModel.saveCurrentPage(page, savePageRequest)
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
R.id.button_screen_rotate -> {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -12,6 +17,8 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.resolveFile
|
||||
import org.koitharu.kotatsu.core.util.ext.tryLaunch
|
||||
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||
@@ -25,7 +32,7 @@ class DownloadsSettingsFragment :
|
||||
BasePreferenceFragment(R.string.downloads),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private val dozeHelper = DozeHelper(this)
|
||||
private val dozeHelper = DozeHelper(this)
|
||||
|
||||
@Inject
|
||||
lateinit var storageManager: LocalStorageManager
|
||||
@@ -33,6 +40,10 @@ class DownloadsSettingsFragment :
|
||||
@Inject
|
||||
lateinit var downloadsScheduler: DownloadWorker.Scheduler
|
||||
|
||||
private val pickFileTreeLauncher = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
|
||||
if (it != null) onDirectoryPicked(it)
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_downloads)
|
||||
dozeHelper.updatePreference()
|
||||
@@ -42,6 +53,7 @@ class DownloadsSettingsFragment :
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
findPreference<Preference>(AppSettings.KEY_LOCAL_STORAGE)?.bindStorageName()
|
||||
findPreference<Preference>(AppSettings.KEY_LOCAL_MANGA_DIRS)?.bindDirectoriesCount()
|
||||
findPreference<Preference>(AppSettings.KEY_PAGES_SAVE_DIR)?.bindPagesDirectory()
|
||||
settings.subscribe(this)
|
||||
}
|
||||
|
||||
@@ -63,6 +75,10 @@ class DownloadsSettingsFragment :
|
||||
AppSettings.KEY_DOWNLOADS_WIFI -> {
|
||||
updateDownloadsConstraints()
|
||||
}
|
||||
|
||||
AppSettings.KEY_PAGES_SAVE_DIR -> {
|
||||
findPreference<Preference>(AppSettings.KEY_PAGES_SAVE_DIR)?.bindPagesDirectory()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,10 +98,27 @@ class DownloadsSettingsFragment :
|
||||
dozeHelper.startIgnoreDoseActivity()
|
||||
}
|
||||
|
||||
AppSettings.KEY_PAGES_SAVE_DIR -> {
|
||||
if (!pickFileTreeLauncher.tryLaunch(settings.getPagesSaveDir(preference.context)?.uri)) {
|
||||
Snackbar.make(
|
||||
requireView(), R.string.operation_not_supported, Snackbar.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDirectoryPicked(uri: Uri) {
|
||||
storageManager.takePermissions(uri)
|
||||
val doc = DocumentFile.fromTreeUri(requireContext(), uri)?.takeIf {
|
||||
it.canWrite()
|
||||
}
|
||||
settings.setPagesSaveDir(doc?.uri)
|
||||
}
|
||||
|
||||
private fun Preference.bindStorageName() {
|
||||
viewLifecycleScope.launch {
|
||||
val storage = storageManager.getDefaultWriteableDir()
|
||||
@@ -104,6 +137,16 @@ class DownloadsSettingsFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private fun Preference.bindPagesDirectory() {
|
||||
viewLifecycleScope.launch {
|
||||
val df = withContext(Dispatchers.IO) {
|
||||
settings.getPagesSaveDir(this@bindPagesDirectory.context)
|
||||
}
|
||||
summary = df?.getDisplayPath(this@bindPagesDirectory.context)
|
||||
?: this@bindPagesDirectory.context.getString(androidx.preference.R.string.not_set)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDownloadsConstraints() {
|
||||
val preference = findPreference<Preference>(AppSettings.KEY_DOWNLOADS_WIFI)
|
||||
viewLifecycleScope.launch {
|
||||
@@ -119,4 +162,9 @@ class DownloadsSettingsFragment :
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DocumentFile.getDisplayPath(context: Context): String {
|
||||
return uri.resolveFile(context)?.path ?: uri.toString()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,4 +35,18 @@
|
||||
android:summary="@string/downloads_settings_info"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
<PreferenceCategory android:title="@string/pages_saving">
|
||||
|
||||
<Preference
|
||||
android:key="pages_dir"
|
||||
android:persistent="false"
|
||||
android:title="@string/default_page_save_dir" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="pages_dir_ask"
|
||||
android:title="@string/ask_for_dest_dir_every_time" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
Reference in New Issue
Block a user