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.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"

View File

@@ -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
}
}
}

View File

@@ -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()

View File

@@ -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 -> {

View File

@@ -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

View File

@@ -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>