Configurable dir for silent pages saving

This commit is contained in:
Koitharu
2024-02-17 12:31:02 +02:00
parent 92ed320f57
commit d71514ec7a
7 changed files with 703 additions and 597 deletions

View File

@@ -11,6 +11,7 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.ArraySet import androidx.collection.ArraySet
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import org.json.JSONArray import org.json.JSONArray
@@ -412,6 +413,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isReadingTimeEstimationEnabled: Boolean val isReadingTimeEstimationEnabled: Boolean
get() = prefs.getBoolean(KEY_READING_TIME, true) get() = prefs.getBoolean(KEY_READING_TIME, true)
val isPagesSavingAskEnabled: Boolean
get() = prefs.getBoolean(KEY_PAGES_SAVE_ASK, true)
fun isTipEnabled(tip: String): Boolean { fun isTipEnabled(tip: String): Boolean {
return prefs.getStringSet(KEY_TIPS_CLOSED, emptySet())?.contains(tip) != true 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) } 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) { fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
prefs.registerOnSharedPreferenceChangeListener(listener) prefs.registerOnSharedPreferenceChangeListener(listener)
} }
@@ -591,6 +604,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_IGNORE_DOZE = "ignore_dose" const val KEY_IGNORE_DOZE = "ignore_dose"
const val KEY_DETAILS_TAB = "details_tab" const val KEY_DETAILS_TAB = "details_tab"
const val KEY_READING_TIME = "reading_time" 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 // About
const val KEY_APP_UPDATE = "app_update" const val KEY_APP_UPDATE = "app_update"

View File

@@ -8,19 +8,26 @@ import android.provider.DocumentsContract
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.net.toUri import androidx.core.net.toUri
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import java.io.File
class PageSaveContract : ActivityResultContracts.CreateDocument("image/*") { class PageSaveContract : ActivityResultContracts.CreateDocument("image/*") {
override fun createIntent(context: Context, input: String): Intent { 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() intent.type = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(input.substringAfterLast('.')) ?: "image/*" .getMimeTypeFromExtension(input.substringAfterLast('.')) ?: "image/*"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val defaultUri = input.toUriOrNull()?.run {
path?.let { p ->
buildUpon().path(p.substringBeforeLast('/')).build()
}
}
intent.putExtra( intent.putExtra(
DocumentsContract.EXTRA_INITIAL_URI, DocumentsContract.EXTRA_INITIAL_URI,
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toUri(), defaultUri ?: Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toUri(),
) )
} }
return intent return intent
} }
} }

View File

