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.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"
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 -> {
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user