@@ -15,6 +15,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import okio.IOException import okio.IOException
import okio.buffer import okio.buffer
import okio.sink 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.source
import org.koitharu.kotatsu.core.util.ext.toFileOrNull import org.koitharu.kotatsu.core.util.ext.toFileOrNull
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable 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" private const val EXTENSION_FALLBACK = "png"
class PageSaveHelper @Inject constructor( class PageSaveHelper @Inject constructor(
@ApplicationContext context: Context, @ApplicationContext private val context: Context,
private val settings: AppSettings,
) { ) {
private var continuation: Continuation<Uri>? = null private var continuation: Continuation<Uri>? = null
@@ -44,14 +46,7 @@ class PageSaveHelper @Inject constructor(
val pageUrl = pageLoader.getPageUrl(page) val pageUrl = pageLoader.getPageUrl(page)
val pageUri = pageLoader.loadPage(page, force = false) val pageUri = pageLoader.loadPage(page, force = false)
val proposedName = getProposedFileName(pageUrl, pageUri) val proposedName = getProposedFileName(pageUrl, pageUri)
val destination = withContext(Dispatchers.Main) { val destination = getDefaultFileUri(proposedName) ?: pickFileUri(saveLauncher, proposedName)
suspendCancellableCoroutine { cont ->
continuation = cont
saveLauncher.launch(proposedName)
}.also {
continuation = null
}
}
runInterruptible(Dispatchers.IO) { runInterruptible(Dispatchers.IO) {
contentResolver.openOutputStream(destination)?.sink()?.buffer() contentResolver.openOutputStream(destination)?.sink()?.buffer()
}?.use { output -> }?.use { output ->
@@ -62,12 +57,35 @@ class PageSaveHelper @Inject constructor(
return destination 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 { fun onActivityResult(uri: Uri): Boolean = continuation?.apply {
resume(uri) resume(uri)
} != null } != null
private suspend fun getProposedFileName(url: String, fileUri: Uri): String { 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) requireNotNull(url.toUri().fragment)
} else { } else {
url.toHttpUrl().pathSegments.last() url.toHttpUrl().pathSegments.last()

View File

@@ -117,6 +117,7 @@ class ReaderConfigSheet :
R.id.button_save_page -> { R.id.button_save_page -> {
val page = viewModel.getCurrentPage() ?: return val page = viewModel.getCurrentPage() ?: return
viewModel.saveCurrentPage(page, savePageRequest) viewModel.saveCurrentPage(page, savePageRequest)
dismissAllowingStateLoss()
} }
R.id.button_screen_rotate -> { R.id.button_screen_rotate -> {

View File

@@ -1,9 +1,14 @@
package org.koitharu.kotatsu.settings package org.koitharu.kotatsu.settings
import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile
import androidx.preference.Preference import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch 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.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug 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.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.LocalStorageManager
@@ -25,7 +32,7 @@ class DownloadsSettingsFragment :
BasePreferenceFragment(R.string.downloads), BasePreferenceFragment(R.string.downloads),
SharedPreferences.OnSharedPreferenceChangeListener { SharedPreferences.OnSharedPreferenceChangeListener {
private val dozeHelper = DozeHelper(this) private val dozeHelper = DozeHelper(this)
@Inject @Inject
lateinit var storageManager: LocalStorageManager lateinit var storageManager: LocalStorageManager
@@ -33,6 +40,10 @@ class DownloadsSettingsFragment :
@Inject @Inject
lateinit var downloadsScheduler: DownloadWorker.Scheduler lateinit var downloadsScheduler: DownloadWorker.Scheduler
private val pickFileTreeLauncher = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
if (it != null) onDirectoryPicked(it)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_downloads) addPreferencesFromResource(R.xml.pref_downloads)
dozeHelper.updatePreference() dozeHelper.updatePreference()
@@ -42,6 +53,7 @@ class DownloadsSettingsFragment :
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
findPreference<Preference>(AppSettings.KEY_LOCAL_STORAGE)?.bindStorageName() findPreference<Preference>(AppSettings.KEY_LOCAL_STORAGE)?.bindStorageName()
findPreference<Preference>(AppSettings.KEY_LOCAL_MANGA_DIRS)?.bindDirectoriesCount() findPreference<Preference>(AppSettings.KEY_LOCAL_MANGA_DIRS)?.bindDirectoriesCount()
findPreference<Preference>(AppSettings.KEY_PAGES_SAVE_DIR)?.bindPagesDirectory()
settings.subscribe(this) settings.subscribe(this)
} }
@@ -63,6 +75,10 @@ class DownloadsSettingsFragment :
AppSettings.KEY_DOWNLOADS_WIFI -> { AppSettings.KEY_DOWNLOADS_WIFI -> {
updateDownloadsConstraints() updateDownloadsConstraints()
} }
AppSettings.KEY_PAGES_SAVE_DIR -> {
findPreference<Preference>(AppSettings.KEY_PAGES_SAVE_DIR)?.bindPagesDirectory()
}
} }
} }
@@ -82,10 +98,27 @@ class DownloadsSettingsFragment :
dozeHelper.startIgnoreDoseActivity() 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) 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() { private fun Preference.bindStorageName() {
viewLifecycleScope.launch { viewLifecycleScope.launch {
val storage = storageManager.getDefaultWriteableDir() 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() { private fun updateDownloadsConstraints() {
val preference = findPreference<Preference>(AppSettings.KEY_DOWNLOADS_WIFI) val preference = findPreference<Preference>(AppSettings.KEY_DOWNLOADS_WIFI)
viewLifecycleScope.launch { 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

View File

@@ -35,4 +35,18 @@
android:summary="@string/downloads_settings_info" android:summary="@string/downloads_settings_info"
app:allowDividerAbove="true" /> 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> </PreferenceScreen>