6.14.8 commit
This commit is contained in:
parent
6266412a48
commit
0e02b670f0
|
@ -26,8 +26,8 @@ android {
|
|||
vectorDrawables.useSupportLibrary false
|
||||
vectorDrawables.generatedDensities = []
|
||||
|
||||
versionCode 3020306
|
||||
versionName "6.14.7"
|
||||
versionCode 3020307
|
||||
versionName "6.14.8"
|
||||
|
||||
applicationId "ac.mdiq.podcini.R"
|
||||
def commit = ""
|
||||
|
|
|
@ -44,9 +44,7 @@ class PodciniApp : Application() {
|
|||
super.onCreate()
|
||||
ClientConfig.USER_AGENT = "Podcini/" + BuildConfig.VERSION_NAME
|
||||
ClientConfig.applicationCallbacks = ApplicationCallbacksImpl()
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler(CrashReportWriter())
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
val builder: StrictMode.VmPolicy.Builder = StrictMode.VmPolicy.Builder()
|
||||
.detectAll()
|
||||
|
@ -56,7 +54,6 @@ class PodciniApp : Application() {
|
|||
}
|
||||
|
||||
singleton = this
|
||||
|
||||
runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
ClientConfigurator.initialize(this@PodciniApp)
|
||||
|
|
|
@ -33,8 +33,9 @@ object NetworkUtils {
|
|||
setAllowMobileFor("auto_download", allow)
|
||||
}
|
||||
|
||||
// not using this
|
||||
val isEnableAutodownloadWifiFilter: Boolean
|
||||
get() = Build.VERSION.SDK_INT < 29 && appPrefs.getBoolean(UserPreferences.Prefs.prefEnableAutoDownloadWifiFilter.name, false)
|
||||
get() = false && Build.VERSION.SDK_INT < 29 && appPrefs.getBoolean(UserPreferences.Prefs.prefEnableAutoDownloadWifiFilter.name, false)
|
||||
|
||||
@JvmStatic
|
||||
val isAutoDownloadAllowed: Boolean
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
package ac.mdiq.podcini.preferences
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
import ac.mdiq.podcini.storage.utils.FilesUtils.getDataFolder
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.io.OutputStreamWriter
|
||||
import java.nio.charset.Charset
|
||||
|
||||
enum class ExportTypes(val contentType: String, val outputNameTemplate: String, @field:StringRes val labelResId: Int) {
|
||||
OPML("text/x-opml", "podcini-feeds-%s.opml", R.string.opml_export_label),
|
||||
OPML_SELECTED("text/x-opml", "podcini-feeds-selected-%s.opml", R.string.opml_export_label),
|
||||
HTML("text/html", "podcini-feeds-%s.html", R.string.html_export_label),
|
||||
FAVORITES("text/html", "podcini-favorites-%s.html", R.string.favorites_export_label),
|
||||
PROGRESS("text/x-json", "podcini-progress-%s.json", R.string.progress_export_label),
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an OPML file into the export directory in the background.
|
||||
*/
|
||||
class ExportWorker private constructor(private val exportWriter: ExportWriter, private val output: File, private val context: Context) {
|
||||
constructor(exportWriter: ExportWriter, context: Context) : this(exportWriter, File(getDataFolder(EXPORT_DIR),
|
||||
DEFAULT_OUTPUT_NAME + "." + exportWriter.fileExtension()), context)
|
||||
suspend fun exportFile(feeds: List<Feed>? = null): File? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
if (output.exists()) {
|
||||
val success = output.delete()
|
||||
Logd(TAG, "Overwriting previously exported file: $success")
|
||||
}
|
||||
var writer: OutputStreamWriter? = null
|
||||
try {
|
||||
writer = OutputStreamWriter(FileOutputStream(output), Charset.forName("UTF-8"))
|
||||
val feeds_ = feeds ?: getFeedList()
|
||||
Logd(TAG, "feeds_: ${feeds_.size}")
|
||||
exportWriter.writeDocument(feeds_, writer, context)
|
||||
output // return the output file
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error during file export", e)
|
||||
null // return null in case of error
|
||||
} finally { writer?.close() }
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
private const val EXPORT_DIR = "export/"
|
||||
private val TAG: String = ExportWorker::class.simpleName ?: "Anonymous"
|
||||
private const val DEFAULT_OUTPUT_NAME = "podcini-feeds"
|
||||
}
|
||||
}
|
||||
|
||||
class DocumentFileExportWorker(private val exportWriter: ExportWriter, private val context: Context, private val outputFileUri: Uri) {
|
||||
suspend fun exportFile(feeds: List<Feed>? = null): DocumentFile {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val output = DocumentFile.fromSingleUri(context, outputFileUri)
|
||||
var outputStream: OutputStream? = null
|
||||
var writer: OutputStreamWriter? = null
|
||||
try {
|
||||
if (output == null) throw IOException()
|
||||
val uri = output.uri
|
||||
outputStream = context.contentResolver.openOutputStream(uri, "wt")
|
||||
if (outputStream == null) throw IOException()
|
||||
writer = OutputStreamWriter(outputStream, Charset.forName("UTF-8"))
|
||||
val feeds_ = feeds ?: getFeedList()
|
||||
Logd("DocumentFileExportWorker", "feeds_: ${feeds_.size}")
|
||||
exportWriter.writeDocument(feeds_, writer, context)
|
||||
output
|
||||
} catch (e: IOException) { throw e
|
||||
} finally {
|
||||
if (writer != null) try { writer.close() } catch (e: IOException) { throw e }
|
||||
if (outputStream != null) try { outputStream.close() } catch (e: IOException) { throw e }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
package ac.mdiq.podcini.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.util.AttributeSet
|
||||
import android.widget.TextView
|
||||
import androidx.preference.PreferenceViewHolder
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr
|
||||
|
||||
class MasterSwitchPreference : SwitchPreferenceCompat {
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(
|
||||
context, attrs, defStyleAttr, defStyleRes)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
super.onBindViewHolder(holder)
|
||||
|
||||
holder.itemView.setBackgroundColor(getColorFromAttr(context, com.google.android.material.R.attr.colorSurfaceVariant))
|
||||
val title = holder.findViewById(android.R.id.title) as? TextView
|
||||
title?.setTypeface(title.typeface, Typeface.BOLD)
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
package ac.mdiq.podcini.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.util.AttributeSet
|
||||
import androidx.preference.ListPreference
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
||||
class MaterialListPreference : ListPreference {
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
|
||||
override fun onClick() {
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
builder.setTitle(title)
|
||||
builder.setIcon(dialogIcon)
|
||||
builder.setNegativeButton(negativeButtonText, null)
|
||||
|
||||
val values = entryValues
|
||||
var selected = -1
|
||||
for (i in values.indices) {
|
||||
if (values[i].toString() == value) {
|
||||
selected = i
|
||||
break
|
||||
}
|
||||
}
|
||||
builder.setSingleChoiceItems(entries, selected) { dialog: DialogInterface, which: Int ->
|
||||
dialog.dismiss()
|
||||
if (which >= 0 && entryValues != null) {
|
||||
val value = entryValues[which].toString()
|
||||
if (callChangeListener(value)) setValue(value)
|
||||
}
|
||||
}
|
||||
builder.show()
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package ac.mdiq.podcini.preferences
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.util.AttributeSet
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
||||
class MaterialMultiSelectListPreference : MultiSelectListPreference {
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
|
||||
override fun onClick() {
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
builder.setTitle(title)
|
||||
builder.setIcon(dialogIcon)
|
||||
builder.setNegativeButton(negativeButtonText, null)
|
||||
|
||||
val selected = BooleanArray(entries.size)
|
||||
val values = entryValues
|
||||
for (i in values.indices) {
|
||||
selected[i] = getValues().contains(values[i].toString())
|
||||
}
|
||||
builder.setMultiChoiceItems(entries, selected) { _: DialogInterface?, which: Int, isChecked: Boolean -> selected[which] = isChecked }
|
||||
builder.setPositiveButton("OK") { _: DialogInterface?, _: Int ->
|
||||
val selectedValues: MutableSet<String> = HashSet()
|
||||
for (i in values.indices) {
|
||||
if (selected[i]) selectedValues.add(entryValues[i].toString())
|
||||
}
|
||||
setValues(selectedValues)
|
||||
}
|
||||
builder.show()
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package ac.mdiq.podcini.preferences
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.ThemePreferenceBinding
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceViewHolder
|
||||
import com.google.android.material.elevation.SurfaceColors
|
||||
|
||||
class ThemePreference : Preference {
|
||||
var binding: ThemePreferenceBinding? = null
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
layoutResource = R.layout.theme_preference
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
|
||||
layoutResource = R.layout.theme_preference
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
super.onBindViewHolder(holder)
|
||||
binding = ThemePreferenceBinding.bind(holder.itemView)
|
||||
updateUi()
|
||||
}
|
||||
|
||||
fun updateThemeCard(card: CardView, theme: UserPreferences.ThemePreference) {
|
||||
val density = context.resources.displayMetrics.density
|
||||
val surfaceColor = SurfaceColors.getColorForElevation(context, 1 * density)
|
||||
val surfaceColorActive = SurfaceColors.getColorForElevation(context, 32 * density)
|
||||
val activeTheme = UserPreferences.theme
|
||||
card.setCardBackgroundColor(if (theme == activeTheme) surfaceColorActive else surfaceColor)
|
||||
card.setOnClickListener {
|
||||
UserPreferences.theme = theme
|
||||
onPreferenceChangeListener?.onPreferenceChange(this, UserPreferences.theme)
|
||||
updateUi()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateUi() {
|
||||
updateThemeCard(binding!!.themeSystemCard, UserPreferences.ThemePreference.SYSTEM)
|
||||
updateThemeCard(binding!!.themeLightCard, UserPreferences.ThemePreference.LIGHT)
|
||||
updateThemeCard(binding!!.themeDarkCard, UserPreferences.ThemePreference.DARK)
|
||||
}
|
||||
}
|
|
@ -26,7 +26,6 @@ object UsageStatistics {
|
|||
|
||||
/**
|
||||
* Sets up the UsageStatistics class.
|
||||
*
|
||||
* @throws IllegalArgumentException if context is null
|
||||
*/
|
||||
@JvmStatic
|
||||
|
|
|
@ -23,13 +23,7 @@ import java.net.Proxy
|
|||
object UserPreferences {
|
||||
private val TAG: String = UserPreferences::class.simpleName ?: "Anonymous"
|
||||
|
||||
// Experimental
|
||||
const val EPISODE_CLEANUP_QUEUE: Int = -1
|
||||
const val EPISODE_CLEANUP_NULL: Int = -2
|
||||
const val EPISODE_CLEANUP_EXCEPT_FAVORITE: Int = -3
|
||||
const val EPISODE_CLEANUP_DEFAULT: Int = 0
|
||||
|
||||
const val EPISODE_CACHE_SIZE_UNLIMITED: Int = -1
|
||||
const val EPISODE_CACHE_SIZE_UNLIMITED: Int = 0
|
||||
|
||||
const val DEFAULT_PAGE_REMEMBER: String = "remember"
|
||||
|
||||
|
@ -104,8 +98,7 @@ object UserPreferences {
|
|||
|
||||
/**
|
||||
* Returns the capacity of the episode cache. This method will return the
|
||||
* negative integer EPISODE_CACHE_SIZE_UNLIMITED if the cache size is set to
|
||||
* 'unlimited'.
|
||||
* EPISODE_CACHE_SIZE_UNLIMITED (0) if the cache size is set to 'unlimited'.
|
||||
*/
|
||||
val episodeCacheSize: Int
|
||||
get() = appPrefs.getString(Prefs.prefEpisodeCacheSize.name, "20")!!.toInt()
|
||||
|
|
|
@ -1,168 +0,0 @@
|
|||
package ac.mdiq.podcini.preferences.fragments
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.autodownloadSelectedNetworks
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isEnableAutodownloadWifiFilter
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
|
||||
class AutoDownloadPreferencesFragment : PreferenceFragmentCompat() {
|
||||
private var selectedNetworks: Array<CheckBoxPreference?>? = null
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.preferences_autodownload)
|
||||
|
||||
setupAutoDownloadScreen()
|
||||
buildAutodownloadSelectedNetworksPreference()
|
||||
setSelectedNetworksEnabled(isEnableAutodownloadWifiFilter)
|
||||
buildEpisodeCleanupPreference()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
(activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.pref_automatic_download_title)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
checkAutodownloadItemVisibility(isEnableAutodownload)
|
||||
}
|
||||
|
||||
private fun setupAutoDownloadScreen() {
|
||||
findPreference<Preference>(UserPreferences.Prefs.prefEnableAutoDl.name)!!.onPreferenceChangeListener =
|
||||
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? ->
|
||||
if (newValue is Boolean) checkAutodownloadItemVisibility(newValue)
|
||||
true
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 29) findPreference<Preference>(UserPreferences.Prefs.prefEnableAutoDownloadWifiFilter.name)!!.isVisible = false
|
||||
findPreference<Preference>(UserPreferences.Prefs.prefEnableAutoDownloadWifiFilter.name)?.onPreferenceChangeListener =
|
||||
Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? ->
|
||||
if (newValue is Boolean) {
|
||||
setSelectedNetworksEnabled(newValue)
|
||||
return@OnPreferenceChangeListener true
|
||||
} else return@OnPreferenceChangeListener false
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkAutodownloadItemVisibility(autoDownload: Boolean) {
|
||||
findPreference<Preference>(UserPreferences.Prefs.prefEpisodeCacheSize.name)!!.isEnabled = autoDownload
|
||||
findPreference<Preference>(UserPreferences.Prefs.prefEnableAutoDownloadOnBattery.name)!!.isEnabled = autoDownload
|
||||
findPreference<Preference>(UserPreferences.Prefs.prefEnableAutoDownloadWifiFilter.name)!!.isEnabled = autoDownload
|
||||
findPreference<Preference>(UserPreferences.Prefs.prefEpisodeCleanup.name)!!.isEnabled = autoDownload
|
||||
setSelectedNetworksEnabled(autoDownload && isEnableAutodownloadWifiFilter)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission") // getConfiguredNetworks needs location permission starting with API 29
|
||||
private fun buildAutodownloadSelectedNetworksPreference() {
|
||||
if (Build.VERSION.SDK_INT >= 29) return
|
||||
val activity: Activity? = activity
|
||||
|
||||
if (selectedNetworks != null) clearAutodownloadSelectedNetworsPreference()
|
||||
|
||||
// get configured networks
|
||||
val wifiservice = activity!!.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||
val networks = wifiservice.configuredNetworks
|
||||
|
||||
if (networks == null) {
|
||||
Log.e(TAG, "Couldn't get list of configure Wi-Fi networks")
|
||||
return
|
||||
}
|
||||
networks.sortWith { x: WifiConfiguration, y: WifiConfiguration -> blankIfNull(x.SSID).compareTo(blankIfNull(y.SSID), ignoreCase = true) }
|
||||
selectedNetworks = arrayOfNulls(networks.size)
|
||||
val prefValues = listOf(*autodownloadSelectedNetworks)
|
||||
val prefScreen = preferenceScreen
|
||||
val clickListener = Preference.OnPreferenceClickListener { preference: Preference ->
|
||||
if (preference is CheckBoxPreference) {
|
||||
val key = preference.getKey()
|
||||
val prefValuesList: MutableList<String?> = ArrayList(listOf(*autodownloadSelectedNetworks))
|
||||
val newValue = preference.isChecked
|
||||
Logd(TAG, "Selected network $key. New state: $newValue")
|
||||
|
||||
val index = prefValuesList.indexOf(key)
|
||||
when {
|
||||
// remove network
|
||||
index >= 0 && !newValue -> prefValuesList.removeAt(index)
|
||||
index < 0 && newValue -> prefValuesList.add(key)
|
||||
}
|
||||
|
||||
setAutodownloadSelectedNetworks(prefValuesList.toTypedArray<String?>())
|
||||
return@OnPreferenceClickListener true
|
||||
} else return@OnPreferenceClickListener false
|
||||
}
|
||||
// create preference for each known network. attach listener and set
|
||||
// value
|
||||
for (i in networks.indices) {
|
||||
val config = networks[i]
|
||||
|
||||
val pref = CheckBoxPreference(activity)
|
||||
val key = config.networkId.toString()
|
||||
pref.title = config.SSID
|
||||
pref.key = key
|
||||
pref.onPreferenceClickListener = clickListener
|
||||
pref.isPersistent = false
|
||||
pref.isChecked = prefValues.contains(key)
|
||||
selectedNetworks!![i] = pref
|
||||
prefScreen.addPreference(pref)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setAutodownloadSelectedNetworks(value: Array<String?>?) {
|
||||
appPrefs.edit().putString(UserPreferences.Prefs.prefAutodownloadSelectedNetworks.name, value!!.joinToString()).apply()
|
||||
}
|
||||
|
||||
private fun clearAutodownloadSelectedNetworsPreference() {
|
||||
if (selectedNetworks != null) {
|
||||
val prefScreen = preferenceScreen
|
||||
for (network in selectedNetworks!!) if (network != null) prefScreen.removePreference(network)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildEpisodeCleanupPreference() {
|
||||
val res = requireActivity().resources
|
||||
|
||||
val pref = findPreference<ListPreference>(UserPreferences.Prefs.prefEpisodeCleanup.name)
|
||||
val values = res.getStringArray(R.array.episode_cleanup_values)
|
||||
val entries = arrayOfNulls<String>(values.size)
|
||||
for (x in values.indices) {
|
||||
when (val v = values[x].toInt()) {
|
||||
UserPreferences.EPISODE_CLEANUP_EXCEPT_FAVORITE -> entries[x] = res.getString(R.string.episode_cleanup_except_favorite_removal)
|
||||
UserPreferences.EPISODE_CLEANUP_QUEUE -> entries[x] = res.getString(R.string.episode_cleanup_queue_removal)
|
||||
UserPreferences.EPISODE_CLEANUP_NULL -> entries[x] = res.getString(R.string.episode_cleanup_never)
|
||||
0 -> entries[x] = res.getString(R.string.episode_cleanup_after_listening)
|
||||
in 1..23 -> entries[x] = res.getQuantityString(R.plurals.episode_cleanup_hours_after_listening, v, v)
|
||||
else -> {
|
||||
val numDays = v / 24 // assume underlying value will be NOT fraction of days, e.g., 36 (hours)
|
||||
entries[x] = res.getQuantityString(R.plurals.episode_cleanup_days_after_listening, numDays, numDays)
|
||||
}
|
||||
}
|
||||
}
|
||||
pref!!.entries = entries
|
||||
}
|
||||
|
||||
private fun setSelectedNetworksEnabled(b: Boolean) {
|
||||
if (selectedNetworks != null) for (p in selectedNetworks!!) p!!.isEnabled = b
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = AutoDownloadPreferencesFragment::class.simpleName ?: "Anonymous"
|
||||
|
||||
private fun blankIfNull(`val`: String?): String {
|
||||
return `val` ?: ""
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,416 +0,0 @@
|
|||
package ac.mdiq.podcini.preferences.fragments
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.ProxySettingsBinding
|
||||
import ac.mdiq.podcini.net.download.service.PodciniHttpClient
|
||||
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.newBuilder
|
||||
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.reinit
|
||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager.restartUpdateAlarm
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.proxyConfig
|
||||
import ac.mdiq.podcini.storage.model.ProxyConfig
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.Patterns
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.TwoStatePreference
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Credentials.basic
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Request.Builder
|
||||
import okhttp3.Response
|
||||
import okhttp3.Route
|
||||
import java.io.IOException
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.net.SocketAddress
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class DownloadsPreferencesFragment : PreferenceFragmentCompat() {
|
||||
private var blockAutoDeleteLocal = true
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
// addPreferencesFromResource(R.xml.preferences_downloads)
|
||||
// setupNetworkScreen()
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
(activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.downloads_pref)
|
||||
return ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
val scrollState = rememberScrollState()
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(16.dp).verticalScroll(scrollState)) {
|
||||
Text(stringResource(R.string.automation), color = textColor, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(stringResource(R.string.feed_refresh_title), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f))
|
||||
var interval by remember { mutableStateOf(appPrefs.getString(UserPreferences.Prefs.prefAutoUpdateIntervall.name, "12")!!) }
|
||||
TextField(value = interval, onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) interval = it },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), label = { Text("(hours)") },
|
||||
singleLine = true, modifier = Modifier.weight(0.5f),
|
||||
trailingIcon = {
|
||||
Icon(imageVector = Icons.Filled.Settings, contentDescription = "Settings icon",
|
||||
modifier = Modifier.size(30.dp).padding(start = 10.dp).clickable(onClick = {
|
||||
if (interval.isEmpty()) interval = "0"
|
||||
appPrefs.edit().putString(UserPreferences.Prefs.prefAutoUpdateIntervall.name, interval).apply()
|
||||
restartUpdateAlarm(requireContext(), true)
|
||||
}))
|
||||
})
|
||||
}
|
||||
Text(stringResource(R.string.feed_refresh_sum), color = textColor)
|
||||
}
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp).clickable(onClick = {
|
||||
(activity as PreferenceActivity).openScreen(R.xml.preferences_autodownload)
|
||||
})) {
|
||||
Text(stringResource(R.string.pref_automatic_download_title), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.pref_automatic_download_sum), color = textColor)
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp)) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(stringResource(R.string.pref_auto_delete_title), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.pref_auto_delete_sum), color = textColor)
|
||||
}
|
||||
Switch(checked = false, onCheckedChange = { appPrefs.edit().putBoolean(UserPreferences.Prefs.prefAutoDelete.name, it).apply() })
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp)) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(stringResource(R.string.pref_auto_local_delete_title), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.pref_auto_local_delete_sum), color = textColor)
|
||||
}
|
||||
Switch(checked = false, onCheckedChange = {
|
||||
if (blockAutoDeleteLocal && it) {
|
||||
// showAutoDeleteEnableDialog()
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(R.string.pref_auto_local_delete_dialog_body)
|
||||
.setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int ->
|
||||
blockAutoDeleteLocal = false
|
||||
(findPreference<Preference>(Prefs.prefAutoDeleteLocal.name) as TwoStatePreference?)!!.isChecked = true
|
||||
blockAutoDeleteLocal = true
|
||||
}
|
||||
.setNegativeButton(R.string.cancel_label, null)
|
||||
.show()
|
||||
}
|
||||
})
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp)) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(stringResource(R.string.pref_keeps_important_episodes_title), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.pref_keeps_important_episodes_sum), color = textColor)
|
||||
}
|
||||
Switch(checked = true, onCheckedChange = { appPrefs.edit().putBoolean(UserPreferences.Prefs.prefFavoriteKeepsEpisode.name, it).apply() })
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp)) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(stringResource(R.string.pref_delete_removes_from_queue_title), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.pref_delete_removes_from_queue_sum), color = textColor)
|
||||
}
|
||||
Switch(checked = true, onCheckedChange = { appPrefs.edit().putBoolean(UserPreferences.Prefs.prefDeleteRemovesFromQueue.name, it).apply() })
|
||||
}
|
||||
Text(stringResource(R.string.download_pref_details), color = textColor, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(top = 10.dp))
|
||||
var showMeteredNetworkOptions by remember { mutableStateOf(false) }
|
||||
var tempSelectedOptions by remember { mutableStateOf(appPrefs.getStringSet(UserPreferences.Prefs.prefMobileUpdateTypes.name, setOf("images"))!!) }
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp).clickable(onClick = { showMeteredNetworkOptions = true })) {
|
||||
Text(stringResource(R.string.pref_metered_network_title), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.pref_mobileUpdate_sum), color = textColor)
|
||||
}
|
||||
if (showMeteredNetworkOptions) {
|
||||
AlertDialog(onDismissRequest = { showMeteredNetworkOptions = false },
|
||||
title = { Text(stringResource(R.string.pref_metered_network_title), style = MaterialTheme.typography.headlineSmall) },
|
||||
text = {
|
||||
Column {
|
||||
MobileUpdateOptions.entries.forEach { option ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(5.dp)
|
||||
.clickable {
|
||||
tempSelectedOptions = if (tempSelectedOptions.contains(option.name)) tempSelectedOptions - option.name
|
||||
else tempSelectedOptions + option.name
|
||||
}) {
|
||||
Checkbox(checked = tempSelectedOptions.contains(option.name),
|
||||
onCheckedChange = {
|
||||
tempSelectedOptions = if (tempSelectedOptions.contains(option.name)) tempSelectedOptions - option.name
|
||||
else tempSelectedOptions + option.name
|
||||
})
|
||||
Text(stringResource(option.res), modifier = Modifier.padding(start = 16.dp), style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
appPrefs.edit().putStringSet(UserPreferences.Prefs.prefMobileUpdateTypes.name, tempSelectedOptions).apply()
|
||||
showMeteredNetworkOptions = false
|
||||
}) { Text(text = "OK") }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { showMeteredNetworkOptions = false }) { Text(text = "Cancel") } }
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp).clickable(onClick = {
|
||||
ProxyDialog(requireContext()).show()
|
||||
})) {
|
||||
Text(stringResource(R.string.pref_proxy_title), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.pref_proxy_sum), color = textColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class MobileUpdateOptions(val res: Int) {
|
||||
feed_refresh(R.string.pref_mobileUpdate_refresh),
|
||||
episode_download(R.string.pref_mobileUpdate_episode_download),
|
||||
auto_download(R.string.pref_mobileUpdate_auto_download),
|
||||
streaming(R.string.pref_mobileUpdate_streaming),
|
||||
images(R.string.pref_mobileUpdate_images),
|
||||
sync(R.string.synchronization_pref);
|
||||
}
|
||||
|
||||
class ProxyDialog(private val context: Context) {
|
||||
private lateinit var dialog: AlertDialog
|
||||
private lateinit var spType: Spinner
|
||||
private lateinit var etHost: EditText
|
||||
private lateinit var etPort: EditText
|
||||
private lateinit var etUsername: EditText
|
||||
private lateinit var etPassword: EditText
|
||||
private lateinit var txtvMessage: TextView
|
||||
private var testSuccessful = false
|
||||
private val port: Int
|
||||
get() {
|
||||
val port = etPort.text.toString()
|
||||
if (port.isNotEmpty()) try { return port.toInt() } catch (e: NumberFormatException) { }
|
||||
return 0
|
||||
}
|
||||
|
||||
fun show(): Dialog {
|
||||
val content = View.inflate(context, R.layout.proxy_settings, null)
|
||||
val binding = ProxySettingsBinding.bind(content)
|
||||
spType = binding.spType
|
||||
dialog = MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.pref_proxy_title)
|
||||
.setView(content)
|
||||
.setNegativeButton(R.string.cancel_label, null)
|
||||
.setPositiveButton(R.string.proxy_test_label, null)
|
||||
.setNeutralButton(R.string.reset, null)
|
||||
.show()
|
||||
// To prevent cancelling the dialog on button click
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener {
|
||||
if (!testSuccessful) {
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
|
||||
test()
|
||||
return@setOnClickListener
|
||||
}
|
||||
setProxyConfig()
|
||||
reinit()
|
||||
dialog.dismiss()
|
||||
}
|
||||
dialog.getButton(AlertDialog.BUTTON_NEUTRAL)?.setOnClickListener {
|
||||
etHost.text.clear()
|
||||
etPort.text.clear()
|
||||
etUsername.text.clear()
|
||||
etPassword.text.clear()
|
||||
setProxyConfig()
|
||||
}
|
||||
val types: MutableList<String> = ArrayList()
|
||||
types.add(Proxy.Type.DIRECT.name)
|
||||
types.add(Proxy.Type.HTTP.name)
|
||||
types.add(Proxy.Type.SOCKS.name)
|
||||
val adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, types)
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
spType.setAdapter(adapter)
|
||||
val proxyConfig = proxyConfig
|
||||
spType.setSelection(adapter.getPosition(proxyConfig.type.name))
|
||||
etHost = binding.etHost
|
||||
if (!proxyConfig.host.isNullOrEmpty()) etHost.setText(proxyConfig.host)
|
||||
etHost.addTextChangedListener(requireTestOnChange)
|
||||
etPort = binding.etPort
|
||||
if (proxyConfig.port > 0) etPort.setText(proxyConfig.port.toString())
|
||||
etPort.addTextChangedListener(requireTestOnChange)
|
||||
etUsername = binding.etUsername
|
||||
if (!proxyConfig.username.isNullOrEmpty()) etUsername.setText(proxyConfig.username)
|
||||
etUsername.addTextChangedListener(requireTestOnChange)
|
||||
etPassword = binding.etPassword
|
||||
if (!proxyConfig.password.isNullOrEmpty()) etPassword.setText(proxyConfig.password)
|
||||
etPassword.addTextChangedListener(requireTestOnChange)
|
||||
if (proxyConfig.type == Proxy.Type.DIRECT) {
|
||||
enableSettings(false)
|
||||
setTestRequired(false)
|
||||
}
|
||||
spType.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
|
||||
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).visibility = if (position == 0) View.GONE else View.VISIBLE
|
||||
enableSettings(position > 0)
|
||||
setTestRequired(position > 0)
|
||||
}
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||
enableSettings(false)
|
||||
}
|
||||
}
|
||||
txtvMessage = binding.txtvMessage
|
||||
checkValidity()
|
||||
return dialog
|
||||
}
|
||||
private fun setProxyConfig() {
|
||||
val type = spType.selectedItem as String
|
||||
val typeEnum = Proxy.Type.valueOf(type)
|
||||
val host = etHost.text.toString()
|
||||
val port = etPort.text.toString()
|
||||
var username: String? = etUsername.text.toString()
|
||||
if (username.isNullOrEmpty()) username = null
|
||||
var password: String? = etPassword.text.toString()
|
||||
if (password.isNullOrEmpty()) password = null
|
||||
var portValue = 0
|
||||
if (port.isNotEmpty()) portValue = port.toInt()
|
||||
val config = ProxyConfig(typeEnum, host, portValue, username, password)
|
||||
proxyConfig = config
|
||||
PodciniHttpClient.setProxyConfig(config)
|
||||
}
|
||||
private val requireTestOnChange: TextWatcher = object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
setTestRequired(true)
|
||||
}
|
||||
}
|
||||
private fun enableSettings(enable: Boolean) {
|
||||
etHost.isEnabled = enable
|
||||
etPort.isEnabled = enable
|
||||
etUsername.isEnabled = enable
|
||||
etPassword.isEnabled = enable
|
||||
}
|
||||
private fun checkValidity(): Boolean {
|
||||
var valid = true
|
||||
if (spType.selectedItemPosition > 0) valid = checkHost()
|
||||
valid = valid and checkPort()
|
||||
return valid
|
||||
}
|
||||
private fun checkHost(): Boolean {
|
||||
val host = etHost.text.toString()
|
||||
if (host.isEmpty()) {
|
||||
etHost.error = context.getString(R.string.proxy_host_empty_error)
|
||||
return false
|
||||
}
|
||||
if ("localhost" != host && !Patterns.DOMAIN_NAME.matcher(host).matches()) {
|
||||
etHost.error = context.getString(R.string.proxy_host_invalid_error)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
private fun checkPort(): Boolean {
|
||||
val port = port
|
||||
if (port < 0 || port > 65535) {
|
||||
etPort.error = context.getString(R.string.proxy_port_invalid_error)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
private fun setTestRequired(required: Boolean) {
|
||||
if (required) {
|
||||
testSuccessful = false
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.proxy_test_label)
|
||||
} else {
|
||||
testSuccessful = true
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(android.R.string.ok)
|
||||
}
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = true
|
||||
}
|
||||
private fun test() {
|
||||
if (!checkValidity()) {
|
||||
setTestRequired(true)
|
||||
return
|
||||
}
|
||||
val res = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
||||
val textColorPrimary = res.getColor(0, 0)
|
||||
res.recycle()
|
||||
val checking = context.getString(R.string.proxy_checking)
|
||||
txtvMessage.setTextColor(textColorPrimary)
|
||||
txtvMessage.text = "{faw_circle_o_notch spin} $checking"
|
||||
txtvMessage.visibility = View.VISIBLE
|
||||
val coroutineScope = CoroutineScope(Dispatchers.Main)
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val type = spType.selectedItem as String
|
||||
val host = etHost.text.toString()
|
||||
val port = etPort.text.toString()
|
||||
val username = etUsername.text.toString()
|
||||
val password = etPassword.text.toString()
|
||||
var portValue = 8080
|
||||
if (port.isNotEmpty()) portValue = port.toInt()
|
||||
val address: SocketAddress = InetSocketAddress.createUnresolved(host, portValue)
|
||||
val proxyType = Proxy.Type.valueOf(type.uppercase())
|
||||
val builder: OkHttpClient.Builder = newBuilder().connectTimeout(10, TimeUnit.SECONDS).proxy(Proxy(proxyType, address))
|
||||
if (username.isNotEmpty()) {
|
||||
builder.proxyAuthenticator { _: Route?, response: Response ->
|
||||
val credentials = basic(username, password)
|
||||
response.request.newBuilder().header("Proxy-Authorization", credentials).build()
|
||||
}
|
||||
}
|
||||
val client: OkHttpClient = builder.build()
|
||||
val request: Request = Builder().url("https://www.example.com").head().build()
|
||||
try {
|
||||
client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException(response.message) }
|
||||
} catch (e: IOException) { throw e }
|
||||
withContext(Dispatchers.Main) {
|
||||
txtvMessage.setTextColor(getColorFromAttr(context, R.attr.icon_green))
|
||||
val message = String.format("%s %s", "{faw_check}", context.getString(R.string.proxy_test_successful))
|
||||
txtvMessage.text = message
|
||||
setTestRequired(false)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
txtvMessage.setTextColor(getColorFromAttr(context, R.attr.icon_red))
|
||||
val message = String.format("%s %s: %s", "{faw_close}", context.getString(R.string.proxy_test_failed), e.message)
|
||||
txtvMessage.text = message
|
||||
setTestRequired(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("EnumEntryName")
|
||||
private enum class Prefs {
|
||||
prefAutoDownloadSettings,
|
||||
prefAutoDeleteLocal,
|
||||
prefProxy,
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,328 +0,0 @@
|
|||
package ac.mdiq.podcini.preferences.fragments
|
||||
|
||||
import ac.mdiq.podcini.BuildConfig
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.ui.activity.BugReportActivity
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity.Screens
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.BufferedReader
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
|
||||
class MainPreferencesFragment : PreferenceFragmentCompat() {
|
||||
|
||||
var copyrightNoticeText by mutableStateOf("")
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
(activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.settings_label)
|
||||
val packageHash = requireContext().packageName.hashCode()
|
||||
when {
|
||||
packageHash != 1329568231 && packageHash != 1297601420 -> {
|
||||
copyrightNoticeText = ("This application is based on Podcini."
|
||||
+ " The Podcini team does NOT provide support for this unofficial version."
|
||||
+ " If you can read this message, the developers of this modification"
|
||||
+ " violate the GNU General Public License (GPL).")
|
||||
}
|
||||
packageHash == 1297601420 -> copyrightNoticeText = "This is a development version of Podcini and not meant for daily use"
|
||||
}
|
||||
|
||||
return ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
val scrollState = rememberScrollState()
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(start = 10.dp, end = 10.dp).verticalScroll(scrollState)) {
|
||||
if (copyrightNoticeText.isNotBlank()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = Icons.Filled.Info, contentDescription = "", tint = Color.Red, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Text(copyrightNoticeText, color = textColor)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_appearance), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Column(modifier = Modifier.weight(1f).clickable(onClick = {
|
||||
(activity as PreferenceActivity).openScreen(R.xml.preferences_user_interface)
|
||||
})) {
|
||||
Text(stringResource(R.string.user_interface_label), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.user_interface_sum), color = textColor)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_play_24dp), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Column(modifier = Modifier.weight(1f).clickable(onClick = {
|
||||
(activity as PreferenceActivity).openScreen(R.xml.preferences_playback)
|
||||
})) {
|
||||
Text(stringResource(R.string.playback_pref), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.playback_pref_sum), color = textColor)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_download), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Column(modifier = Modifier.weight(1f).clickable(onClick = {
|
||||
(activity as PreferenceActivity).openScreen(Screens.preferences_downloads)
|
||||
})) {
|
||||
Text(stringResource(R.string.downloads_pref), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.downloads_pref_sum), color = textColor)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_cloud), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Column(modifier = Modifier.weight(1f).clickable(onClick = {
|
||||
(activity as PreferenceActivity).openScreen(R.xml.preferences_synchronization)
|
||||
})) {
|
||||
Text(stringResource(R.string.synchronization_pref), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.synchronization_sum), color = textColor)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_storage), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Column(modifier = Modifier.weight(1f).clickable(onClick = {
|
||||
(activity as PreferenceActivity).openScreen(Screens.preferences_import_export)
|
||||
})) {
|
||||
Text(stringResource(R.string.import_export_pref), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.import_export_summary), color = textColor)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_notifications), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Column(modifier = Modifier.weight(1f).clickable(onClick = {
|
||||
(activity as PreferenceActivity).openScreen(R.xml.preferences_notifications)
|
||||
})) {
|
||||
Text(stringResource(R.string.notification_pref_fragment), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(stringResource(R.string.pref_backup_on_google_title), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.pref_backup_on_google_sum), color = textColor)
|
||||
}
|
||||
Switch(checked = true, onCheckedChange = {
|
||||
appPrefs.edit().putBoolean(UserPreferences.Prefs.prefOPMLBackup.name, it).apply()
|
||||
// Restart the app
|
||||
val intent = context?.packageManager?.getLaunchIntentForPackage(requireContext().packageName)
|
||||
intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
context?.startActivity(intent)
|
||||
})
|
||||
}
|
||||
HorizontalDivider(modifier = Modifier.fillMaxWidth().height(1.dp).padding(top = 10.dp))
|
||||
Text(stringResource(R.string.project_pref), color = textColor, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, modifier = Modifier.padding(top = 15.dp))
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_questionmark), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Column(modifier = Modifier.weight(1f).clickable(onClick = {
|
||||
openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini")
|
||||
})) {
|
||||
Text(stringResource(R.string.documentation_support), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_chat), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Column(modifier = Modifier.weight(1f).clickable(onClick = {
|
||||
openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini/discussions")
|
||||
})) {
|
||||
Text(stringResource(R.string.visit_user_forum), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_contribute), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Column(modifier = Modifier.weight(1f).clickable(onClick = {
|
||||
openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini")
|
||||
})) {
|
||||
Text(stringResource(R.string.pref_contribute), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_bug), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Column(modifier = Modifier.weight(1f).clickable(onClick = {
|
||||
startActivity(Intent(activity, BugReportActivity::class.java))
|
||||
})) {
|
||||
Text(stringResource(R.string.bug_report_title), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_info), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Column(modifier = Modifier.weight(1f).clickable(onClick = {
|
||||
parentFragmentManager.beginTransaction().replace(R.id.settingsContainer, AboutFragment()).addToBackStack(getString(R.string.about_pref)).commit()
|
||||
})) {
|
||||
Text(stringResource(R.string.about_pref), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AboutFragment : PreferenceFragmentCompat() {
|
||||
@SuppressLint("CommitTransaction")
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
(activity as PreferenceActivity).supportActionBar?.setTitle(R.string.about_pref)
|
||||
return ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Column(modifier = Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp)) {
|
||||
Image(painter = painterResource(R.drawable.teaser), contentDescription = "")
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 10.dp, top = 5.dp, bottom = 5.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_star), contentDescription = "", tint = textColor)
|
||||
Column(Modifier.padding(start = 10.dp).clickable(onClick = {
|
||||
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText(getString(R.string.bug_report_title), findPreference<Preference>("about_version")!!.summary)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
if (Build.VERSION.SDK_INT <= 32) Snackbar.make(requireView(), R.string.copied_to_clipboard, Snackbar.LENGTH_SHORT).show()
|
||||
})) {
|
||||
Text(stringResource(R.string.podcini_version), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(String.format("%s (%s)", BuildConfig.VERSION_NAME, BuildConfig.COMMIT_HASH), color = textColor)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 10.dp, top = 5.dp, bottom = 5.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_questionmark), contentDescription = "", tint = textColor)
|
||||
Column(Modifier.padding(start = 10.dp).clickable(onClick = {
|
||||
openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini/")
|
||||
})) {
|
||||
Text(stringResource(R.string.online_help), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.online_help_sum), color = textColor)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 10.dp, top = 5.dp, bottom = 5.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_info), contentDescription = "", tint = textColor)
|
||||
Column(Modifier.padding(start = 10.dp).clickable(onClick = {
|
||||
openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini/blob/main/PrivacyPolicy.md")
|
||||
})) {
|
||||
Text(stringResource(R.string.privacy_policy), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text("Podcini PrivacyPolicy", color = textColor)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 10.dp, top = 5.dp, bottom = 5.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_info), contentDescription = "", tint = textColor)
|
||||
Column(Modifier.padding(start = 10.dp).clickable(onClick = {
|
||||
parentFragmentManager.beginTransaction().replace(R.id.settingsContainer, LicensesFragment()).addToBackStack(getString(R.string.translators)).commit()
|
||||
})) {
|
||||
Text(stringResource(R.string.licenses), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.licenses_summary), color = textColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LicensesFragment : Fragment() {
|
||||
private val licenses = mutableStateListOf<LicenseItem>()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { MainView() } } }
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
licenses.clear()
|
||||
val stream = requireContext().assets.open("licenses.xml")
|
||||
val docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
||||
val libraryList = docBuilder.parse(stream).getElementsByTagName("library")
|
||||
for (i in 0 until libraryList.length) {
|
||||
val lib = libraryList.item(i).attributes
|
||||
licenses.add(LicenseItem(lib.getNamedItem("name").textContent,
|
||||
String.format("By %s, %s license", lib.getNamedItem("author").textContent, lib.getNamedItem("license").textContent), lib.getNamedItem("website").textContent, lib.getNamedItem("licenseText").textContent))
|
||||
}
|
||||
}.invokeOnCompletion { throwable -> if (throwable!= null) Toast.makeText(context, throwable.message, Toast.LENGTH_LONG).show() }
|
||||
return composeView
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainView() {
|
||||
val lazyListState = rememberLazyListState()
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
var curLicenseIndex by remember { mutableIntStateOf(-1) }
|
||||
if (showDialog) Dialog(onDismissRequest = { showDialog = false }) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text(licenses[curLicenseIndex].title, color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Row {
|
||||
Button(onClick = { openInBrowser(requireContext(), licenses[curLicenseIndex].licenseUrl) }) { Text("View website") }
|
||||
Spacer(Modifier.weight(1f))
|
||||
Button(onClick = { showLicenseText(licenses[curLicenseIndex].licenseTextFile) }) { Text("View license") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LazyColumn(state = lazyListState, modifier = Modifier.fillMaxWidth().padding(start = 20.dp, end = 20.dp, top = 20.dp, bottom = 20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
itemsIndexed(licenses) { index, item ->
|
||||
Column(Modifier.clickable(onClick = {
|
||||
curLicenseIndex = index
|
||||
showDialog = true
|
||||
})) {
|
||||
Text(item.title, color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(item.subtitle, color = textColor, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showLicenseText(licenseTextFile: String) {
|
||||
try {
|
||||
val reader = BufferedReader(InputStreamReader(requireContext().assets.open(licenseTextFile), "UTF-8"))
|
||||
val licenseText = StringBuilder()
|
||||
var line = ""
|
||||
while ((reader.readLine()?.also { line = it }) != null) licenseText.append(line).append("\n")
|
||||
MaterialAlertDialogBuilder(requireContext()).setMessage(licenseText).show()
|
||||
} catch (e: IOException) { e.printStackTrace() }
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
(activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.licenses)
|
||||
}
|
||||
|
||||
private class LicenseItem(val title: String, val subtitle: String, val licenseUrl: String, val licenseTextFile: String)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package ac.mdiq.podcini.preferences.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
||||
import ac.mdiq.podcini.net.sync.SynchronizationSettings
|
||||
|
||||
class NotificationPreferencesFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.preferences_notifications)
|
||||
setUpScreen()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
(activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.notification_pref_fragment)
|
||||
}
|
||||
|
||||
private fun setUpScreen() {
|
||||
findPreference<Preference>(PREF_GPODNET_NOTIFICATIONS)!!.isEnabled = SynchronizationSettings.isProviderConnected
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_GPODNET_NOTIFICATIONS = "pref_gpodnet_notifications"
|
||||
}
|
||||
}
|
|
@ -1,184 +0,0 @@
|
|||
package ac.mdiq.podcini.preferences.fragments
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.prefPlaybackSpeed
|
||||
import ac.mdiq.podcini.preferences.UsageStatistics
|
||||
import ac.mdiq.podcini.preferences.UsageStatistics.doNotAskAgain
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.fallbackSpeed
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.setVideoMode
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.speedforwardSpeed
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.compose.PlaybackSpeedDialog
|
||||
import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.ViewGroup
|
||||
import androidx.collection.ArrayMap
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlin.math.round
|
||||
|
||||
class PlaybackPreferencesFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.preferences_playback)
|
||||
setupPlaybackScreen()
|
||||
// buildSmartMarkAsPlayedPreference()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
(activity as PreferenceActivity).supportActionBar?.setTitle(R.string.playback_pref)
|
||||
}
|
||||
|
||||
private fun setupPlaybackScreen() {
|
||||
findPreference<Preference>(Prefs.prefPlaybackSpeedLauncher.name)?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
val composeView = ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
val showDialog = remember { mutableStateOf(true) }
|
||||
CustomTheme(requireContext()) {
|
||||
PlaybackSpeedDialog(listOf(), initSpeed = prefPlaybackSpeed, maxSpeed = 3f, isGlobal = true, onDismiss = {
|
||||
showDialog.value = false
|
||||
(view as? ViewGroup)?.removeView(this@apply)
|
||||
}) { speed -> UserPreferences.setPlaybackSpeed(speed) }
|
||||
}
|
||||
}
|
||||
}
|
||||
(view as? ViewGroup)?.addView(composeView)
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(Prefs.prefPlaybackRewindDeltaLauncher.name)?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null)
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(Prefs.prefPlaybackVideoModeLauncher.name)?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
VideoModeDialog.showDialog(requireContext())
|
||||
true
|
||||
}
|
||||
|
||||
findPreference<Preference>(Prefs.prefPlaybackSpeedForwardLauncher.name)?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
val composeView = ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
val showDialog = remember { mutableStateOf(true) }
|
||||
CustomTheme(requireContext()) {
|
||||
PlaybackSpeedDialog(listOf(), initSpeed = speedforwardSpeed, maxSpeed = 10f, isGlobal = true, onDismiss = {
|
||||
showDialog.value = false
|
||||
(view as? ViewGroup)?.removeView(this@apply)
|
||||
}) { speed ->
|
||||
val speed_ = when {
|
||||
speed < 0.0f -> 0.0f
|
||||
speed > 10.0f -> 10.0f
|
||||
else -> 0f
|
||||
}
|
||||
speedforwardSpeed = round(10 * speed_) / 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(view as? ViewGroup)?.addView(composeView)
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(Prefs.prefPlaybackFallbackSpeedLauncher.name)?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
val composeView = ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
val showDialog = remember { mutableStateOf(true) }
|
||||
CustomTheme(requireContext()) {
|
||||
PlaybackSpeedDialog(listOf(), initSpeed = fallbackSpeed, maxSpeed = 3f, isGlobal = true, onDismiss = {
|
||||
showDialog.value = false
|
||||
(view as? ViewGroup)?.removeView(this@apply)
|
||||
}) { speed ->
|
||||
val speed_ = when {
|
||||
speed < 0.0f -> 0.0f
|
||||
speed > 3.0f -> 3.0f
|
||||
else -> 0f
|
||||
}
|
||||
fallbackSpeed = round(100 * speed_) / 100f
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(view as? ViewGroup)?.addView(composeView)
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(Prefs.prefPlaybackFastForwardDeltaLauncher.name)?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null)
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(Prefs.prefStreamOverDownload.name)?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, _: Any? ->
|
||||
// Update all visible lists to reflect new streaming action button
|
||||
// TODO: need another event type?
|
||||
EventFlow.postEvent(FlowEvent.EpisodePlayedEvent())
|
||||
// User consciously decided whether to prefer the streaming button, disable suggestion to change that
|
||||
doNotAskAgain(UsageStatistics.ACTION_STREAM)
|
||||
true
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
findPreference<Preference>(UserPreferences.Prefs.prefUnpauseOnHeadsetReconnect.name)?.isVisible = false
|
||||
findPreference<Preference>(UserPreferences.Prefs.prefUnpauseOnBluetoothReconnect.name)?.isVisible = false
|
||||
}
|
||||
buildEnqueueLocationPreference()
|
||||
}
|
||||
|
||||
private fun buildEnqueueLocationPreference() {
|
||||
val res = requireActivity().resources
|
||||
val options: MutableMap<String, String> = ArrayMap()
|
||||
run {
|
||||
val keys = res.getStringArray(R.array.enqueue_location_values)
|
||||
val values = res.getStringArray(R.array.enqueue_location_options)
|
||||
for (i in keys.indices) {
|
||||
options[keys[i]] = values[i]
|
||||
}
|
||||
}
|
||||
|
||||
val pref = requirePreference<ListPreference>(UserPreferences.Prefs.prefEnqueueLocation.name)
|
||||
pref.summary = res.getString(R.string.pref_enqueue_location_sum, options[pref.value])
|
||||
pref.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? ->
|
||||
if (newValue !is String) return@OnPreferenceChangeListener false
|
||||
pref.summary = res.getString(R.string.pref_enqueue_location_sum, options[newValue])
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : Preference?> requirePreference(key: CharSequence): T {
|
||||
// Possibly put it to a common method in abstract base class
|
||||
return findPreference<T>(key) ?: throw IllegalArgumentException("Preference with key '$key' is not found")
|
||||
}
|
||||
|
||||
object VideoModeDialog {
|
||||
fun showDialog(context: Context) {
|
||||
val dialog = MaterialAlertDialogBuilder(context)
|
||||
dialog.setTitle(context.getString(R.string.pref_playback_video_mode))
|
||||
dialog.setNegativeButton(android.R.string.cancel) { d: DialogInterface, _: Int -> d.dismiss() }
|
||||
val selected = videoPlayMode
|
||||
val entryValues = listOf(*context.resources.getStringArray(R.array.video_mode_options_values))
|
||||
val selectedIndex = entryValues.indexOf("" + selected)
|
||||
val items = context.resources.getStringArray(R.array.video_mode_options)
|
||||
dialog.setSingleChoiceItems(items, selectedIndex) { d: DialogInterface, which: Int ->
|
||||
if (selectedIndex != which) setVideoMode(entryValues[which].toInt())
|
||||
d.dismiss()
|
||||
}
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
private enum class Prefs {
|
||||
prefPlaybackSpeedLauncher,
|
||||
prefPlaybackRewindDeltaLauncher,
|
||||
prefPlaybackFallbackSpeedLauncher,
|
||||
prefPlaybackSpeedForwardLauncher,
|
||||
prefPlaybackFastForwardDeltaLauncher,
|
||||
prefStreamOverDownload,
|
||||
prefPlaybackVideoModeLauncher,
|
||||
}
|
||||
}
|
|
@ -1,700 +0,0 @@
|
|||
package ac.mdiq.podcini.preferences.fragments
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.*
|
||||
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
|
||||
import ac.mdiq.podcini.net.sync.SyncService
|
||||
import ac.mdiq.podcini.net.sync.SynchronizationCredentials
|
||||
import ac.mdiq.podcini.net.sync.SynchronizationProviderViewData
|
||||
import ac.mdiq.podcini.net.sync.SynchronizationSettings
|
||||
import ac.mdiq.podcini.net.sync.SynchronizationSettings.isProviderConnected
|
||||
import ac.mdiq.podcini.net.sync.SynchronizationSettings.setSelectedSyncProvider
|
||||
import ac.mdiq.podcini.net.sync.SynchronizationSettings.setWifiSyncEnabled
|
||||
import ac.mdiq.podcini.net.sync.SynchronizationSettings.wifiSyncEnabledKey
|
||||
import ac.mdiq.podcini.net.sync.gpoddernet.GpodnetService
|
||||
import ac.mdiq.podcini.net.sync.gpoddernet.model.GpodnetDevice
|
||||
import ac.mdiq.podcini.net.sync.nextcloud.NextcloudLoginFlow
|
||||
import ac.mdiq.podcini.net.sync.wifi.WifiSyncService.Companion.hostPort
|
||||
import ac.mdiq.podcini.net.sync.wifi.WifiSyncService.Companion.startInstantSync
|
||||
import ac.mdiq.podcini.storage.utils.FileNameGenerator.generateFileName
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.Context.WIFI_SERVICE
|
||||
import android.content.DialogInterface
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateUtils
|
||||
import android.text.method.HideReturnsTransformationMethod
|
||||
import android.text.method.PasswordTransformationMethod
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class SynchronizationPreferencesFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.preferences_synchronization)
|
||||
setupScreen()
|
||||
updateScreen()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
(activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.synchronization_pref)
|
||||
updateScreen()
|
||||
procFlowEvents()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
cancelFlowEvents()
|
||||
(activity as PreferenceActivity).supportActionBar!!.subtitle = ""
|
||||
}
|
||||
|
||||
private var eventSink: Job? = null
|
||||
private fun cancelFlowEvents() {
|
||||
eventSink?.cancel()
|
||||
eventSink = null
|
||||
}
|
||||
private fun procFlowEvents() {
|
||||
if (eventSink != null) return
|
||||
eventSink = lifecycleScope.launch {
|
||||
EventFlow.events.collectLatest { event ->
|
||||
Logd("SynchronizationPreferencesFragment", "Received event: ${event.TAG}")
|
||||
when (event) {
|
||||
is FlowEvent.SyncServiceEvent -> syncStatusChanged(event)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun syncStatusChanged(event: FlowEvent.SyncServiceEvent) {
|
||||
if (!isProviderConnected && !wifiSyncEnabledKey) return
|
||||
|
||||
updateScreen()
|
||||
if (event.messageResId == R.string.sync_status_error || event.messageResId == R.string.sync_status_success)
|
||||
updateLastSyncReport(SynchronizationSettings.isLastSyncSuccessful, SynchronizationSettings.lastSyncAttempt)
|
||||
else (activity as PreferenceActivity).supportActionBar!!.setSubtitle(event.messageResId)
|
||||
}
|
||||
|
||||
private fun setupScreen() {
|
||||
val activity: Activity? = activity
|
||||
findPreference<Preference>(Prefs.pref_gpodnet_setlogin_information.name)?.setOnPreferenceClickListener {
|
||||
val dialog: AuthenticationDialog = object : AuthenticationDialog(requireContext(), R.string.pref_gpodnet_setlogin_information_title,
|
||||
false, SynchronizationCredentials.username, null) {
|
||||
override fun onConfirmed(username: String, password: String) {
|
||||
SynchronizationCredentials.password = password
|
||||
}
|
||||
}
|
||||
dialog.show()
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(Prefs.pref_synchronization_sync.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
SyncService.syncImmediately(requireActivity().applicationContext)
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(Prefs.pref_synchronization_force_full_sync.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
SyncService.fullSync(requireContext())
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(Prefs.pref_synchronization_logout.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
SynchronizationCredentials.clear(requireContext())
|
||||
Snackbar.make(requireView(), R.string.pref_synchronization_logout_toast, Snackbar.LENGTH_LONG).show()
|
||||
setSelectedSyncProvider(null)
|
||||
updateScreen()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateScreen() {
|
||||
val preferenceInstantSync = findPreference<Preference>(Prefs.preference_instant_sync.name)
|
||||
preferenceInstantSync!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
WifiAuthenticationFragment().show(childFragmentManager, WifiAuthenticationFragment.TAG)
|
||||
true
|
||||
}
|
||||
|
||||
val loggedIn = isProviderConnected
|
||||
val preferenceHeader = findPreference<Preference>(Prefs.preference_synchronization_description.name)
|
||||
if (loggedIn) {
|
||||
val selectedProvider = SynchronizationProviderViewData.fromIdentifier(selectedSyncProviderKey)
|
||||
preferenceHeader!!.title = ""
|
||||
if (selectedProvider != null) {
|
||||
preferenceHeader.setSummary(selectedProvider.summaryResource)
|
||||
preferenceHeader.setIcon(selectedProvider.iconResource)
|
||||
}
|
||||
preferenceHeader.onPreferenceClickListener = null
|
||||
} else {
|
||||
preferenceHeader!!.setTitle(R.string.synchronization_choose_title)
|
||||
preferenceHeader.setSummary(R.string.synchronization_summary_unchoosen)
|
||||
preferenceHeader.setIcon(R.drawable.ic_cloud)
|
||||
preferenceHeader.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
chooseProviderAndLogin()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
val gpodnetSetLoginPreference = findPreference<Preference>(Prefs.pref_gpodnet_setlogin_information.name)
|
||||
gpodnetSetLoginPreference!!.isVisible = isProviderSelected(SynchronizationProviderViewData.GPODDER_NET)
|
||||
gpodnetSetLoginPreference.isEnabled = loggedIn
|
||||
findPreference<Preference>(Prefs.pref_synchronization_sync.name)!!.isVisible = loggedIn
|
||||
findPreference<Preference>(Prefs.pref_synchronization_force_full_sync.name)!!.isVisible = loggedIn
|
||||
findPreference<Preference>(Prefs.pref_synchronization_logout.name)!!.isVisible = loggedIn
|
||||
if (loggedIn) {
|
||||
val summary = getString(R.string.synchronization_login_status,
|
||||
SynchronizationCredentials.username, SynchronizationCredentials.hosturl)
|
||||
val formattedSummary = HtmlCompat.fromHtml(summary, HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
findPreference<Preference>(Prefs.pref_synchronization_logout.name)!!.summary = formattedSummary
|
||||
updateLastSyncReport(SynchronizationSettings.isLastSyncSuccessful, SynchronizationSettings.lastSyncAttempt)
|
||||
} else {
|
||||
findPreference<Preference>(Prefs.pref_synchronization_logout.name)?.summary = ""
|
||||
(activity as PreferenceActivity).supportActionBar?.setSubtitle("")
|
||||
}
|
||||
}
|
||||
|
||||
private fun chooseProviderAndLogin() {
|
||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||
builder.setTitle(R.string.dialog_choose_sync_service_title)
|
||||
|
||||
val providers = SynchronizationProviderViewData.entries.toTypedArray()
|
||||
val adapter: ListAdapter = object : ArrayAdapter<SynchronizationProviderViewData?>(requireContext(), R.layout.alertdialog_sync_provider_chooser, providers) {
|
||||
var holder: ViewHolder? = null
|
||||
|
||||
inner class ViewHolder {
|
||||
var icon: ImageView? = null
|
||||
var title: TextView? = null
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
var convertView = convertView
|
||||
val inflater = LayoutInflater.from(context)
|
||||
if (convertView == null) {
|
||||
convertView = inflater.inflate(R.layout.alertdialog_sync_provider_chooser, null)
|
||||
val binding = AlertdialogSyncProviderChooserBinding.bind(convertView)
|
||||
holder = ViewHolder()
|
||||
if (holder != null) {
|
||||
holder!!.icon = binding.icon
|
||||
holder!!.title = binding.title
|
||||
convertView.tag = holder
|
||||
}
|
||||
} else holder = convertView.tag as ViewHolder
|
||||
|
||||
val synchronizationProviderViewData = getItem(position)
|
||||
holder!!.title!!.setText(synchronizationProviderViewData!!.summaryResource)
|
||||
holder!!.icon!!.setImageResource(synchronizationProviderViewData.iconResource)
|
||||
return convertView!!
|
||||
}
|
||||
}
|
||||
|
||||
builder.setAdapter(adapter) { _: DialogInterface?, which: Int ->
|
||||
when (providers[which]) {
|
||||
SynchronizationProviderViewData.GPODDER_NET -> GpodderAuthenticationFragment().show(childFragmentManager,
|
||||
GpodderAuthenticationFragment.TAG)
|
||||
SynchronizationProviderViewData.NEXTCLOUD_GPODDER -> NextcloudAuthenticationFragment().show(childFragmentManager,
|
||||
NextcloudAuthenticationFragment.TAG)
|
||||
}
|
||||
updateScreen()
|
||||
}
|
||||
|
||||
builder.show()
|
||||
}
|
||||
|
||||
private fun isProviderSelected(provider: SynchronizationProviderViewData): Boolean {
|
||||
val selectedSyncProviderKey = selectedSyncProviderKey
|
||||
return provider.identifier == selectedSyncProviderKey
|
||||
}
|
||||
|
||||
private val selectedSyncProviderKey: String
|
||||
get() = SynchronizationSettings.selectedSyncProviderKey?:""
|
||||
|
||||
private fun updateLastSyncReport(successful: Boolean, lastTime: Long) {
|
||||
val status = String.format("%1\$s (%2\$s)", getString(if (successful) R.string.gpodnetsync_pref_report_successful else R.string.gpodnetsync_pref_report_failed),
|
||||
DateUtils.getRelativeDateTimeString(context, lastTime, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, DateUtils.FORMAT_SHOW_TIME))
|
||||
(activity as PreferenceActivity).supportActionBar!!.subtitle = status
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a dialog with a username and password text field and an optional checkbox to save username and preferences.
|
||||
*/
|
||||
abstract class AuthenticationDialog(context: Context, titleRes: Int, enableUsernameField: Boolean, usernameInitialValue: String?, passwordInitialValue: String?)
|
||||
: MaterialAlertDialogBuilder(context) {
|
||||
|
||||
var passwordHidden: Boolean = true
|
||||
|
||||
init {
|
||||
setTitle(titleRes)
|
||||
val viewBinding = AuthenticationDialogBinding.inflate(LayoutInflater.from(context))
|
||||
setView(viewBinding.root)
|
||||
|
||||
viewBinding.usernameEditText.isEnabled = enableUsernameField
|
||||
if (usernameInitialValue != null) viewBinding.usernameEditText.setText(usernameInitialValue)
|
||||
if (passwordInitialValue != null) viewBinding.passwordEditText.setText(passwordInitialValue)
|
||||
|
||||
viewBinding.showPasswordButton.setOnClickListener {
|
||||
if (passwordHidden) {
|
||||
viewBinding.passwordEditText.transformationMethod = HideReturnsTransformationMethod.getInstance()
|
||||
viewBinding.showPasswordButton.alpha = 1.0f
|
||||
} else {
|
||||
viewBinding.passwordEditText.transformationMethod = PasswordTransformationMethod.getInstance()
|
||||
viewBinding.showPasswordButton.alpha = 0.6f
|
||||
}
|
||||
passwordHidden = !passwordHidden
|
||||
}
|
||||
|
||||
setOnCancelListener { onCancelled() }
|
||||
setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> onCancelled() }
|
||||
setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int ->
|
||||
onConfirmed(viewBinding.usernameEditText.text.toString(), viewBinding.passwordEditText.text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun onCancelled() {}
|
||||
|
||||
protected abstract fun onConfirmed(username: String, password: String)
|
||||
}
|
||||
|
||||
/**
|
||||
* Guides the user through the authentication process.
|
||||
*/
|
||||
class NextcloudAuthenticationFragment : DialogFragment(), NextcloudLoginFlow.AuthenticationCallback {
|
||||
private var binding: NextcloudAuthDialogBinding? = null
|
||||
private var nextcloudLoginFlow: NextcloudLoginFlow? = null
|
||||
private var shouldDismiss = false
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = MaterialAlertDialogBuilder(requireContext())
|
||||
dialog.setTitle(R.string.gpodnetauth_login_butLabel)
|
||||
dialog.setNegativeButton(R.string.cancel_label, null)
|
||||
dialog.setCancelable(false)
|
||||
this.isCancelable = false
|
||||
|
||||
binding = NextcloudAuthDialogBinding.inflate(layoutInflater)
|
||||
dialog.setView(binding!!.root)
|
||||
|
||||
binding!!.chooseHostButton.setOnClickListener {
|
||||
nextcloudLoginFlow = NextcloudLoginFlow(getHttpClient(), binding!!.serverUrlText.text.toString(), requireContext(), this)
|
||||
startLoginFlow()
|
||||
}
|
||||
if (savedInstanceState?.getStringArrayList(EXTRA_LOGIN_FLOW) != null) {
|
||||
nextcloudLoginFlow = NextcloudLoginFlow.fromInstanceState(getHttpClient(), requireContext(), this,
|
||||
savedInstanceState.getStringArrayList(EXTRA_LOGIN_FLOW)!!)
|
||||
startLoginFlow()
|
||||
}
|
||||
return dialog.create()
|
||||
}
|
||||
private fun startLoginFlow() {
|
||||
binding!!.errorText.visibility = View.GONE
|
||||
binding!!.chooseHostButton.visibility = View.GONE
|
||||
binding!!.loginProgressContainer.visibility = View.VISIBLE
|
||||
binding!!.serverUrlText.isEnabled = false
|
||||
nextcloudLoginFlow!!.start()
|
||||
}
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
if (nextcloudLoginFlow != null) outState.putStringArrayList(EXTRA_LOGIN_FLOW, nextcloudLoginFlow!!.saveInstanceState())
|
||||
}
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
nextcloudLoginFlow?.cancel()
|
||||
}
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
nextcloudLoginFlow?.onResume()
|
||||
|
||||
if (shouldDismiss) dismiss()
|
||||
}
|
||||
override fun onNextcloudAuthenticated(server: String, username: String, password: String) {
|
||||
setSelectedSyncProvider(SynchronizationProviderViewData.NEXTCLOUD_GPODDER)
|
||||
SynchronizationCredentials.clear(requireContext())
|
||||
SynchronizationCredentials.password = password
|
||||
SynchronizationCredentials.hosturl = server
|
||||
SynchronizationCredentials.username = username
|
||||
SyncService.fullSync(requireContext())
|
||||
if (isResumed) dismiss()
|
||||
else shouldDismiss = true
|
||||
}
|
||||
override fun onNextcloudAuthError(errorMessage: String?) {
|
||||
binding!!.loginProgressContainer.visibility = View.GONE
|
||||
binding!!.errorText.visibility = View.VISIBLE
|
||||
binding!!.errorText.text = errorMessage
|
||||
binding!!.chooseHostButton.visibility = View.VISIBLE
|
||||
binding!!.serverUrlText.isEnabled = true
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = NextcloudAuthenticationFragment::class.simpleName ?: "Anonymous"
|
||||
private const val EXTRA_LOGIN_FLOW = "LoginFlow"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Guides the user through the authentication process.
|
||||
*/
|
||||
class GpodderAuthenticationFragment : DialogFragment() {
|
||||
private var viewFlipper: ViewFlipper? = null
|
||||
private var currentStep = -1
|
||||
private var service: GpodnetService? = null
|
||||
@Volatile
|
||||
private var username: String? = null
|
||||
@Volatile
|
||||
private var password: String? = null
|
||||
@Volatile
|
||||
private var selectedDevice: GpodnetDevice? = null
|
||||
private var devices: List<GpodnetDevice>? = null
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = MaterialAlertDialogBuilder(requireContext())
|
||||
dialog.setTitle(R.string.gpodnetauth_login_butLabel)
|
||||
dialog.setNegativeButton(R.string.cancel_label, null)
|
||||
dialog.setCancelable(false)
|
||||
this.isCancelable = false
|
||||
val binding = GpodnetauthDialogBinding.inflate(layoutInflater)
|
||||
// val root = View.inflate(context, R.layout.gpodnetauth_dialog, null)
|
||||
viewFlipper = binding.viewflipper
|
||||
advance()
|
||||
dialog.setView(binding.root)
|
||||
|
||||
return dialog.create()
|
||||
}
|
||||
private fun setupHostView(view: View) {
|
||||
val binding = GpodnetauthHostBinding.bind(view)
|
||||
val selectHost = binding.chooseHostButton
|
||||
val serverUrlText = binding.serverUrlText
|
||||
selectHost.setOnClickListener {
|
||||
if (serverUrlText.text.isNullOrEmpty()) return@setOnClickListener
|
||||
|
||||
SynchronizationCredentials.clear(requireContext())
|
||||
SynchronizationCredentials.hosturl = serverUrlText.text.toString()
|
||||
service = GpodnetService(getHttpClient(), SynchronizationCredentials.hosturl, SynchronizationCredentials.deviceID?:"",
|
||||
SynchronizationCredentials.username?:"", SynchronizationCredentials.password?:"")
|
||||
dialog?.setTitle(SynchronizationCredentials.hosturl)
|
||||
advance()
|
||||
}
|
||||
}
|
||||
private fun setupLoginView(view: View) {
|
||||
val binding = GpodnetauthCredentialsBinding.bind(view)
|
||||
val username = binding.etxtUsername
|
||||
val password = binding.etxtPassword
|
||||
val login = binding.butLogin
|
||||
val txtvError = binding.credentialsError
|
||||
val progressBar = binding.progBarLogin
|
||||
val createAccountWarning = binding.createAccountWarning
|
||||
|
||||
if (SynchronizationCredentials.hosturl != null && SynchronizationCredentials.hosturl!!.startsWith("http://"))
|
||||
createAccountWarning.visibility = View.VISIBLE
|
||||
|
||||
password.setOnEditorActionListener { _: TextView?, actionID: Int, _: KeyEvent? -> actionID == EditorInfo.IME_ACTION_GO && login.performClick() }
|
||||
|
||||
login.setOnClickListener {
|
||||
val usernameStr = username.text.toString()
|
||||
val passwordStr = password.text.toString()
|
||||
|
||||
if (usernameHasUnwantedChars(usernameStr)) {
|
||||
txtvError.setText(R.string.gpodnetsync_username_characters_error)
|
||||
txtvError.visibility = View.VISIBLE
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
login.isEnabled = false
|
||||
progressBar.visibility = View.VISIBLE
|
||||
txtvError.visibility = View.GONE
|
||||
val inputManager = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputManager.hideSoftInputFromWindow(login.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
service?.setCredentials(usernameStr, passwordStr)
|
||||
service?.login()
|
||||
if (service != null) devices = service!!.devices
|
||||
this@GpodderAuthenticationFragment.username = usernameStr
|
||||
this@GpodderAuthenticationFragment.password = passwordStr
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
login.isEnabled = true
|
||||
progressBar.visibility = View.GONE
|
||||
advance()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
login.isEnabled = true
|
||||
progressBar.visibility = View.GONE
|
||||
txtvError.text = e.cause!!.message
|
||||
txtvError.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun setupDeviceView(view: View) {
|
||||
val binding = GpodnetauthDeviceBinding.bind(view)
|
||||
val deviceName = binding.deviceName
|
||||
val devicesContainer = binding.devicesContainer
|
||||
deviceName.setText(generateDeviceName())
|
||||
|
||||
val createDeviceButton = binding.createDeviceButton
|
||||
createDeviceButton.setOnClickListener { createDevice(view) }
|
||||
|
||||
for (device in devices!!) {
|
||||
val rBinding = GpodnetauthDeviceRowBinding.inflate(layoutInflater)
|
||||
// val row = View.inflate(context, R.layout.gpodnetauth_device_row, null)
|
||||
val selectDeviceButton = rBinding.selectDeviceButton
|
||||
selectDeviceButton.setOnClickListener {
|
||||
selectedDevice = device
|
||||
advance()
|
||||
}
|
||||
selectDeviceButton.text = device.caption
|
||||
devicesContainer.addView(rBinding.root)
|
||||
}
|
||||
}
|
||||
private fun createDevice(view: View) {
|
||||
val binding = GpodnetauthDeviceBinding.bind(view)
|
||||
val deviceName = binding.deviceName
|
||||
val txtvError = binding.deviceSelectError
|
||||
val progBarCreateDevice = binding.progbarCreateDevice
|
||||
|
||||
val deviceNameStr = deviceName.text.toString()
|
||||
if (isDeviceInList(deviceNameStr)) return
|
||||
|
||||
progBarCreateDevice.visibility = View.VISIBLE
|
||||
txtvError.visibility = View.GONE
|
||||
deviceName.isEnabled = false
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val device = withContext(Dispatchers.IO) {
|
||||
val deviceId = generateDeviceId(deviceNameStr)
|
||||
service!!.configureDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE)
|
||||
GpodnetDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE.toString(), 0)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
progBarCreateDevice.visibility = View.GONE
|
||||
selectedDevice = device
|
||||
advance()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
deviceName.isEnabled = true
|
||||
progBarCreateDevice.visibility = View.GONE
|
||||
txtvError.text = e.message
|
||||
txtvError.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun generateDeviceName(): String {
|
||||
val baseName = getString(R.string.gpodnetauth_device_name_default, Build.MODEL)
|
||||
var name = baseName
|
||||
var num = 1
|
||||
while (isDeviceInList(name)) {
|
||||
name = "$baseName ($num)"
|
||||
num++
|
||||
}
|
||||
return name
|
||||
}
|
||||
private fun generateDeviceId(name: String): String {
|
||||
// devices names must be of a certain form:
|
||||
// https://gpoddernet.readthedocs.org/en/latest/api/reference/general.html#devices
|
||||
return generateFileName(name).replace("\\W".toRegex(), "_").lowercase()
|
||||
}
|
||||
private fun isDeviceInList(name: String): Boolean {
|
||||
if (devices == null) return false
|
||||
|
||||
val id = generateDeviceId(name)
|
||||
for (device in devices!!) {
|
||||
if (device.id == id || device.caption == name) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
private fun setupFinishView(view: View) {
|
||||
val binding = GpodnetauthFinishBinding.bind(view)
|
||||
val sync = binding.butSyncNow
|
||||
|
||||
sync.setOnClickListener {
|
||||
dismiss()
|
||||
SyncService.sync(requireContext())
|
||||
}
|
||||
}
|
||||
private fun advance() {
|
||||
if (currentStep < STEP_FINISH) {
|
||||
val view = viewFlipper!!.getChildAt(currentStep + 1)
|
||||
when (currentStep) {
|
||||
STEP_DEFAULT -> setupHostView(view)
|
||||
STEP_HOSTNAME -> setupLoginView(view)
|
||||
STEP_LOGIN -> {
|
||||
check(!(username == null || password == null)) { "Username and password must not be null here" }
|
||||
setupDeviceView(view)
|
||||
}
|
||||
STEP_DEVICE -> {
|
||||
checkNotNull(selectedDevice) { "Device must not be null here" }
|
||||
setSelectedSyncProvider(SynchronizationProviderViewData.GPODDER_NET)
|
||||
SynchronizationCredentials.username = username
|
||||
SynchronizationCredentials.password = password
|
||||
SynchronizationCredentials.deviceID = selectedDevice!!.id
|
||||
setupFinishView(view)
|
||||
}
|
||||
}
|
||||
if (currentStep != STEP_DEFAULT) viewFlipper!!.showNext()
|
||||
currentStep++
|
||||
} else dismiss()
|
||||
}
|
||||
private fun usernameHasUnwantedChars(username: String): Boolean {
|
||||
val special = Pattern.compile("[!@#$%&*()+=|<>?{}\\[\\]~]")
|
||||
val containsUnwantedChars = special.matcher(username)
|
||||
return containsUnwantedChars.find()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = GpodderAuthenticationFragment::class.simpleName ?: "Anonymous"
|
||||
|
||||
private const val STEP_DEFAULT = -1
|
||||
private const val STEP_HOSTNAME = 0
|
||||
private const val STEP_LOGIN = 1
|
||||
private const val STEP_DEVICE = 2
|
||||
private const val STEP_FINISH = 3
|
||||
}
|
||||
}
|
||||
|
||||
class WifiAuthenticationFragment : DialogFragment() {
|
||||
private var binding: WifiSyncDialogBinding? = null
|
||||
private var portNum = 0
|
||||
private var isGuest: Boolean? = null
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = MaterialAlertDialogBuilder(requireContext())
|
||||
dialog.setTitle(R.string.connect_to_peer)
|
||||
dialog.setNegativeButton(R.string.cancel_label, null)
|
||||
dialog.setPositiveButton(R.string.confirm_label, null)
|
||||
|
||||
binding = WifiSyncDialogBinding.inflate(layoutInflater)
|
||||
dialog.setView(binding!!.root)
|
||||
|
||||
binding!!.hostAddressText.setText(SynchronizationCredentials.hosturl?:"")
|
||||
portNum = SynchronizationCredentials.hostport
|
||||
if (portNum == 0) portNum = hostPort
|
||||
binding!!.hostPortText.setText(portNum.toString())
|
||||
|
||||
binding!!.guestButton.setOnClickListener {
|
||||
binding!!.hostAddressText.visibility = View.VISIBLE
|
||||
binding!!.hostPortText.visibility = View.VISIBLE
|
||||
binding!!.hostButton.visibility = View.INVISIBLE
|
||||
SynchronizationCredentials.hosturl = binding!!.hostAddressText.text.toString()
|
||||
portNum = binding!!.hostPortText.text.toString().toInt()
|
||||
isGuest = true
|
||||
SynchronizationCredentials.hostport = portNum
|
||||
}
|
||||
binding!!.hostButton.setOnClickListener {
|
||||
binding!!.hostAddressText.visibility = View.VISIBLE
|
||||
binding!!.hostPortText.visibility = View.VISIBLE
|
||||
binding!!.guestButton.visibility = View.INVISIBLE
|
||||
val wifiManager = requireContext().applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
|
||||
val ipAddress = wifiManager.connectionInfo.ipAddress
|
||||
val ipString = String.format(Locale.US, "%d.%d.%d.%d", ipAddress and 0xff, ipAddress shr 8 and 0xff, ipAddress shr 16 and 0xff, ipAddress shr 24 and 0xff)
|
||||
binding!!.hostAddressText.setText(ipString)
|
||||
binding!!.hostAddressText.isEnabled = false
|
||||
portNum = binding!!.hostPortText.text.toString().toInt()
|
||||
isGuest = false
|
||||
SynchronizationCredentials.hostport = portNum
|
||||
}
|
||||
procFlowEvents()
|
||||
return dialog.create()
|
||||
}
|
||||
override fun onDestroy() {
|
||||
cancelFlowEvents()
|
||||
super.onDestroy()
|
||||
}
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val d = dialog as? AlertDialog
|
||||
if (d != null) {
|
||||
val confirmButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
|
||||
confirmButton.setOnClickListener {
|
||||
Logd(TAG, "confirm button pressed")
|
||||
if (isGuest == null) {
|
||||
Toast.makeText(requireContext(), R.string.host_or_guest, Toast.LENGTH_LONG).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
binding!!.progressContainer.visibility = View.VISIBLE
|
||||
confirmButton.visibility = View.INVISIBLE
|
||||
val cancelButton = d.getButton(Dialog.BUTTON_NEGATIVE) as Button
|
||||
cancelButton.visibility = View.INVISIBLE
|
||||
portNum = binding!!.hostPortText.text.toString().toInt()
|
||||
setWifiSyncEnabled(true)
|
||||
startInstantSync(requireContext(), portNum, binding!!.hostAddressText.text.toString(), isGuest!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var eventSink: Job? = null
|
||||
private fun cancelFlowEvents() {
|
||||
eventSink?.cancel()
|
||||
eventSink = null
|
||||
}
|
||||
private fun procFlowEvents() {
|
||||
if (eventSink != null) return
|
||||
eventSink = lifecycleScope.launch {
|
||||
EventFlow.events.collectLatest { event ->
|
||||
Logd(TAG, "Received event: ${event.TAG}")
|
||||
when (event) {
|
||||
is FlowEvent.SyncServiceEvent -> syncStatusChanged(event)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fun syncStatusChanged(event: FlowEvent.SyncServiceEvent) {
|
||||
when (event.messageResId) {
|
||||
R.string.sync_status_error -> {
|
||||
Toast.makeText(requireContext(), event.message, Toast.LENGTH_LONG).show()
|
||||
dialog?.dismiss()
|
||||
}
|
||||
R.string.sync_status_success -> {
|
||||
Toast.makeText(requireContext(), R.string.sync_status_success, Toast.LENGTH_LONG).show()
|
||||
dialog?.dismiss()
|
||||
}
|
||||
R.string.sync_status_in_progress -> {
|
||||
binding!!.progressBar.progress = event.message.toInt()
|
||||
}
|
||||
else -> {
|
||||
Logd(TAG, "Sync result unknow ${event.messageResId}")
|
||||
// Toast.makeText(context, "Sync result unknow ${event.messageResId}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = WifiAuthenticationFragment::class.simpleName ?: "Anonymous"
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("EnumEntryName")
|
||||
private enum class Prefs {
|
||||
preference_instant_sync,
|
||||
preference_synchronization_description,
|
||||
pref_gpodnet_setlogin_information,
|
||||
pref_synchronization_sync,
|
||||
pref_synchronization_force_full_sync,
|
||||
pref_synchronization_logout,
|
||||
}
|
||||
}
|
|
@ -1,169 +0,0 @@
|
|||
package ac.mdiq.podcini.preferences.fragments
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.defaultPage
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.fullNotificationButtons
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.hiddenDrawerItems
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.setShowRemainTimeSetting
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity.Companion.getTitleOfPage
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity.Screens
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity.SwipePreferencesFragment
|
||||
import ac.mdiq.podcini.ui.fragment.NavDrawerFragment.Companion.navMap
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
||||
class UserInterfacePreferencesFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.preferences_user_interface)
|
||||
setupInterfaceScreen()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
(activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.user_interface_label)
|
||||
}
|
||||
|
||||
private fun setupInterfaceScreen() {
|
||||
val restartApp = Preference.OnPreferenceChangeListener { _: Preference?, _: Any? ->
|
||||
ActivityCompat.recreate(requireActivity())
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(UserPreferences.Prefs.prefTheme.name)!!.onPreferenceChangeListener = restartApp
|
||||
findPreference<Preference>(UserPreferences.Prefs.prefThemeBlack.name)!!.onPreferenceChangeListener = restartApp
|
||||
findPreference<Preference>(UserPreferences.Prefs.prefTintedColors.name)!!.onPreferenceChangeListener = restartApp
|
||||
if (Build.VERSION.SDK_INT < 31) findPreference<Preference>(UserPreferences.Prefs.prefTintedColors.name)!!.isVisible = false
|
||||
|
||||
findPreference<Preference>(UserPreferences.Prefs.showTimeLeft.name)?.setOnPreferenceChangeListener { _: Preference?, newValue: Any? ->
|
||||
setShowRemainTimeSetting(newValue as Boolean?)
|
||||
// TODO: need another event type?
|
||||
// EventFlow.postEvent(FlowEvent.EpisodePlayedEvent())
|
||||
EventFlow.postEvent(FlowEvent.PlayerSettingsEvent())
|
||||
true
|
||||
}
|
||||
|
||||
fun drawerPreferencesDialog(context: Context, callback: Runnable?) {
|
||||
val hiddenItems = hiddenDrawerItems.map { it.trim() }.toMutableSet()
|
||||
// val navTitles = context.resources.getStringArray(R.array.nav_drawer_titles)
|
||||
val navTitles = navMap.values.map { context.resources.getString(it.nameRes).trim() }.toTypedArray()
|
||||
val checked = BooleanArray(navMap.size)
|
||||
for (i in navMap.keys.indices) {
|
||||
val tag = navMap.keys.toList()[i]
|
||||
if (!hiddenItems.contains(tag)) checked[i] = true
|
||||
}
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
builder.setTitle(R.string.drawer_preferences)
|
||||
builder.setMultiChoiceItems(navTitles, checked) { _: DialogInterface?, which: Int, isChecked: Boolean ->
|
||||
if (isChecked) hiddenItems.remove(navMap.keys.toList()[which])
|
||||
else hiddenItems.add((navMap.keys.toList()[which]).trim())
|
||||
}
|
||||
builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int ->
|
||||
hiddenDrawerItems = hiddenItems.toList()
|
||||
if (hiddenItems.contains(defaultPage)) {
|
||||
for (tag in navMap.keys) {
|
||||
if (!hiddenItems.contains(tag)) {
|
||||
defaultPage = tag
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
callback?.run()
|
||||
}
|
||||
builder.setNegativeButton(R.string.cancel_label, null)
|
||||
builder.create().show()
|
||||
}
|
||||
|
||||
findPreference<Preference>(UserPreferences.Prefs.prefHiddenDrawerItems.name)?.setOnPreferenceClickListener {
|
||||
drawerPreferencesDialog(requireContext(), null)
|
||||
true
|
||||
}
|
||||
|
||||
findPreference<Preference>(UserPreferences.Prefs.prefFullNotificationButtons.name)?.setOnPreferenceClickListener {
|
||||
showFullNotificationButtonsDialog()
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(PREF_SWIPE)?.setOnPreferenceClickListener {
|
||||
(activity as PreferenceActivity).openScreen(Screens.preferences_swipe)
|
||||
true
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 26) findPreference<Preference>(UserPreferences.Prefs.prefExpandNotify.name)!!.isVisible = false
|
||||
}
|
||||
|
||||
|
||||
private fun showFullNotificationButtonsDialog() {
|
||||
val context: Context? = activity
|
||||
|
||||
val preferredButtons = fullNotificationButtons
|
||||
val allButtonNames = context!!.resources.getStringArray(R.array.full_notification_buttons_options)
|
||||
val buttonIDs = intArrayOf(2, 3, 4)
|
||||
val exactItems = 2
|
||||
val completeListener = DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> fullNotificationButtons = preferredButtons }
|
||||
val title = context.resources.getString(
|
||||
R.string.pref_full_notification_buttons_title)
|
||||
|
||||
showNotificationButtonsDialog(preferredButtons.toMutableList(), allButtonNames, buttonIDs, title, exactItems, completeListener)
|
||||
}
|
||||
|
||||
private fun showNotificationButtonsDialog(preferredButtons: MutableList<Int>?, allButtonNames: Array<String>, buttonIds: IntArray,
|
||||
title: String, exactItems: Int, completeListener: DialogInterface.OnClickListener) {
|
||||
val checked = BooleanArray(allButtonNames.size) // booleans default to false in java
|
||||
|
||||
val context: Context? = activity
|
||||
|
||||
// Clear buttons that are not part of the setting anymore
|
||||
for (i in preferredButtons!!.indices.reversed()) {
|
||||
var isValid = false
|
||||
for (j in checked.indices) {
|
||||
if (buttonIds[j] == preferredButtons[i]) {
|
||||
isValid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!isValid) preferredButtons.removeAt(i)
|
||||
}
|
||||
|
||||
for (i in checked.indices) if (preferredButtons.contains(buttonIds[i])) checked[i] = true
|
||||
|
||||
val builder = MaterialAlertDialogBuilder(context!!)
|
||||
builder.setTitle(title)
|
||||
builder.setMultiChoiceItems(allButtonNames,
|
||||
checked) { _: DialogInterface?, which: Int, isChecked: Boolean ->
|
||||
checked[which] = isChecked
|
||||
if (isChecked) preferredButtons.add(buttonIds[which])
|
||||
else preferredButtons.remove(buttonIds[which])
|
||||
}
|
||||
builder.setPositiveButton(R.string.confirm_label, null)
|
||||
builder.setNegativeButton(R.string.cancel_label, null)
|
||||
val dialog = builder.create()
|
||||
|
||||
dialog.show()
|
||||
|
||||
val positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||
|
||||
positiveButton.setOnClickListener {
|
||||
if (preferredButtons.size != exactItems) {
|
||||
val selectionView = dialog.listView
|
||||
Snackbar.make(selectionView, String.format(context.resources.getString(R.string.pref_compact_notification_buttons_dialog_error_exact), exactItems), Snackbar.LENGTH_SHORT).show()
|
||||
} else {
|
||||
completeListener.onClick(dialog, AlertDialog.BUTTON_POSITIVE)
|
||||
dialog.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_SWIPE = "prefSwipe"
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package ac.mdiq.podcini.storage.algorithms
|
||||
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.EPISODE_CLEANUP_NULL
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.episodeCacheSize
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
|
||||
|
@ -13,6 +12,7 @@ import ac.mdiq.podcini.storage.model.Episode
|
|||
import ac.mdiq.podcini.storage.model.EpisodeFilter
|
||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
|
||||
import ac.mdiq.podcini.storage.model.PlayState
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity.AutoDownloadPreferencesFragment.EpisodeCleanupOptions
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
|
@ -25,7 +25,7 @@ object AutoCleanups {
|
|||
private val TAG: String = AutoCleanups::class.simpleName ?: "Anonymous"
|
||||
|
||||
private var episodeCleanupValue: Int
|
||||
get() = appPrefs.getString(UserPreferences.Prefs.prefEpisodeCleanup.name, "" + EPISODE_CLEANUP_NULL)!!.toInt()
|
||||
get() = appPrefs.getString(UserPreferences.Prefs.prefEpisodeCleanup.name, EpisodeCleanupOptions.Never.num.toString())!!.toIntOrNull() ?: EpisodeCleanupOptions.Never.num
|
||||
set(episodeCleanupValue) {
|
||||
appPrefs.edit().putString(UserPreferences.Prefs.prefEpisodeCleanup.name, episodeCleanupValue.toString()).apply()
|
||||
}
|
||||
|
@ -46,9 +46,9 @@ object AutoCleanups {
|
|||
if (!isEnableAutodownload) return APNullCleanupAlgorithm()
|
||||
|
||||
return when (val cleanupValue = episodeCleanupValue) {
|
||||
UserPreferences.EPISODE_CLEANUP_EXCEPT_FAVORITE -> ExceptFavoriteCleanupAlgorithm()
|
||||
UserPreferences.EPISODE_CLEANUP_QUEUE -> APQueueCleanupAlgorithm()
|
||||
EPISODE_CLEANUP_NULL -> APNullCleanupAlgorithm()
|
||||
EpisodeCleanupOptions.ExceptFavorites.num -> ExceptFavoriteCleanupAlgorithm()
|
||||
EpisodeCleanupOptions.NotInQueue.num -> APQueueCleanupAlgorithm()
|
||||
EpisodeCleanupOptions.Never.num -> APNullCleanupAlgorithm()
|
||||
else -> APCleanupAlgorithm(cleanupValue)
|
||||
}
|
||||
}
|
||||
|
@ -61,9 +61,7 @@ object AutoCleanups {
|
|||
get() {
|
||||
val candidates: MutableList<Episode> = ArrayList()
|
||||
val downloadedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.downloaded.name), EpisodeSortOrder.DATE_NEW_OLD)
|
||||
for (item in downloadedItems) {
|
||||
if (item.media != null && item.media!!.downloaded && !item.isSUPER) candidates.add(item)
|
||||
}
|
||||
for (item in downloadedItems) if (item.media != null && item.media!!.downloaded && !item.isSUPER) candidates.add(item)
|
||||
return candidates
|
||||
}
|
||||
override fun getReclaimableItems(): Int {
|
||||
|
@ -82,13 +80,8 @@ object AutoCleanups {
|
|||
val delete = if (candidates.size > numToRemove) candidates.subList(0, numToRemove) else candidates
|
||||
for (item in delete) {
|
||||
if (item.media == null) continue
|
||||
try {
|
||||
runBlocking { deleteEpisodeMedia(context, item).join() }
|
||||
} catch (e: InterruptedException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: ExecutionException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
try { runBlocking { deleteEpisodeMedia(context, item).join() }
|
||||
} catch (e: InterruptedException) { e.printStackTrace() } catch (e: ExecutionException) { e.printStackTrace() }
|
||||
}
|
||||
val counter = delete.size
|
||||
Log.i(TAG, String.format(Locale.US, "Auto-delete deleted %d episodes (%d requested)", counter, numToRemove))
|
||||
|
@ -96,7 +89,7 @@ object AutoCleanups {
|
|||
}
|
||||
public override fun getDefaultCleanupParameter(): Int {
|
||||
val cacheSize = episodeCacheSize
|
||||
if (cacheSize != UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED) {
|
||||
if (cacheSize > UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED) {
|
||||
val downloadedEpisodes = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name))
|
||||
if (downloadedEpisodes > cacheSize) return downloadedEpisodes - cacheSize
|
||||
}
|
||||
|
@ -115,8 +108,7 @@ object AutoCleanups {
|
|||
val downloadedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.downloaded.name), EpisodeSortOrder.DATE_NEW_OLD)
|
||||
val idsInQueues = getInQueueEpisodeIds()
|
||||
for (item in downloadedItems) {
|
||||
if (item.media != null && item.media!!.downloaded && !idsInQueues.contains(item.id) && !item.isSUPER)
|
||||
candidates.add(item)
|
||||
if (item.media != null && item.media!!.downloaded && !idsInQueues.contains(item.id) && !item.isSUPER) candidates.add(item)
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
@ -136,13 +128,8 @@ object AutoCleanups {
|
|||
val delete = if (candidates.size > numToRemove) candidates.subList(0, numToRemove) else candidates
|
||||
for (item in delete) {
|
||||
if (item.media == null) continue
|
||||
try {
|
||||
runBlocking { deleteEpisodeMedia(context, item).join() }
|
||||
} catch (e: InterruptedException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: ExecutionException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
try { runBlocking { deleteEpisodeMedia(context, item).join() }
|
||||
} catch (e: InterruptedException) { e.printStackTrace() } catch (e: ExecutionException) { e.printStackTrace() }
|
||||
}
|
||||
val counter = delete.size
|
||||
Log.i(TAG, String.format(Locale.US, "Auto-delete deleted %d episodes (%d requested)", counter, numToRemove))
|
||||
|
@ -203,13 +190,8 @@ object AutoCleanups {
|
|||
}
|
||||
val delete = if (candidates.size > numToRemove) candidates.subList(0, numToRemove) else candidates
|
||||
for (item in delete) {
|
||||
try {
|
||||
runBlocking { deleteEpisodeMedia(context, item).join() }
|
||||
} catch (e: InterruptedException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: ExecutionException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
try { runBlocking { deleteEpisodeMedia(context, item).join() }
|
||||
} catch (e: InterruptedException) { e.printStackTrace() } catch (e: ExecutionException) { e.printStackTrace() }
|
||||
}
|
||||
val counter = delete.size
|
||||
Log.i(TAG, String.format(Locale.US, "Auto-delete deleted %d episodes (%d requested)", counter, numToRemove))
|
||||
|
@ -276,7 +258,7 @@ object AutoCleanups {
|
|||
* @return the number of episodes to delete in order to make room
|
||||
*/
|
||||
fun getNumEpisodesToCleanup(amountOfRoomNeeded: Int): Int {
|
||||
if (amountOfRoomNeeded >= 0 && episodeCacheSize != UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED) {
|
||||
if (amountOfRoomNeeded >= 0 && episodeCacheSize > UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED) {
|
||||
val downloadedEpisodes = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name))
|
||||
if (downloadedEpisodes + amountOfRoomNeeded >= episodeCacheSize) return (downloadedEpisodes + amountOfRoomNeeded - episodeCacheSize)
|
||||
}
|
||||
|
|
|
@ -171,7 +171,7 @@ object AutoDownloads {
|
|||
val autoDownloadableCount = candidates.size
|
||||
val downloadedCount = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name))
|
||||
val deletedCount = AutoCleanups.build().makeRoomForEpisodes(context, autoDownloadableCount)
|
||||
val cacheIsUnlimited = episodeCacheSize == UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED
|
||||
val cacheIsUnlimited = episodeCacheSize <= UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED
|
||||
val allowedCount =
|
||||
if (cacheIsUnlimited || episodeCacheSize >= downloadedCount + autoDownloadableCount) autoDownloadableCount
|
||||
else episodeCacheSize - (downloadedCount - deletedCount)
|
||||
|
|
|
@ -66,9 +66,7 @@ object Feeds {
|
|||
fun buildTags() {
|
||||
val tagsSet = mutableSetOf<String>()
|
||||
val feedsCopy = getFeedList()
|
||||
for (feed in feedsCopy) {
|
||||
if (feed.preferences != null) tagsSet.addAll(feed.preferences!!.tags.filter { it != TAG_ROOT })
|
||||
}
|
||||
for (feed in feedsCopy) if (feed.preferences != null) tagsSet.addAll(feed.preferences!!.tags.filter { it != TAG_ROOT })
|
||||
val newTags = tagsSet - tags.toSet()
|
||||
if (newTags.isNotEmpty()) {
|
||||
tags.clear()
|
||||
|
@ -160,8 +158,7 @@ object Feeds {
|
|||
fun getFeed(feedId: Long, copy: Boolean = false): Feed? {
|
||||
val f = realm.query(Feed::class, "id == $feedId").first().find()
|
||||
return if (f != null) {
|
||||
if (copy) realm.copyFromRealm(f)
|
||||
else f
|
||||
if (copy) realm.copyFromRealm(f) else f
|
||||
} else null
|
||||
}
|
||||
|
||||
|
@ -170,9 +167,7 @@ object Feeds {
|
|||
if (feed.id != 0L) return getFeed(feed.id, copy)
|
||||
val feeds = getFeedList()
|
||||
val feedId = feed.identifyingValue
|
||||
for (f in feeds) {
|
||||
if (f.identifyingValue == feedId) return if (copy) realm.copyFromRealm(f) else f
|
||||
}
|
||||
for (f in feeds) if (f.identifyingValue == feedId) return if (copy) realm.copyFromRealm(f) else f
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -769,12 +764,7 @@ object Feeds {
|
|||
}
|
||||
internal fun canonicalizeTitle(title: String?): String {
|
||||
if (title == null) return ""
|
||||
return title
|
||||
.trim { it <= ' ' }
|
||||
.replace('“', '"')
|
||||
.replace('”', '"')
|
||||
.replace('„', '"')
|
||||
.replace('—', '-')
|
||||
return title.trim { it <= ' ' }.replace('“', '"').replace('”', '"').replace('„', '"').replace('—', '-')
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package ac.mdiq.podcini.storage.database
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||
|
@ -24,8 +25,11 @@ import java.util.*
|
|||
object Queues {
|
||||
private val TAG: String = Queues::class.simpleName ?: "Anonymous"
|
||||
|
||||
enum class EnqueueLocation {
|
||||
BACK, FRONT, AFTER_CURRENTLY_PLAYING, RANDOM
|
||||
enum class EnqueueLocation(val res: Int) {
|
||||
BACK(R.string.enqueue_location_back),
|
||||
FRONT(R.string.enqueue_location_front),
|
||||
AFTER_CURRENTLY_PLAYING(R.string.enqueue_location_after_current),
|
||||
RANDOM(R.string.enqueue_location_random)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -35,6 +35,8 @@ import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
|||
import ac.mdiq.podcini.ui.compose.ChaptersDialog
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.compose.PlaybackSpeedFullDialog
|
||||
import ac.mdiq.podcini.ui.compose.SkipDialog
|
||||
import ac.mdiq.podcini.ui.compose.SkipDirection
|
||||
import ac.mdiq.podcini.ui.dialog.*
|
||||
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
||||
import ac.mdiq.podcini.ui.view.ShownotesWebView
|
||||
|
@ -771,14 +773,36 @@ class VideoplayerActivity : CastEnabledActivity() {
|
|||
binding.sbPosition.setOnSeekBarChangeListener(this)
|
||||
binding.rewindButton.setOnClickListener { onRewind() }
|
||||
binding.rewindButton.setOnLongClickListener {
|
||||
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null)
|
||||
val composeView = ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
val showDialog = remember { mutableStateOf(true) }
|
||||
CustomTheme(requireContext()) {
|
||||
SkipDialog(SkipDirection.SKIP_REWIND, onDismissRequest = {
|
||||
showDialog.value = false
|
||||
(view as? ViewGroup)?.removeView(this@apply)
|
||||
}) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
(view as? ViewGroup)?.addView(composeView)
|
||||
true
|
||||
}
|
||||
binding.playButton.setIsVideoScreen(true)
|
||||
binding.playButton.setOnClickListener { onPlayPause() }
|
||||
binding.fastForwardButton.setOnClickListener { onFastForward() }
|
||||
binding.fastForwardButton.setOnLongClickListener {
|
||||
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null)
|
||||
val composeView = ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
val showDialog = remember { mutableStateOf(true) }
|
||||
CustomTheme(requireContext()) {
|
||||
SkipDialog(SkipDirection.SKIP_FORWARD, onDismissRequest = {
|
||||
showDialog.value = false
|
||||
(view as? ViewGroup)?.removeView(this@apply)
|
||||
}) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
(view as? ViewGroup)?.addView(composeView)
|
||||
false
|
||||
}
|
||||
// To suppress touches directly below the slider
|
||||
|
|
|
@ -1112,11 +1112,11 @@ fun EpisodeSortDialog(initOrder: EpisodeSortOrder, showKeepSorted: Boolean = fal
|
|||
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
val scrollState = rememberScrollState()
|
||||
var sortIndex by remember { mutableIntStateOf(initOrder.ordinal) }
|
||||
var sortIndex by remember { mutableIntStateOf(initOrder.ordinal/2) }
|
||||
var keepSorted by remember { mutableStateOf(false) }
|
||||
Column(Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp).verticalScroll(scrollState)) {
|
||||
NonlazyGrid(columns = 2, itemCount = orderList.size) { index ->
|
||||
var dir by remember { mutableStateOf(true) }
|
||||
var dir by remember { mutableStateOf(if (sortIndex == index) initOrder.ordinal % 2 == 0 else true) }
|
||||
OutlinedButton(modifier = Modifier.padding(2.dp), elevation = null, border = BorderStroke(2.dp, if (sortIndex != index) textColor else Color.Green),
|
||||
onClick = {
|
||||
sortIndex = index
|
||||
|
|
|
@ -16,7 +16,9 @@ import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
|
|||
import ac.mdiq.podcini.storage.database.Feeds.buildTags
|
||||
import ac.mdiq.podcini.storage.database.Feeds.createSynthetic
|
||||
import ac.mdiq.podcini.storage.database.Feeds.deleteFeedSync
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getTags
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsert
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||
import ac.mdiq.podcini.storage.model.*
|
||||
|
@ -278,8 +280,9 @@ fun RenameOrCreateSyntheticFeed(feed_: Feed? = null, onDismissRequest: () -> Uni
|
|||
|
||||
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TagSettingDialog(feeds: List<Feed>, onDismiss: () -> Unit) {
|
||||
fun TagSettingDialog(feeds_: List<Feed>, onDismiss: () -> Unit) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
val feeds = realm.query(Feed::class).query("id IN $0", feeds_.map {it.id}).find()
|
||||
val suggestions = remember { getTags() }
|
||||
val commonTags = remember {
|
||||
if (feeds.size == 1) feeds[0].preferences?.tags?.toMutableStateList()?: mutableStateListOf<String>()
|
||||
|
@ -344,13 +347,13 @@ fun TagSettingDialog(feeds: List<Feed>, onDismiss: () -> Unit) {
|
|||
Button(onClick = { onDismiss() }) { Text("Cancel") }
|
||||
Spacer(Modifier.weight(1f))
|
||||
Button(onClick = {
|
||||
if ((tags.toSet() + commonTags.toSet()).isNotEmpty()) for (f in feeds) upsertBlk(f) {
|
||||
Logd("TagsSettingDialog", "tags: [$tags] commonTags: [$commonTags]")
|
||||
// if (feeds.size == 1) it.preferences?.tags?.clear()
|
||||
// else if (commonTags.isNotEmpty()) it.preferences?.tags?.removeAll(commonTags)
|
||||
if (commonTags.isNotEmpty()) it.preferences?.tags?.removeAll(commonTags)
|
||||
if (tags.isNotEmpty()) it.preferences?.tags?.addAll(tags)
|
||||
if (text.isNotBlank()) it.preferences?.tags?.add(text)
|
||||
Logd("TagsSettingDialog", "tags: [${tags.joinToString()}] commonTags: [${commonTags.joinToString()}]")
|
||||
if ((tags.toSet() + commonTags.toSet()).isNotEmpty() || text.isNotBlank()) {
|
||||
for (f in feeds) upsertBlk(f) {
|
||||
if (commonTags.isNotEmpty()) it.preferences?.tags?.removeAll(commonTags)
|
||||
if (tags.isNotEmpty()) it.preferences?.tags?.addAll(tags)
|
||||
if (text.isNotBlank()) it.preferences?.tags?.add(text)
|
||||
}
|
||||
buildTags()
|
||||
}
|
||||
onDismiss()
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
package ac.mdiq.podcini.ui.compose
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun SkipDialog(direction: SkipDirection, onDismissRequest: ()->Unit, callBack: (Int)->Unit) {
|
||||
val titleRes = if (direction == SkipDirection.SKIP_FORWARD) R.string.pref_fast_forward else R.string.pref_rewind
|
||||
var interval by remember { mutableStateOf((if (direction == SkipDirection.SKIP_FORWARD) UserPreferences.fastForwardSecs else UserPreferences.rewindSecs).toString()) }
|
||||
AlertDialog(onDismissRequest = { onDismissRequest() },
|
||||
title = { Text(stringResource(titleRes), style = MaterialTheme.typography.titleLarge) },
|
||||
text = {
|
||||
TextField(value = interval, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Companion.Number), label = { Text("seconds") }, singleLine = true,
|
||||
onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) interval = it }) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
if (interval.isNotBlank()) {
|
||||
val value = interval.toInt()
|
||||
if (direction == SkipDirection.SKIP_FORWARD) UserPreferences.fastForwardSecs = value
|
||||
else UserPreferences.rewindSecs = value
|
||||
callBack(value)
|
||||
onDismissRequest()
|
||||
}
|
||||
}) { Text(text = "OK") }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { onDismissRequest() }) { Text(text = "Cancel") } }
|
||||
)
|
||||
}
|
||||
|
||||
enum class SkipDirection {
|
||||
SKIP_FORWARD, SKIP_REWIND
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
|
||||
import java.text.NumberFormat
|
||||
import java.util.*
|
||||
|
||||
object SkipPreferenceDialog {
|
||||
fun showSkipPreference(context: Context, direction: SkipDirection, textView: TextView? = null) {
|
||||
var checked = 0
|
||||
val skipSecs = if (direction == SkipDirection.SKIP_FORWARD) fastForwardSecs else rewindSecs
|
||||
|
||||
val values = context.resources.getIntArray(R.array.seek_delta_values)
|
||||
val choices = arrayOfNulls<String>(values.size)
|
||||
for (i in values.indices) {
|
||||
if (skipSecs == values[i]) checked = i
|
||||
choices[i] = String.format(Locale.getDefault(), "%d %s", values[i], context.getString(R.string.time_seconds))
|
||||
}
|
||||
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
builder.setTitle(if (direction == SkipDirection.SKIP_FORWARD) R.string.pref_fast_forward else R.string.pref_rewind)
|
||||
builder.setSingleChoiceItems(choices, checked) { dialog: DialogInterface, _: Int ->
|
||||
val choice = (dialog as AlertDialog).listView.checkedItemPosition
|
||||
if (choice < 0 || choice >= values.size) System.err.printf("Choice in showSkipPreference is out of bounds %d", choice)
|
||||
else {
|
||||
val seconds = values[choice]
|
||||
if (direction == SkipDirection.SKIP_FORWARD) fastForwardSecs = seconds
|
||||
else rewindSecs = seconds
|
||||
if (textView != null) textView.text = NumberFormat.getInstance().format(seconds.toLong())
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
builder.setNegativeButton(R.string.cancel_label, null)
|
||||
builder.show()
|
||||
}
|
||||
|
||||
enum class SkipDirection {
|
||||
SKIP_FORWARD, SKIP_REWIND
|
||||
}
|
||||
}
|
|
@ -40,6 +40,8 @@ import ac.mdiq.podcini.ui.compose.ChaptersDialog
|
|||
import ac.mdiq.podcini.ui.compose.ChooseRatingDialog
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.compose.PlaybackSpeedFullDialog
|
||||
import ac.mdiq.podcini.ui.compose.SkipDialog
|
||||
import ac.mdiq.podcini.ui.compose.SkipDirection
|
||||
import ac.mdiq.podcini.ui.dialog.*
|
||||
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
||||
import ac.mdiq.podcini.ui.view.ShownotesWebView
|
||||
|
@ -62,6 +64,7 @@ import androidx.compose.foundation.layout.*
|
|||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
|
@ -97,7 +100,6 @@ import com.google.android.material.snackbar.Snackbar
|
|||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import net.dankito.readability4j.Readability4J
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.text.DecimalFormat
|
||||
import java.text.NumberFormat
|
||||
import kotlin.math.cos
|
||||
|
@ -259,16 +261,14 @@ class AudioPlayerFragment : Fragment() {
|
|||
}
|
||||
Spacer(Modifier.weight(0.1f))
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
var showSkipDialog by remember { mutableStateOf(false) }
|
||||
var rewindSecs by remember { mutableStateOf(NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong())) }
|
||||
if (showSkipDialog) SkipDialog(SkipDirection.SKIP_REWIND, onDismissRequest = { showSkipDialog = false }) { rewindSecs = it.toString() }
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_fast_rewind), tint = textColor,
|
||||
contentDescription = "rewind",
|
||||
modifier = Modifier.width(43.dp).height(43.dp).combinedClickable(onClick = {
|
||||
// TODO: the check appears not necessary and hurting cast
|
||||
// if (controller != null && playbackService?.isServiceReady() == true)
|
||||
playbackService?.mPlayer?.seekDelta(-UserPreferences.rewindSecs * 1000)
|
||||
}, onLongClick = {
|
||||
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND)
|
||||
}))
|
||||
val rewindSecs = remember { NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong()) }
|
||||
modifier = Modifier.width(43.dp).height(43.dp).combinedClickable(
|
||||
onClick = { playbackService?.mPlayer?.seekDelta(-UserPreferences.rewindSecs * 1000) },
|
||||
onLongClick = { showSkipDialog = true }))
|
||||
Text(rewindSecs, color = textColor, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
Spacer(Modifier.weight(0.1f))
|
||||
|
@ -293,16 +293,14 @@ class AudioPlayerFragment : Fragment() {
|
|||
}))
|
||||
Spacer(Modifier.weight(0.1f))
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
var showSkipDialog by remember { mutableStateOf(false) }
|
||||
var fastForwardSecs by remember { mutableStateOf(NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong())) }
|
||||
if (showSkipDialog) SkipDialog(SkipDirection.SKIP_FORWARD, onDismissRequest = {showSkipDialog = false }) { fastForwardSecs = it.toString()}
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_fast_forward), tint = textColor,
|
||||
contentDescription = "forward",
|
||||
modifier = Modifier.width(43.dp).height(43.dp).combinedClickable(onClick = {
|
||||
// TODO: the check appears not necessary and hurting cast
|
||||
// if (controller != null && playbackService?.isServiceReady() == true)
|
||||
playbackService?.mPlayer?.seekDelta(UserPreferences.fastForwardSecs * 1000)
|
||||
}, onLongClick = {
|
||||
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_FORWARD)
|
||||
}))
|
||||
val fastForwardSecs = remember { NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong()) }
|
||||
modifier = Modifier.width(43.dp).height(43.dp).combinedClickable(
|
||||
onClick = { playbackService?.mPlayer?.seekDelta(UserPreferences.fastForwardSecs * 1000) },
|
||||
onLongClick = { showSkipDialog = true }))
|
||||
Text(fastForwardSecs, color = textColor, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
Spacer(Modifier.weight(0.1f))
|
||||
|
|
|
@ -187,12 +187,13 @@ class FeedEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
}
|
||||
}
|
||||
if (showNewSynthetic) RenameOrCreateSyntheticFeed(feed) {showNewSynthetic = false}
|
||||
if (showSortDialog) EpisodeSortDialog(initOrder = sortOrder, onDismissRequest = {showSortDialog = false}) { sortOrder, _ ->
|
||||
if (showSortDialog) EpisodeSortDialog(initOrder = sortOrder, onDismissRequest = {showSortDialog = false}) { sortOrder_, _ ->
|
||||
if (feed != null) {
|
||||
Logd(TAG, "persist Episode SortOrder")
|
||||
Logd(TAG, "persist Episode SortOrder_")
|
||||
sortOrder = sortOrder_
|
||||
runOnIOScope {
|
||||
val feed_ = realm.query(Feed::class, "id == ${feed!!.id}").first().find()
|
||||
if (feed_ != null) upsert(feed_) { it.sortOrder = sortOrder }
|
||||
if (feed_ != null) feed = upsert(feed_) { it.sortOrder = sortOrder_ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -272,7 +272,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
// tags
|
||||
Column {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
if (showDialog) TagSettingDialog(feeds = listOf(feed!!), onDismiss = { showDialog = false })
|
||||
if (showDialog) TagSettingDialog(feeds_ = listOf(feed!!), onDismiss = { showDialog = false })
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Icon(ImageVector.vectorResource(id = R.drawable.ic_tag), "", tint = textColor)
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
|
|
|
@ -4,10 +4,12 @@ import ac.mdiq.podcini.R
|
|||
import ac.mdiq.podcini.databinding.ComposeFragmentBinding
|
||||
import ac.mdiq.podcini.databinding.DialogSwitchPreferenceBinding
|
||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager
|
||||
import ac.mdiq.podcini.preferences.DocumentFileExportWorker
|
||||
import ac.mdiq.podcini.preferences.ExportTypes
|
||||
import ac.mdiq.podcini.preferences.ExportWorker
|
||||
import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlWriter
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.preferences.fragments.ImportExportPreferencesFragment.*
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getTags
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
|
@ -611,7 +613,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
isExpanded = false
|
||||
selectMode = false
|
||||
Logd(TAG, "baseline_import_export_24: ${selected.size}")
|
||||
val exportType = Export.OPML_SELECTED
|
||||
val exportType = ExportTypes.OPML_SELECTED
|
||||
val title = String.format(exportType.outputNameTemplate, SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date()))
|
||||
val intentPickAction = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
|
|
|
@ -1,123 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:background="?android:attr/colorBackground"
|
||||
android:gravity="top"
|
||||
android:padding="8dp">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/themeSystemCard"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
android:clickable="true"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:layout_weight="1"
|
||||
app:cardElevation="0dp"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:contentPadding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:adjustViewBounds="true"
|
||||
android:src="@drawable/theme_preview_system" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/themeSystemRadio"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:text="@string/pref_theme_title_automatic"
|
||||
android:clickable="false" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/themeLightCard"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
android:clickable="true"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:layout_weight="1"
|
||||
app:cardElevation="0dp"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:contentPadding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:adjustViewBounds="true"
|
||||
android:src="@drawable/theme_preview_light" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/themeLightRadio"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:text="@string/pref_theme_title_light"
|
||||
android:clickable="false" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/themeDarkCard"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
android:clickable="true"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:layout_weight="1"
|
||||
app:cardElevation="0dp"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:contentPadding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:adjustViewBounds="true"
|
||||
android:src="@drawable/theme_preview_dark" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/themeDarkCardRadio"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:text="@string/pref_theme_title_dark"
|
||||
android:clickable="false" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
</LinearLayout>
|
|
@ -110,8 +110,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">ابدا</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">عند عدم التفضيل</string>
|
||||
<string name="episode_cleanup_queue_removal">إذا لم يكن في لائحة الاستماع</string>
|
||||
<string name="episode_cleanup_except_favorite">عند عدم التفضيل</string>
|
||||
<string name="episode_cleanup_not_in_queue">إذا لم يكن في لائحة الاستماع</string>
|
||||
<string name="episode_cleanup_after_listening">بعد الانتهاء</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="zero">%d ساعة بعد الأنتهاء</item>
|
||||
|
@ -390,7 +390,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">التنزيل عند عدم الشحن الجهاز</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">السماح بالتنزيل التلقائي عندما لا يتم شحن البطارية</string>
|
||||
<string name="pref_episode_cache_title">تخزين الحلقات</string>
|
||||
<string name="pref_episode_cache_summary">العدد الإجمالي للحلقات التي تم تنزيلها والمخزنة مؤقتًا على الجهاز. سيتم تعليق التنزيل التلقائي إذا تم الوصول إلى هذا الرقم.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">استخدم صورة غلاف الحلقة</string>
|
||||
<string name="pref_episode_cover_summary">استخدم الغلاف المخصص للحلقة في القوائم إن وجد. إذا لم يتم تحديد هذا الاختيار ، فسيستخدم التطبيق صورة غلاف البودكاست.</string>
|
||||
<string name="pref_show_remain_time_title">أظهر الوقت المتبقي</string>
|
||||
|
|
|
@ -56,8 +56,8 @@
|
|||
<string name="feed_auto_download_always">Siempres</string>
|
||||
<string name="feed_auto_download_never">Enxamás</string>
|
||||
<string name="episode_cleanup_never">Enxamás</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Al nun tar en Favoritos</string>
|
||||
<string name="episode_cleanup_queue_removal">Al nun tar na cola</string>
|
||||
<string name="episode_cleanup_except_favorite">Al nun tar en Favoritos</string>
|
||||
<string name="episode_cleanup_not_in_queue">Al nun tar na cola</string>
|
||||
<string name="episode_cleanup_after_listening">Dempués d\'acabar</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 hora dempués d\'acabar</item>
|
||||
|
@ -182,7 +182,7 @@
|
|||
<string name="pref_autodl_wifi_filter_sum">Namás permite la descarga automática nes redes Wi-Fi esbillaes.</string>
|
||||
<string name="pref_automatic_download_on_battery_title">Baxar al nun tar cargando la batería</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Permite la descarga automática cuando la batería nun tea cargando.</string>
|
||||
<string name="pref_episode_cache_summary">El númberu total d\'episodios baxaos y atroxaos na caché del preséu. La descarga automática va suspendese si s\'algama\'l númberu qu\'afites.</string>
|
||||
|
||||
<string name="pref_episode_cover_summary">Nes llistes, úsase la portada de los episodios siempres que seya posible. Si nun se marca esta opción, l\'aplicación va usar siempres la portada de los podcasts.</string>
|
||||
<string name="pref_show_remain_time_summary">Al activar esta opción, amuesa\'l tiempu que falta de los episodios. Si se desactiva, amuesa la duración total de los episodios.</string>
|
||||
<string name="pref_theme_title_light">Claridá</string>
|
||||
|
|
|
@ -99,8 +99,8 @@
|
|||
<string name="feed_auto_download_always">Bepred</string>
|
||||
<string name="feed_auto_download_never">Morse</string>
|
||||
<string name="episode_cleanup_never">Morse</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Pa n\'emañ ket er sinedoù</string>
|
||||
<string name="episode_cleanup_queue_removal">Pa n\'emañ ket el lost</string>
|
||||
<string name="episode_cleanup_except_favorite">Pa n\'emañ ket er sinedoù</string>
|
||||
<string name="episode_cleanup_not_in_queue">Pa n\'emañ ket el lost</string>
|
||||
<string name="episode_cleanup_after_listening">Goude bezañ echuet</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">%d eur goude bezañ selaouet</item>
|
||||
|
@ -308,7 +308,7 @@
|
|||
<string name="pref_autodl_wifi_filter_sum">Aotren ar pellgargañ emgefreek war ar rouedadoù Wi-Fi diuzet nemetken.</string>
|
||||
<string name="pref_automatic_download_on_battery_title">Pellgargañ pa ne vez ket o kargañ</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Aotren ar pellgargañ emgefreek pa ne vez ket ar benveg o kargañ</string>
|
||||
<string name="pref_episode_cache_summary">Niver hollek a rannoù pellgarget lakaet e krubuilh ar benveg. Diweredekaet e vo ar pellgargañ emgefreek mard eo tizhet an niver-mañ.</string>
|
||||
|
||||
<string name="pref_episode_cover_summary">Ober gant golo ar rann e listennoù pa vez dioutañ. Mard eo digevasket e vo graet gant golo ar podskignad.</string>
|
||||
<string name="pref_show_remain_time_summary">Diskouez amzer ar rannoù a chom mard eo gweredekaet. Ma n\'eo ket e tiskouez padelezh ar rannoù. </string>
|
||||
<string name="pref_theme_title_light">Sklaer</string>
|
||||
|
|
|
@ -106,8 +106,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">Mai</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Quan no és favorit</string>
|
||||
<string name="episode_cleanup_queue_removal">Quan no està a la cua</string>
|
||||
<string name="episode_cleanup_except_favorite">Quan no és favorit</string>
|
||||
<string name="episode_cleanup_not_in_queue">Quan no està a la cua</string>
|
||||
<string name="episode_cleanup_after_listening">Després d\'acabar</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 hora després d\'acabar</item>
|
||||
|
@ -366,7 +366,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">Baixa mentre no es carrega</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Permet les baixades automàtiques mentre la bateria no es carrega</string>
|
||||
<string name="pref_episode_cache_title">Episodis baixats</string>
|
||||
<string name="pref_episode_cache_summary">Nombre total d\'episodis baixats al dispositiu. La baixada automàtica serà suspesa si s\'arriba a aquest nombre.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">Usa la coberta de l\'episodi</string>
|
||||
<string name="pref_episode_cover_summary">Utilitza la portada específica de l\'episodi quan sigui possible. Si es desactiva, l\'aplicació utilitzarà sempre la portada del podcast.</string>
|
||||
<string name="pref_show_remain_time_title">Mostra el temps restant</string>
|
||||
|
|
|
@ -115,8 +115,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">Nikdy</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Pokud není mezi oblíbenými</string>
|
||||
<string name="episode_cleanup_queue_removal">Pokud není ve frontě</string>
|
||||
<string name="episode_cleanup_except_favorite">Pokud není mezi oblíbenými</string>
|
||||
<string name="episode_cleanup_not_in_queue">Pokud není ve frontě</string>
|
||||
<string name="episode_cleanup_after_listening">Po dokončení</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">%d hodinu po dokončení</item>
|
||||
|
@ -398,7 +398,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">Stahovat, pokud neprobíhá nabíjení</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Povolit automatické stahování i pokud není baterie nabíjena</string>
|
||||
<string name="pref_episode_cache_title">Odkládací prostor pro epizody</string>
|
||||
<string name="pref_episode_cache_summary">Celkový počet epizod stažených na zařízení. Automatické stahování se zastaví při dosažení této hodnoty.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">Použít obrázek epizody</string>
|
||||
<string name="pref_episode_cover_summary">Použít v seznamu obrázek přímo z epizody, pokud je k dispozici. Není-li tato možnost zaškrtnuta, tak se vždy použije obrázek podcastu.</string>
|
||||
<string name="pref_show_remain_time_title">Zobrazit zbývající čas</string>
|
||||
|
|
|
@ -115,8 +115,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">Aldrig</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Hvis ikke en favorit</string>
|
||||
<string name="episode_cleanup_queue_removal">Når ikke i kø</string>
|
||||
<string name="episode_cleanup_except_favorite">Hvis ikke en favorit</string>
|
||||
<string name="episode_cleanup_not_in_queue">Når ikke i kø</string>
|
||||
<string name="episode_cleanup_after_listening">Efter færdig afspilning</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 time efter afslutning</item>
|
||||
|
@ -383,7 +383,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">Tillad overførsel ved batteridrift</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Tillad automatisk overførsel, når batteriet ikke oplades</string>
|
||||
<string name="pref_episode_cache_title">Mellemlager for afsnit</string>
|
||||
<string name="pref_episode_cache_summary">Totalt antal af overførte afsnit gemt på din enhed. Automatisk overførsel stilles i bero, når dette nummer nåes.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">Brug afsnitbillede</string>
|
||||
<string name="pref_episode_cover_summary">Brug det afsnitspecifikke cover i lister når muligt. Hvis dette slås fra, vil appen altid bruge podcastens coverbillede.</string>
|
||||
<string name="pref_show_remain_time_title">Vis resterende tid</string>
|
||||
|
|
|
@ -119,8 +119,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">Nie</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Wenn nicht favorisiert</string>
|
||||
<string name="episode_cleanup_queue_removal">Wenn nicht in der Warteschlange</string>
|
||||
<string name="episode_cleanup_except_favorite">Wenn nicht favorisiert</string>
|
||||
<string name="episode_cleanup_not_in_queue">Wenn nicht in der Warteschlange</string>
|
||||
<string name="episode_cleanup_after_listening">Wenn fertig gespielt</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 Stunde nachdem fertig gespielt</item>
|
||||
|
@ -387,7 +387,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">Automatischer Download im Akkubetrieb</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Automatische Downloads auch erlauben, wenn der Akku nicht geladen wird</string>
|
||||
<string name="pref_episode_cache_title">Episodenspeicher</string>
|
||||
<string name="pref_episode_cache_summary">Gesamtzahl der heruntergeladenen Episoden, die auf dem Gerät gespeichert werden. Der automatische Download wird pausiert, wenn diese Anzahl erreicht ist.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">Episoden-Bilder verwenden</string>
|
||||
<string name="pref_episode_cover_summary">Falls verfügbar, episodenspezifische Titelbilder in Listen verwenden. Falls nicht ausgewählt, wird immer das Titelbild des Podcasts verwendet.</string>
|
||||
<string name="pref_show_remain_time_title">Verbleibende Zeit anzeigen</string>
|
||||
|
|
|
@ -118,8 +118,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">Nunca</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Cuando no esté en Favoritos</string>
|
||||
<string name="episode_cleanup_queue_removal">Cuando no esté en la cola</string>
|
||||
<string name="episode_cleanup_except_favorite">Cuando no esté en Favoritos</string>
|
||||
<string name="episode_cleanup_not_in_queue">Cuando no esté en la cola</string>
|
||||
<string name="episode_cleanup_after_listening">Después de acabar</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 hora después de acabar</item>
|
||||
|
@ -389,7 +389,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">Descargar cuando no se está cargando</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Permitir la descarga automática cuando la batería no se esté cargando</string>
|
||||
<string name="pref_episode_cache_title">Caché de episodios</string>
|
||||
<string name="pref_episode_cache_summary">Número total de episodios cacheados en el dispositivo. La descarga automática se suspenderá si se alcanza este número.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">Usar portada del episodio</string>
|
||||
<string name="pref_episode_cover_summary">Usar la portada de cada episodio en las listas cuando sea posible. Si se desactiva, la aplicación siempre usará la portada del podcast.</string>
|
||||
<string name="pref_show_remain_time_title">Mostrar el tiempo restante</string>
|
||||
|
|
|
@ -95,8 +95,8 @@
|
|||
<string name="feed_auto_download_always">Alati</string>
|
||||
<string name="feed_auto_download_never">Mitte kunagi</string>
|
||||
<string name="episode_cleanup_never">Mitte kunagi</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Kui pole lemmik</string>
|
||||
<string name="episode_cleanup_queue_removal">Kui pole järjekorras</string>
|
||||
<string name="episode_cleanup_except_favorite">Kui pole lemmik</string>
|
||||
<string name="episode_cleanup_not_in_queue">Kui pole järjekorras</string>
|
||||
<string name="episode_cleanup_after_listening">Pärast lõpetamist</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 tund pärast lõpetamist</item>
|
||||
|
@ -274,7 +274,7 @@
|
|||
<string name="pref_autodl_wifi_filter_sum">Luba automaatne allalaadimine ainult valitud Wifi võrkudes.</string>
|
||||
<string name="pref_automatic_download_on_battery_title">Allalaadimine, kui seade ei lae</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Luba automaatne allalaadimine ka siis, kui seade pole laadimas</string>
|
||||
<string name="pref_episode_cache_summary">Seadme puhvrisse allalaaditud saadete koguarv. Automaatne allalaadimine peatub, kui selle numbrini jõutakse.</string>
|
||||
|
||||
<string name="pref_episode_cover_summary">Kui võimalik, kasutatakse nimekirjades saate kaanepilti. Kui märkimata, kasutab rakendus alati taskuhäälingu kaanepilti.</string>
|
||||
<string name="pref_show_remain_time_summary">Kui märgitud, kuvatakse saate alles jäänud aega. Kui märkimata, näidatakse saadete kogupikkust.</string>
|
||||
<string name="pref_theme_title_light">Hele</string>
|
||||
|
|
|
@ -109,8 +109,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">Inoiz ez</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Gogoko ez denean</string>
|
||||
<string name="episode_cleanup_queue_removal">Ilaran ez dagoenean</string>
|
||||
<string name="episode_cleanup_except_favorite">Gogoko ez denean</string>
|
||||
<string name="episode_cleanup_not_in_queue">Ilaran ez dagoenean</string>
|
||||
<string name="episode_cleanup_after_listening">Bukatu ondoren</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 ordu bukatu ondoren</item>
|
||||
|
@ -345,7 +345,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">Deskargatu kargatzen ari ez denean</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Onartu deskarga bateria kargatzen ari ez denean</string>
|
||||
<string name="pref_episode_cache_title">Saioen cache-a</string>
|
||||
<string name="pref_episode_cache_summary">Gailuan katxeatutako saioen zenbatekoa. Deskarga automatikoa bertan behera utziko da zenbaki honetara heltzean.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">Erabili saioaren azala</string>
|
||||
<string name="pref_episode_cover_summary">Erabili atalaren azal espezifikoa zerrendetan eskuragarri dagoenean. Desaktibatzen bada, aplikazioak podcastaren azala erabiliko du beti.</string>
|
||||
<string name="pref_show_remain_time_title">Erakutsi geratzen den denbora</string>
|
||||
|
|
|
@ -112,8 +112,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">هرگز</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">وقتی که جزو علاقهمندیها نباشد</string>
|
||||
<string name="episode_cleanup_queue_removal">وقتی که در صف نیست</string>
|
||||
<string name="episode_cleanup_except_favorite">وقتی که جزو علاقهمندیها نباشد</string>
|
||||
<string name="episode_cleanup_not_in_queue">وقتی که در صف نیست</string>
|
||||
<string name="episode_cleanup_after_listening">بعد از تمام شدن</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">۱ ساعت پس از پایان</item>
|
||||
|
@ -367,7 +367,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">بارگیری زمانی که سیستم شارژ نمیشود مجاز باشد</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">وقتی باتری شارژ نمی شود ، امکان بارگیری خودکار وجود داشته باشد</string>
|
||||
<string name="pref_episode_cache_title">انبارهٔ قسمت</string>
|
||||
<string name="pref_episode_cache_summary">تعداد کل قسمتهای بار گرفتهٔ انبار شده روی افزاره. بارگیری خودکار در صورت رسیدن به این عدد معلّق خواهد شد.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">استفاده از جلد قسمت</string>
|
||||
<string name="pref_episode_cover_summary">هر زمان که جلد مخصوص قسمت در دسترس بود از آن استفاده کن. در صورت لغو انتخاب ، برنامه همیشه از تصویر جلد پادکست استفاده می کند.</string>
|
||||
<string name="pref_show_remain_time_title">نمایش زمان مانده</string>
|
||||
|
|
|
@ -105,8 +105,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">Ei koskaan</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Kun ei ole suosikeissa</string>
|
||||
<string name="episode_cleanup_queue_removal">Kun ei ole jonossa</string>
|
||||
<string name="episode_cleanup_except_favorite">Kun ei ole suosikeissa</string>
|
||||
<string name="episode_cleanup_not_in_queue">Kun ei ole jonossa</string>
|
||||
<string name="episode_cleanup_after_listening">Lopetuksen jälkeen</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 hour after finishing</item>
|
||||
|
@ -355,7 +355,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">Lataa, kun akkua ei ladata</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Salli automaattiset lataukset, kun akku ei ole latautumassa</string>
|
||||
<string name="pref_episode_cache_title">Jaksovälimuisti</string>
|
||||
<string name="pref_episode_cache_summary">Ladattuja jaksoja yhteensä välimuistissa tällä laitteella. Automaattinen lataaminen pysäytetään, jos tämä raja ylittyy.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">Käytä jakson kansikuvaa</string>
|
||||
<string name="pref_episode_cover_summary">Käytä jaksokohtaista kansikuvaa luetteloissa aina, kun se on saatavilla. Jos tätä ei ole valittu, sovellus käyttää aina podcastin kansikuvaa.</string>
|
||||
<string name="pref_show_remain_time_title">Näytä jäljellä oleva aika</string>
|
||||
|
|
|
@ -119,8 +119,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">Jamais</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Quand pas un favori</string>
|
||||
<string name="episode_cleanup_queue_removal">Quand pas dans la liste de lecture</string>
|
||||
<string name="episode_cleanup_except_favorite">Quand pas un favori</string>
|
||||
<string name="episode_cleanup_not_in_queue">Quand pas dans la liste de lecture</string>
|
||||
<string name="episode_cleanup_after_listening">Après avoir été écouté</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 heure après avoir été écouté</item>
|
||||
|
@ -391,7 +391,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">Télécharger lorsque l\'appareil n\'est pas en charge</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Autoriser le téléchargement automatique quand l\'appareil n\'est pas en train de charger</string>
|
||||
<string name="pref_episode_cache_title">Nombre d\'épisodes stockés</string>
|
||||
<string name="pref_episode_cache_summary">Nombre maximum d\'épisodes stockés sur l\'appareil. Le téléchargement automatique sera suspendu si ce nombre est atteint.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">Image des épisodes</string>
|
||||
<string name="pref_episode_cover_summary">Utiliser dans les listes les images des épisodes. Sinon l\'image du podcast sera utilisée.</string>
|
||||
<string name="pref_show_remain_time_title">Afficher la durée restante</string>
|
||||
|
|
|
@ -115,8 +115,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">Nunca</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Cando non favorito</string>
|
||||
<string name="episode_cleanup_queue_removal">Cando non esté na cola</string>
|
||||
<string name="episode_cleanup_except_favorite">Cando non favorito</string>
|
||||
<string name="episode_cleanup_not_in_queue">Cando non esté na cola</string>
|
||||
<string name="episode_cleanup_after_listening">Tras rematar</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 hora tras rematar</item>
|
||||
|
@ -382,7 +382,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">Descargar elementos cando non esté cargando</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Permitir a descarga automática cando a batería non está a cargar</string>
|
||||
<string name="pref_episode_cache_title">Caché de episodios</string>
|
||||
<string name="pref_episode_cache_summary">O número total de episodios descargados na caché do dispositivo. A descarga automática suspenderase se se alcanza este número.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">Usar portada do episodio</string>
|
||||
<string name="pref_episode_cover_summary">Usar a capa específica do episodio nas listas cando estivese dispoñible. Sen marcar, a app sempre usará a imaxe de cuberta do podcast.</string>
|
||||
<string name="pref_show_remain_time_title">Mostrar tempo restante</string>
|
||||
|
|
|
@ -98,8 +98,8 @@
|
|||
<string name="feed_auto_download_never">Soha</string>
|
||||
|
||||
<string name="episode_cleanup_never">Soha</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Ha nincs felvéve a kedvencek közé</string>
|
||||
<string name="episode_cleanup_queue_removal">Ha nincs sorbaállítva</string>
|
||||
<string name="episode_cleanup_except_favorite">Ha nincs felvéve a kedvencek közé</string>
|
||||
<string name="episode_cleanup_not_in_queue">Ha nincs sorbaállítva</string>
|
||||
<string name="episode_cleanup_after_listening">Befejezés után</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">befejezés után 1 órával</item>
|
||||
|
@ -285,7 +285,7 @@
|
|||
<string name="pref_autodl_wifi_filter_sum">Automatikus letöltés engedélyezése csak a kiválasztott Wi-Fi hálózatok esetén.</string>
|
||||
<string name="pref_automatic_download_on_battery_title">Letöltés, ha nincs akkumulátortöltés</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Automatikus letöltés engedélyezése, ha az akkumulátor nem töltődik</string>
|
||||
<string name="pref_episode_cache_summary">Az eszközön tárolt letöltött epizódok száma. Az automatikus letöltés felfüggesztésre kerül, ha eléri ezt a számot.</string>
|
||||
|
||||
<string name="pref_episode_cover_summary">Epizódspecifikus borító használata, ha lehetséges. Ha nincs bekapcsolva, akkor az alkalmazás mindig a podcast borítóját fogja használni.</string>
|
||||
<string name="pref_show_remain_time_summary">Ha be van jelölve, akkor megjeleníti az epizódból hátralévő időt. Ha nincs bejelölve, akkor az epizód hosszát jeleníti meg.</string>
|
||||
<string name="pref_theme_title_light">Világos</string>
|
||||
|
|
|
@ -92,8 +92,8 @@
|
|||
<string name="feed_auto_download_always">Selalu</string>
|
||||
<string name="feed_auto_download_never">Tidak pernah</string>
|
||||
<string name="episode_cleanup_never">Tidak pernah</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Saat tidak difavoritkan</string>
|
||||
<string name="episode_cleanup_queue_removal">Ketika tidak dalam antrian</string>
|
||||
<string name="episode_cleanup_except_favorite">Saat tidak difavoritkan</string>
|
||||
<string name="episode_cleanup_not_in_queue">Ketika tidak dalam antrian</string>
|
||||
<string name="episode_cleanup_after_listening">Setelah menyelesaikan</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="other">%d jam setelah menyelesaikan</item>
|
||||
|
@ -299,7 +299,7 @@
|
|||
<string name="pref_autodl_wifi_filter_sum">Izinkan unduh otomatis hanya untuk jaringan Wi-Fi yang terpilih.</string>
|
||||
<string name="pref_automatic_download_on_battery_title">Unduh saat tidak mengisi daya</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Izinkan unduh otomatis saat baterai tidak mengisi daya</string>
|
||||
<string name="pref_episode_cache_summary">Jumlah unduhan episode yang tercached pada perangkat. Unduhan otomatis akan ditangguhkan saat mencapai angka yang ditetapkan.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">Gunakan sampul episode</string>
|
||||
<string name="pref_episode_cover_summary">Gunakan sampul spesifik episode dalam daftar jika bisa. Jika tidak dicentang, aplikasi akan tetap menggunakan sampul podcast.</string>
|
||||
<string name="pref_show_remain_time_title">Tampilkan waktu yang tersisa</string>
|
||||
|
|
|
@ -119,8 +119,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">Mai</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Quando non preferito</string>
|
||||
<string name="episode_cleanup_queue_removal">Quando non è in coda</string>
|
||||
<string name="episode_cleanup_except_favorite">Quando non preferito</string>
|
||||
<string name="episode_cleanup_not_in_queue">Quando non è in coda</string>
|
||||
<string name="episode_cleanup_after_listening">Dopo il completamento</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 ora dal completamento</item>
|
||||
|
@ -391,7 +391,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">Scarica episodi con batteria non in carica</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Permetti il download automatico quando la batteria non è in carica</string>
|
||||
<string name="pref_episode_cache_title">Cache degli episodi</string>
|
||||
<string name="pref_episode_cache_summary">Numero di episodi scaricati memorizzabili sul dispositivo. I download automatici vengono interrotti se si raggiunge questo valore.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">Usa immagine episodio</string>
|
||||
<string name="pref_episode_cover_summary">Usa l\'immagine dell\'episodio quando disponibile. Se disattivato, l\'app userà sempre l\'immagine di copertina del podcast.</string>
|
||||
<string name="pref_show_remain_time_title">Mostra tempo residuo</string>
|
||||
|
|
|
@ -115,8 +115,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">אף פעם</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">כאשר לא במועדפים</string>
|
||||
<string name="episode_cleanup_queue_removal">כאשר לא בתור</string>
|
||||
<string name="episode_cleanup_except_favorite">כאשר לא במועדפים</string>
|
||||
<string name="episode_cleanup_not_in_queue">כאשר לא בתור</string>
|
||||
<string name="episode_cleanup_after_listening">אחרי סיום</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">שעה לאחר הסיום</item>
|
||||
|
@ -394,7 +394,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">להוריד שלא בזמן טעינה</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">לאפשר הורדה אוטומטית כאשר הסוללה אינה בטעינה</string>
|
||||
<string name="pref_episode_cache_title">מטמון פרקים</string>
|
||||
<string name="pref_episode_cache_summary">המספר הכולל של פרקים שהורדו ונשמרים במכשיר. הורדה אוטומטית תושבת אם הכמות הזאת הושגה.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">להשתמש בעטיפת פרק</string>
|
||||
<string name="pref_episode_cover_summary">להשתמש בעטיפת הפרק ברשימות כאשר ניתן. אם האפשרות לא סומנה היישומון ישתמש בתמונת העטיפה של הפודקאסט.</string>
|
||||
<string name="pref_show_remain_time_title">הצגת הזמן שנותר</string>
|
||||
|
|
|
@ -85,8 +85,8 @@
|
|||
<string name="feed_auto_download_always">常に</string>
|
||||
<string name="feed_auto_download_never">しない</string>
|
||||
<string name="episode_cleanup_never">しない</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">お気に入りされていない</string>
|
||||
<string name="episode_cleanup_queue_removal">キューにない時</string>
|
||||
<string name="episode_cleanup_except_favorite">お気に入りされていない</string>
|
||||
<string name="episode_cleanup_not_in_queue">キューにない時</string>
|
||||
<string name="episode_cleanup_after_listening">完了後</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="other">完了後 %d 時間</item>
|
||||
|
@ -261,7 +261,7 @@
|
|||
<string name="pref_autodl_wifi_filter_sum">選択したWi-Fiネットワークに対してのみ自動ダウンロードを許可します。</string>
|
||||
<string name="pref_automatic_download_on_battery_title">充電時以外にダウンロード</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">バッテリーを充電していない時に自動ダウンロードを許可します</string>
|
||||
<string name="pref_episode_cache_summary">デバイスにキャッシュされるダウンロード済エピソードの合計数。この数に達すると自動ダウンロードが中断されます。</string>
|
||||
|
||||
<string name="pref_episode_cover_summary">エピソード固有のカバー画像が利用可能な場合は常にリストで使います。チェックを外すと、アプリは常にポッドキャストのカバー画像を使います。</string>
|
||||
<string name="pref_show_remain_time_summary">チェックをすると、エピソードの残り時間を表示します。チェックを外すと、エピソードの合計時間を表示します。</string>
|
||||
<string name="pref_theme_title_light">ライト</string>
|
||||
|
|
|
@ -105,8 +105,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">안 함</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">즐겨찾기 아닐 때</string>
|
||||
<string name="episode_cleanup_queue_removal">대기열에 없을 때</string>
|
||||
<string name="episode_cleanup_except_favorite">즐겨찾기 아닐 때</string>
|
||||
<string name="episode_cleanup_not_in_queue">대기열에 없을 때</string>
|
||||
<string name="episode_cleanup_after_listening">끝나고 나서</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="other">끝마치고 나서 %d시간</item>
|
||||
|
@ -355,7 +355,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">충전하지 않을 때 다운로드</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">배터리 충전 중이 아닐 때 자동 다운로드 허용</string>
|
||||
<string name="pref_episode_cache_title">에피소드 캐시</string>
|
||||
<string name="pref_episode_cache_summary">장치에 임시 저장한 다운로드한 에피소드의 전체 개수. 이 숫자에 도달하면 자동 다운로드가 지연됩니다.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">에피소드 커버 사용</string>
|
||||
<string name="pref_episode_cover_summary">에피소드마다 설정된 커버가 있으면 그 커버를 사용합니다. 사용하지 않으면, 앱에서는 항상 팟캐스트 커버 이미지를 사용합니다.</string>
|
||||
<string name="pref_show_remain_time_title">남은 시간 표시</string>
|
||||
|
|
|
@ -72,8 +72,8 @@
|
|||
<string name="feed_auto_download_always">Visada</string>
|
||||
<string name="feed_auto_download_never">Niekada</string>
|
||||
<string name="episode_cleanup_never">Niekada</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Kai nėra mėgstamas</string>
|
||||
<string name="episode_cleanup_queue_removal">Jei nėra eilėje</string>
|
||||
<string name="episode_cleanup_except_favorite">Kai nėra mėgstamas</string>
|
||||
<string name="episode_cleanup_not_in_queue">Jei nėra eilėje</string>
|
||||
<string name="episode_cleanup_after_listening">Pabaigus klausyti</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">Praėjus 1 valandai po perklausymo</item>
|
||||
|
@ -252,7 +252,7 @@
|
|||
<string name="pref_autodl_wifi_filter_sum">Leisti automatinį atsiuntimą tik prisijungus prie nurodytų belaidžių tinklų.</string>
|
||||
<string name="pref_automatic_download_on_battery_title">Atsiuntimas ne įkrovimo metu</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Leisti automatinį atsiuntimą kai baterija nėra įkraunama</string>
|
||||
<string name="pref_episode_cache_summary">Bendras podėlyje atsiųstų epizodų skaičius šiame įrenginyje. Pasiekus šį skaičių automatinis atsiuntimas bus pristabdytas.</string>
|
||||
|
||||
<string name="pref_episode_cover_summary">Naudoti epizodo viršelį, kai prieinama. Nepažymėjus, programėlė visada naudos tinklalaidės viršelio paveikslėlį.</string>
|
||||
<string name="pref_show_remain_time_summary">Kai pažymėta, atvaizduojamas likęs epizodų laikas. Jei nepažymėta, atvaizduojama bendra epizodų trukmė.</string>
|
||||
<string name="pref_theme_title_light">Šviesi</string>
|
||||
|
|
|
@ -105,8 +105,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">Aldri</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Hvis ikke favoritt</string>
|
||||
<string name="episode_cleanup_queue_removal">Når ikke i kø</string>
|
||||
<string name="episode_cleanup_except_favorite">Hvis ikke favoritt</string>
|
||||
<string name="episode_cleanup_not_in_queue">Når ikke i kø</string>
|
||||
<string name="episode_cleanup_after_listening">Etter fullført avspilling</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 time etter fullført avspilling</item>
|
||||
|
@ -359,7 +359,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">Last ned når enheten ikke lader</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Tillat automatisk nedlasting når enheten ikke står til lading</string>
|
||||
<string name="pref_episode_cache_title">Mellomlager for episoder</string>
|
||||
<string name="pref_episode_cache_summary">Totalt antall nedlastede episoder bufret på enheten. Automatisk nedlasting vil bli stoppet hvis dette nummeret er nådd.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">Bruk episode-cover</string>
|
||||
<string name="pref_episode_cover_summary">Bruk et episode-spesifikt cover hvis det er tilgjengelig. Hvis dette ikke er valgt vil appen alltid bruke podkastens cover-bilde.</string>
|
||||
<string name="pref_show_remain_time_title">Vis gjenværende tid</string>
|
||||
|
|
|
@ -111,8 +111,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">Nooit</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Indien niet favoriet</string>
|
||||
<string name="episode_cleanup_queue_removal">Indien niet in wachtrij</string>
|
||||
<string name="episode_cleanup_except_favorite">Indien niet favoriet</string>
|
||||
<string name="episode_cleanup_not_in_queue">Indien niet in wachtrij</string>
|
||||
<string name="episode_cleanup_after_listening">Als aflevering is beluisterd</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 uur na afronden</item>
|
||||
|
@ -344,7 +344,7 @@
|
|||
<string name="pref_autodl_wifi_filter_sum">Automatisch downloaden alleen toestaan via gekozen wifi-netwerken.</string>
|
||||
<string name="pref_automatic_download_on_battery_title">Downloaden als toestel niet oplaadt</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Sta automatisch downloaden toe wanneer toestel niet oplaadt</string>
|
||||
<string name="pref_episode_cache_summary">Geef hier het maximum aantal in de cache van het toestel te downloaden afleveringen op. Downloaden stopt na dit aantal.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">Cover van aflevering gebruiken</string>
|
||||
<string name="pref_episode_cover_summary">De aflevering-specifieke omslag in lijsten gebruiken indien beschikbaar. Indien uitgevinkt wordt altijd de podcastomslag afbeelding gebruikt.</string>
|
||||
<string name="pref_show_remain_time_summary">Inschakelen om de resterende tijd te tonen. Anders wordt de totale duur van een aflevering getoond.</string>
|
||||
|
|
|
@ -109,8 +109,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">Nigdy</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Gdy nie oznaczone jako ulubione</string>
|
||||
<string name="episode_cleanup_queue_removal">Kiedy nie są w kolejce</string>
|
||||
<string name="episode_cleanup_except_favorite">Gdy nie oznaczone jako ulubione</string>
|
||||
<string name="episode_cleanup_not_in_queue">Kiedy nie są w kolejce</string>
|
||||
<string name="episode_cleanup_after_listening">Po odtworzeniu</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 godzinę po odtworzeniu</item>
|
||||
|
@ -363,7 +363,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">Pobieraj, gdy nie ładuje</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Zezwól na automatyczne pobieranie, gdy bateria nie jest ładowana.</string>
|
||||
<string name="pref_episode_cache_title">Pamięć podręczna odcinków</string>
|
||||
<string name="pref_episode_cache_summary">Całkowita liczba odcinków zapisanych na urządzeniu. Automatyczne pobieranie zostanie przerwane, jeśli zostanie ona osiągnięta.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">Użyj okładek odcinków</string>
|
||||
<string name="pref_episode_cover_summary">Użyj okładek konkretnych odcinków na listach, kiedy to możliwe. Odznaczenie spowoduje, że aplikacja zawsze będzie używała okładki kanału.</string>
|
||||
<string name="pref_show_remain_time_title">Pokaż pozostały czas</string>
|
||||
|
|
|
@ -108,8 +108,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">Nunca</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Quando não estiver entre os favoritos</string>
|
||||
<string name="episode_cleanup_queue_removal">Quando não está na fila</string>
|
||||
<string name="episode_cleanup_except_favorite">Quando não estiver entre os favoritos</string>
|
||||
<string name="episode_cleanup_not_in_queue">Quando não está na fila</string>
|
||||
<string name="episode_cleanup_after_listening">Depois de concluído</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 hora após finalizar</item>
|
||||
|
@ -357,7 +357,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">Baixar enquanto não está carregando</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Permitir download automático enquanto a bateria não está carregando</string>
|
||||
<string name="pref_episode_cache_title">Cache de episódios</string>
|
||||
<string name="pref_episode_cache_summary">Número total de episódios baixados em cache no dispositivo. O download automático será suspenso se esse número for atingido.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">Usar capa do episódio</string>
|
||||
<string name="pref_episode_cover_summary">Usa a capa específica do episódio em listas, sempre que disponível. Se desmarcada, o aplicativo sempre usará a imagem de capa do podcast.</string>
|
||||
<string name="pref_show_remain_time_summary">Exibe o tempo restante dos episódios, quando marcado. Se estiver desmarcado, exibe a duração total dos episódios.</string>
|
||||
|
|
|
@ -118,8 +118,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">Nunca</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Se não for favorito</string>
|
||||
<string name="episode_cleanup_queue_removal">Se não estiver na fila</string>
|
||||
<string name="episode_cleanup_except_favorite">Se não for favorito</string>
|
||||
<string name="episode_cleanup_not_in_queue">Se não estiver na fila</string>
|
||||
<string name="episode_cleanup_after_listening">Ao terminar</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 hora depois de terminar</item>
|
||||
|
@ -389,7 +389,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">Descarregar se não estiver a carregar</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Permitir descarga automática se a bateria não estiver a ser carregada</string>
|
||||
<string name="pref_episode_cache_title">Cache de episódios</string>
|
||||
<string name="pref_episode_cache_summary">Número máximo de episódios descarregados para colocar em cache. A descarga automática será suspensa se este número for atingido.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">Utilizar imagem do episódio</string>
|
||||
<string name="pref_episode_cover_summary">Utilizar imagem do episódio, se disponível. Se desativar esta opção, será utilizada a imagem do podcast.</string>
|
||||
<string name="pref_show_remain_time_title">Mostrar tempo restante</string>
|
||||
|
|
|
@ -115,8 +115,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">Niciodată</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Cănd nu este preferată</string>
|
||||
<string name="episode_cleanup_queue_removal">Când nu e în coadă</string>
|
||||
<string name="episode_cleanup_except_favorite">Cănd nu este preferată</string>
|
||||
<string name="episode_cleanup_not_in_queue">Când nu e în coadă</string>
|
||||
<string name="episode_cleanup_after_listening">După terminare</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 oră după terminare</item>
|
||||
|
@ -388,7 +388,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">Descarcă când nu se incarcă</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Permite descărcările automate când bateria nu se încarcă</string>
|
||||
<string name="pref_episode_cache_title">Cache de episoade</string>
|
||||
<string name="pref_episode_cache_summary">Numărul total de episoade descărcate pe dispozitiv. Dacă acest număr va fii atins atunci descărcările automate vor fii oprite.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">Folosește coperta episodului</string>
|
||||
<string name="pref_episode_cover_summary">Folosiți coperta specifică episodului în liste când acesta este disponibilă.Dacă opțiunea nu este activată, aplicația va folosi întodeauna coperta podcastului.</string>
|
||||
<string name="pref_show_remain_time_title">Arată timpul rămas</string>
|
||||
|
|
|
@ -105,8 +105,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">никогда</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Когда не в избранном</string>
|
||||
<string name="episode_cleanup_queue_removal">Когда не в очереди</string>
|
||||
<string name="episode_cleanup_except_favorite">Когда не в избранном</string>
|
||||
<string name="episode_cleanup_not_in_queue">Когда не в очереди</string>
|
||||
<string name="episode_cleanup_after_listening">После прослушивания</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 час после прослушивания</item>
|
||||
|
@ -371,7 +371,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">Загружать без зарядки</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Разрешать автоматическую загрузку когда батарея не заряжается</string>
|
||||
<string name="pref_episode_cache_title">Кэш выпусков</string>
|
||||
<string name="pref_episode_cache_summary">Общее количество загруженных в кэш выпусков. По достижении этого количества автоматическая загрузка будет приостановлена.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">Использовать обложку выпуска</string>
|
||||
<string name="pref_episode_cover_summary">Если выпуск содержит свою обложку, показывать в списках её. Если не выбрано, всегда используется обложка подкаста.</string>
|
||||
<string name="pref_show_remain_time_title">Показывать оставшееся время</string>
|
||||
|
|
|
@ -115,8 +115,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">Nikdy</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Ak nie je medzi obľúbenými</string>
|
||||
<string name="episode_cleanup_queue_removal">Ak nie je v poradí</string>
|
||||
<string name="episode_cleanup_except_favorite">Ak nie je medzi obľúbenými</string>
|
||||
<string name="episode_cleanup_not_in_queue">Ak nie je v poradí</string>
|
||||
<string name="episode_cleanup_after_listening">Po dokončení</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 hodinu po dokončení</item>
|
||||
|
@ -393,7 +393,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">Sťahovať keď neprebieha nabíjanie</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Povoliť automatické sťahovanie aj keď nie je batéria nabíjaná</string>
|
||||
<string name="pref_episode_cache_title">Vyrovnávacia pamäť epizód</string>
|
||||
<string name="pref_episode_cache_summary">Celkový počet epizód stiahnutých do zariadenia. Pri dosiahnutí tejto hodnoty sa automatické sťahovanie zastaví.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">Použiť obrázok epizódy</string>
|
||||
<string name="pref_episode_cover_summary">Ak je k dispozícií, použiť v zozname obrázok priamo z epizódy. Ak táto možnost nie je zaškrtnutá, tak sa vždy použije obrázok podcastu.</string>
|
||||
<string name="pref_show_remain_time_title">Zobraziť zostávajúci čas</string>
|
||||
|
|
|
@ -71,8 +71,8 @@
|
|||
<string name="feed_auto_download_always">Vedno</string>
|
||||
<string name="feed_auto_download_never">Nikoli</string>
|
||||
<string name="episode_cleanup_never">Nikoli</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Ko ni dodan med priljubljene</string>
|
||||
<string name="episode_cleanup_queue_removal">Ko ne čaka v čakalni vrsti</string>
|
||||
<string name="episode_cleanup_except_favorite">Ko ni dodan med priljubljene</string>
|
||||
<string name="episode_cleanup_not_in_queue">Ko ne čaka v čakalni vrsti</string>
|
||||
<string name="episode_cleanup_after_listening">Po končanem</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 uro po končanem</item>
|
||||
|
@ -248,7 +248,7 @@
|
|||
<string name="pref_autodl_wifi_filter_sum">Dovoli samodejni prenos samo za izbrana omrežja Wi-Fi.</string>
|
||||
<string name="pref_automatic_download_on_battery_title">Prenesite, ko se ne polni</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Dovoli samodejni prenos, ko se baterija ne polni</string>
|
||||
<string name="pref_episode_cache_summary">Skupno število prenesenih epizod, shranjenih v predpomnilniku naprave. Samodejni prenos bo prekinjen, če bo dosežena ta številka.</string>
|
||||
|
||||
<string name="pref_episode_cover_summary">Uporabite naslovnico za posamezno epizodo na seznamih, kadar je na voljo. Če ta možnost ni izbrana, bo aplikacija vedno uporabljala naslovno sliko podcasta.</string>
|
||||
<string name="pref_show_remain_time_summary">Prikaži preostali čas epizod, ko je izbrano. Če ni potrjeno, prikaže skupno trajanje epizod.</string>
|
||||
<string name="pref_theme_title_light">Svetlo</string>
|
||||
|
|
|
@ -115,8 +115,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">Aldrig</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">När ej favorit</string>
|
||||
<string name="episode_cleanup_queue_removal">Om inte köad</string>
|
||||
<string name="episode_cleanup_except_favorite">När ej favorit</string>
|
||||
<string name="episode_cleanup_not_in_queue">Om inte köad</string>
|
||||
<string name="episode_cleanup_after_listening">Efter färdigspelad</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 timma efter klar</item>
|
||||
|
@ -382,7 +382,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">Nedladdning vid batteridrift</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Tillåt automatisk nedladdning när batteriet inte laddas</string>
|
||||
<string name="pref_episode_cache_title">Episodcache</string>
|
||||
<string name="pref_episode_cache_summary">Totalt antal nedladdade epidoder som ligger i enhetens cache. Automatisk nedladdning kommer att vänta om detta antal nås.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">Använd episodomslag</string>
|
||||
<string name="pref_episode_cover_summary">Använd episodspecifika omslag i listan när de är tillgängliga. Om urkryssat kommer appen alltid använda podcastens omslagsbild.</string>
|
||||
<string name="pref_show_remain_time_title">Visa återstående tid</string>
|
||||
|
|
|
@ -110,8 +110,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">Hiçbir zaman</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Favorilenmemişken</string>
|
||||
<string name="episode_cleanup_queue_removal">Sırada değilse</string>
|
||||
<string name="episode_cleanup_except_favorite">Favorilenmemişken</string>
|
||||
<string name="episode_cleanup_not_in_queue">Sırada değilse</string>
|
||||
<string name="episode_cleanup_after_listening">Bittikten sonra</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">Bittikten 1 saat sonra</item>
|
||||
|
@ -366,7 +366,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">Şarj olmuyorken indir</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Pil şarj olmuyorken otomatik indirmeye izin ver</string>
|
||||
<string name="pref_episode_cache_title">Bölüm önbelleği</string>
|
||||
<string name="pref_episode_cache_summary">Cihazda önbelleğe alınan indirilmiş bölümlerin toplam sayısı. Bu sayıya ulaşılırsa otomatik indirme askıya alınacaktır.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">Bölüm kapağını kullan</string>
|
||||
<string name="pref_episode_cover_summary">Mümkün olduğunda listelerde bölüme özel kapağı kullanın. İşaretli değilse, uygulama her zaman Podcast kapak resmini kullanacaktır.</string>
|
||||
<string name="pref_show_remain_time_title">Kalan süreyi göster</string>
|
||||
|
|
|
@ -115,8 +115,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">Ніколи</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">Коли не вибрано</string>
|
||||
<string name="episode_cleanup_queue_removal">Якщо не в черзі</string>
|
||||
<string name="episode_cleanup_except_favorite">Коли не вибрано</string>
|
||||
<string name="episode_cleanup_not_in_queue">Якщо не в черзі</string>
|
||||
<string name="episode_cleanup_after_listening">Після закінчення</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 година після завершення</item>
|
||||
|
@ -394,7 +394,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">Завантаження без зарядного пристрою</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Дозволити автозавантаження коли зарядний пристрій не підключений</string>
|
||||
<string name="pref_episode_cache_title">Кеш епізодів</string>
|
||||
<string name="pref_episode_cache_summary">Загальна кількість завантажених епізодів, кешованих на пристрої. Якщо цей номер буде досягнутий, автоматичне завантаження буде призупинено.</string>
|
||||
|
||||
<string name="pref_episode_cover_title">Показувати обкладинку епізоду</string>
|
||||
<string name="pref_episode_cover_summary">Використовуйте обкладинку для епізоду у списках, коли це можливо. Якщо цей прапорець не встановлено, програма завжди буде використовувати зображення обкладинки подкасту.</string>
|
||||
<string name="pref_show_remain_time_title">Показати залишок часу</string>
|
||||
|
|
|
@ -119,8 +119,8 @@
|
|||
|
||||
|
||||
<string name="episode_cleanup_never">从不</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">当未收藏时</string>
|
||||
<string name="episode_cleanup_queue_removal">当不在队列中</string>
|
||||
<string name="episode_cleanup_except_favorite">当未收藏时</string>
|
||||
<string name="episode_cleanup_not_in_queue">当不在队列中</string>
|
||||
<string name="episode_cleanup_after_listening">结束后</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="other">结束后 %d 小时</item>
|
||||
|
@ -382,7 +382,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">未充电时下载</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">未充电时允许自动下载</string>
|
||||
<string name="pref_episode_cache_title">节目缓存</string>
|
||||
<string name="pref_episode_cache_summary">缓存在设备上的已下载节目总数 若达到此数目,自动下载将被暂停</string>
|
||||
|
||||
<string name="pref_episode_cover_title">使用节目封面</string>
|
||||
<string name="pref_episode_cover_summary">勾选后,在列表中使用一期节目特定的封面图。如果不勾选,应用程序将始终使用播客封面图像。</string>
|
||||
<string name="pref_show_remain_time_title">显示剩余时间</string>
|
||||
|
|
|
@ -67,8 +67,8 @@
|
|||
<string name="feed_auto_download_always">總是</string>
|
||||
<string name="feed_auto_download_never">不予下載</string>
|
||||
<string name="episode_cleanup_never">不予刪除</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">若未標記為最愛</string>
|
||||
<string name="episode_cleanup_queue_removal">若未列入待播清單</string>
|
||||
<string name="episode_cleanup_except_favorite">若未標記為最愛</string>
|
||||
<string name="episode_cleanup_not_in_queue">若未列入待播清單</string>
|
||||
<string name="episode_cleanup_after_listening">聽完後</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="other">聽完後 %d 小時</item>
|
||||
|
@ -210,7 +210,7 @@
|
|||
<string name="pref_autodl_wifi_filter_sum">限定於特定 Wi-Fi 連線時自動下載</string>
|
||||
<string name="pref_automatic_download_on_battery_title">未充電時下載</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">允許未充電時也自動下載</string>
|
||||
<string name="pref_episode_cache_summary">在本機中可以暫存的集數,若達上限則將停止自動下載。</string>
|
||||
|
||||
<string name="pref_episode_cover_summary">在單集有專屬封面的情況下使用該封面圖。如果取消,則一律使用 Podcast 的封面圖</string>
|
||||
<string name="pref_show_remain_time_summary">勾選時顯示剩餘播放時間,不勾選時顯示單集的總時間。</string>
|
||||
<string name="pref_theme_title_light">淡色</string>
|
||||
|
|
|
@ -1,147 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- <string-array name="feed_refresh_interval_entries">-->
|
||||
<!-- <item>@string/feed_refresh_never</item>-->
|
||||
<!-- <item>@string/feed_every_hour</item>-->
|
||||
<!-- <item>@string/feed_every_2_hours</item>-->
|
||||
<!-- <item>@string/feed_every_4_hours</item>-->
|
||||
<!-- <item>@string/feed_every_8_hours</item>-->
|
||||
<!-- <item>@string/feed_every_12_hours</item>-->
|
||||
<!-- <item>@string/feed_every_24_hours</item>-->
|
||||
<!-- <item>@string/feed_every_72_hours</item>-->
|
||||
<!-- </string-array>-->
|
||||
<!-- <string-array name="feed_refresh_interval_values">-->
|
||||
<!-- <item>0</item>-->
|
||||
<!-- <item>1</item>-->
|
||||
<!-- <item>2</item>-->
|
||||
<!-- <item>4</item>-->
|
||||
<!-- <item>8</item>-->
|
||||
<!-- <item>12</item>-->
|
||||
<!-- <item>24</item>-->
|
||||
<!-- <item>72</item>-->
|
||||
<!-- </string-array>-->
|
||||
|
||||
<!-- <string-array name="smart_mark_as_played_values">-->
|
||||
<!-- <item>0</item>-->
|
||||
<!-- <item>15</item>-->
|
||||
<!-- <item>30</item>-->
|
||||
<!-- <item>60</item>-->
|
||||
<!-- <item>120</item>-->
|
||||
<!-- <item>300</item>-->
|
||||
<!-- </string-array>-->
|
||||
|
||||
<integer-array name="seek_delta_values">
|
||||
<item>5</item>
|
||||
<item>10</item>
|
||||
<item>15</item>
|
||||
<item>20</item>
|
||||
<item>30</item>
|
||||
<item>45</item>
|
||||
<item>60</item>
|
||||
</integer-array>
|
||||
|
||||
<string-array name="episode_cache_size_entries">
|
||||
<item>5</item>
|
||||
<item>10</item>
|
||||
<item>25</item>
|
||||
<item>50</item>
|
||||
<item>100</item>
|
||||
<item>500</item>
|
||||
<item>@string/pref_episode_cache_unlimited</item>
|
||||
</string-array>
|
||||
<string-array name="episode_cache_size_values">
|
||||
<item>5</item>
|
||||
<item>10</item>
|
||||
<item>25</item>
|
||||
<item>50</item>
|
||||
<item>100</item>
|
||||
<item>500</item>
|
||||
<item>-1</item>
|
||||
</string-array>
|
||||
|
||||
<!-- <string-array name="mobile_update_entries">-->
|
||||
<!-- <item>@string/pref_mobileUpdate_refresh</item>-->
|
||||
<!-- <item>@string/pref_mobileUpdate_episode_download</item>-->
|
||||
<!-- <item>@string/pref_mobileUpdate_auto_download</item>-->
|
||||
<!-- <item>@string/pref_mobileUpdate_streaming</item>-->
|
||||
<!-- <item>@string/pref_mobileUpdate_images</item>-->
|
||||
<!-- <item>@string/synchronization_pref</item>-->
|
||||
<!-- </string-array>-->
|
||||
<!-- <string-array name="mobile_update_values">-->
|
||||
<!-- <item>feed_refresh</item>-->
|
||||
<!-- <item>episode_download</item>-->
|
||||
<!-- <item>auto_download</item>-->
|
||||
<!-- <item>streaming</item>-->
|
||||
<!-- <item>images</item>-->
|
||||
<!-- <item>sync</item>-->
|
||||
<!-- </string-array>-->
|
||||
|
||||
<string-array name="mobile_update_default_value">
|
||||
<item>images</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="episode_cleanup_entries">
|
||||
<item>@string/episode_cleanup_except_favorite_removal</item>
|
||||
<item>@string/episode_cleanup_queue_removal</item>
|
||||
<item>0</item>
|
||||
<item>1</item>
|
||||
<item>3</item>
|
||||
<item>5</item>
|
||||
<item>7</item>
|
||||
<item>@string/episode_cleanup_never</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="button_action_options">
|
||||
<item>@string/button_action_fast_forward</item>
|
||||
<item>@string/button_action_rewind</item>
|
||||
<item>@string/button_action_skip_episode</item>
|
||||
<item>@string/button_action_restart_episode</item>
|
||||
</string-array>
|
||||
<string-array name="button_action_values">
|
||||
<item>@string/keycode_media_fast_forward</item>
|
||||
<item>@string/keycode_media_rewind</item>
|
||||
<item>@string/keycode_media_next</item>
|
||||
<item>@string/keycode_media_previous</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="enqueue_location_options">
|
||||
<item>@string/enqueue_location_back</item>
|
||||
<item>@string/enqueue_location_front</item>
|
||||
<item>@string/enqueue_location_after_current</item>
|
||||
<item>@string/enqueue_location_random</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="enqueue_location_values">
|
||||
<!-- MUST be the same as UserPreferences.EnqueueLocation enum -->
|
||||
<item>BACK</item>
|
||||
<item>FRONT</item>
|
||||
<item>AFTER_CURRENTLY_PLAYING</item>
|
||||
<item>RANDOM</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="episode_cleanup_values">
|
||||
<item>-3</item>
|
||||
<item>-1</item>
|
||||
<item>0</item>
|
||||
<item>12</item>
|
||||
<item>24</item>
|
||||
<item>72</item>
|
||||
<item>120</item>
|
||||
<item>168</item>
|
||||
<item>-2</item>
|
||||
</string-array>
|
||||
|
||||
<!-- <string-array name="nav_drawer_titles">-->
|
||||
<!-- <item>@string/subscriptions_label</item>-->
|
||||
<!-- <item>@string/queue_label</item>-->
|
||||
<!-- <item>@string/episodes_label</item>-->
|
||||
<!-- <item>@string/downloads_label</item>-->
|
||||
<!-- <item>@string/shared_log_label</item>-->
|
||||
<!-- <item>@string/playback_history_label</item>-->
|
||||
<!-- <item>@string/statistics_label</item>-->
|
||||
<!-- <item>@string/add_feed_label</item>-->
|
||||
<!-- </string-array>-->
|
||||
|
||||
<string-array name="video_mode_options">
|
||||
<item>@string/pref_video_mode_small_window</item>
|
||||
<item>@string/pref_video_mode_full_screen</item>
|
||||
|
@ -158,25 +16,4 @@
|
|||
<item>@string/next_chapter</item>
|
||||
<item>@string/playback_speed</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="default_page_values">
|
||||
<item>SubscriptionsFragment</item>
|
||||
<item>QueuesFragment</item>
|
||||
<item>AllEpisodesFragment</item>
|
||||
<item>DownloadsFragment</item>
|
||||
<item>PlaybackHistoryFragment</item>
|
||||
<item>AddFeedFragment</item>
|
||||
<item>StatisticsFragment</item>
|
||||
<item>remember</item>
|
||||
</string-array>
|
||||
<string-array name="default_page_titles">
|
||||
<item>@string/subscriptions_label</item>
|
||||
<item>@string/queue_label</item>
|
||||
<item>@string/episodes_label</item>
|
||||
<item>@string/downloads_label</item>
|
||||
<item>@string/playback_history_label</item>
|
||||
<item>@string/add_feed_label</item>
|
||||
<item>@string/statistics_label</item>
|
||||
<item>@string/remember_last_page</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
|
|
@ -169,9 +169,10 @@
|
|||
<string name="remove_from_current_feed">Remove from current feed</string>
|
||||
|
||||
|
||||
<string name="episode_cleanup_limit_by">Limit by</string>
|
||||
<string name="episode_cleanup_never">Never</string>
|
||||
<string name="episode_cleanup_except_favorite_removal">When not favorited</string>
|
||||
<string name="episode_cleanup_queue_removal">When not in queue</string>
|
||||
<string name="episode_cleanup_except_favorite">When not favorited</string>
|
||||
<string name="episode_cleanup_not_in_queue">When not in queue</string>
|
||||
<string name="episode_cleanup_after_listening">After finishing</string>
|
||||
<plurals name="episode_cleanup_hours_after_listening">
|
||||
<item quantity="one">1 hour after finishing</item>
|
||||
|
@ -524,7 +525,7 @@
|
|||
<string name="pref_automatic_download_on_battery_title">Download when not charging</string>
|
||||
<string name="pref_automatic_download_on_battery_sum">Allow automatic download when the battery is not charging</string>
|
||||
<string name="pref_episode_cache_title">Episode cache</string>
|
||||
<string name="pref_episode_cache_summary">Total number of downloaded episodes cached on the device. Automatic download will be suspended if this number is reached.</string>
|
||||
<string name="pref_episode_cache_summary">Total number of downloaded episodes cached on the device (0 means unlimited). Automatic download will be suspended if this number is reached.</string>
|
||||
<string name="pref_auto_download_counting_played_title">Counting played</string>
|
||||
<string name="pref_auto_download_counting_played_summary">Set if downloaded episodes already played count into the episode cache</string>
|
||||
<string name="pref_episode_cover_title">Use episode cover</string>
|
||||
|
@ -742,6 +743,7 @@
|
|||
<string name="sleep_timer_enabled_label">Sleep timer enabled</string>
|
||||
|
||||
<!-- Synchronisation -->
|
||||
<string name="wifi_sync">Sync with device on Wifi</string>
|
||||
<string name="wifi_sync_summary_unchoosen">You can connect two devices on the same wifi network to synchronize your episodes play states</string>
|
||||
<string name="synchronization_choose_title">Choose synchronization provider</string>
|
||||
<string name="synchronization_summary_unchoosen">You can choose from multiple providers to synchronize your subscriptions and episode play state with</string>
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:search="http://schemas.android.com/apk/com.bytehamster.lib.preferencesearch">
|
||||
|
||||
<ac.mdiq.podcini.preferences.MasterSwitchPreference
|
||||
android:key="prefEnableAutoDl"
|
||||
android:title="@string/pref_automatic_download_title"
|
||||
search:summary="@string/pref_automatic_download_sum"
|
||||
android:defaultValue="false"/>
|
||||
<ac.mdiq.podcini.preferences.MaterialListPreference
|
||||
android:defaultValue="25"
|
||||
android:entries="@array/episode_cache_size_entries"
|
||||
android:key="prefEpisodeCacheSize"
|
||||
android:title="@string/pref_episode_cache_title"
|
||||
android:summary="@string/pref_episode_cache_summary"
|
||||
android:entryValues="@array/episode_cache_size_values"/>
|
||||
<ac.mdiq.podcini.preferences.MaterialListPreference
|
||||
android:defaultValue="-1"
|
||||
android:entries="@array/episode_cleanup_entries"
|
||||
android:key="prefEpisodeCleanup"
|
||||
android:title="@string/pref_episode_cleanup_title"
|
||||
android:summary="@string/pref_episode_cleanup_summary"
|
||||
android:entryValues="@array/episode_cleanup_values"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:key="prefEnableAutoDownloadOnBattery"
|
||||
android:title="@string/pref_automatic_download_on_battery_title"
|
||||
android:summary="@string/pref_automatic_download_on_battery_sum"
|
||||
android:defaultValue="true"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:key="prefEnableAutoDownloadWifiFilter"
|
||||
android:title="@string/pref_autodl_wifi_filter_title"
|
||||
android:summary="@string/pref_autodl_wifi_filter_sum"/>
|
||||
</PreferenceScreen>
|
|
@ -1,19 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/notification_group_errors">
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:enabled="true"
|
||||
android:key="prefShowDownloadReport"
|
||||
android:summary="@string/notification_channel_download_error_description"
|
||||
android:title="@string/notification_channel_download_error" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="pref_gpodnet_notifications"
|
||||
android:summary="@string/notification_channel_sync_error_description"
|
||||
android:title="@string/notification_channel_sync_error" />
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
|
@ -1,130 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<PreferenceCategory android:title="@string/interruptions">
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:enabled="true"
|
||||
android:key="prefPauseOnHeadsetDisconnect"
|
||||
android:summary="@string/pref_pauseOnDisconnect_sum"
|
||||
android:title="@string/pref_pauseOnHeadsetDisconnect_title"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:enabled="true"
|
||||
android:dependency="prefPauseOnHeadsetDisconnect"
|
||||
android:key="prefUnpauseOnHeadsetReconnect"
|
||||
android:summary="@string/pref_unpauseOnHeadsetReconnect_sum"
|
||||
android:title="@string/pref_unpauseOnHeadsetReconnect_title"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:enabled="true"
|
||||
android:dependency="prefPauseOnHeadsetDisconnect"
|
||||
android:key="prefUnpauseOnBluetoothReconnect"
|
||||
android:summary="@string/pref_unpauseOnBluetoothReconnect_sum"
|
||||
android:title="@string/pref_unpauseOnBluetoothReconnect_title"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:enabled="true"
|
||||
android:key="prefPauseForFocusLoss"
|
||||
android:summary="@string/pref_pausePlaybackForFocusLoss_sum"
|
||||
android:title="@string/pref_pausePlaybackForFocusLoss_title"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/playback_control">
|
||||
<Preference
|
||||
android:key="prefPlaybackFastForwardDeltaLauncher"
|
||||
android:summary="@string/pref_fast_forward_sum"
|
||||
android:title="@string/pref_fast_forward"/>
|
||||
<Preference
|
||||
android:key="prefPlaybackRewindDeltaLauncher"
|
||||
android:summary="@string/pref_rewind_sum"
|
||||
android:title="@string/pref_rewind"/>
|
||||
<Preference
|
||||
android:key="prefPlaybackSpeedLauncher"
|
||||
android:summary="@string/pref_playback_speed_sum"
|
||||
android:title="@string/playback_speed"/>
|
||||
<Preference
|
||||
android:key="prefPlaybackFallbackSpeedLauncher"
|
||||
android:summary="@string/pref_fallback_speed_sum"
|
||||
android:title="@string/pref_fallback_speed"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="prefPlaybackTimeRespectsSpeed"
|
||||
android:summary="@string/pref_playback_time_respects_speed_sum"
|
||||
android:title="@string/pref_playback_time_respects_speed_title"/>
|
||||
<Preference
|
||||
android:key="prefPlaybackSpeedForwardLauncher"
|
||||
android:summary="@string/pref_speed_forward_sum"
|
||||
android:title="@string/pref_speed_forward"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="prefStreamOverDownload"
|
||||
android:summary="@string/pref_stream_over_download_sum"
|
||||
android:title="@string/pref_stream_over_download_title"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="prefLowQualityOnMobile"
|
||||
android:summary="@string/pref_low_quality_on_mobile_sum"
|
||||
android:title="@string/pref_low_quality_on_mobile_title"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="prefUseAdaptiveProgressUpdate"
|
||||
android:summary="@string/pref_use_adaptive_progress_sum"
|
||||
android:title="@string/pref_use_adaptive_progress_title"/>
|
||||
<Preference
|
||||
android:title="@string/pref_playback_video_mode"
|
||||
android:key="prefPlaybackVideoModeLauncher"
|
||||
android:summary="@string/pref_playback_video_mode_sum"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/reassign_hardware_buttons">
|
||||
<ac.mdiq.podcini.preferences.MaterialListPreference
|
||||
android:defaultValue="@string/keycode_media_fast_forward"
|
||||
android:entries="@array/button_action_options"
|
||||
android:entryValues="@array/button_action_values"
|
||||
android:key="prefHardwareForwardButton"
|
||||
android:title="@string/pref_hardware_forward_button_title"
|
||||
android:summary="@string/pref_hardware_forward_button_summary"/>
|
||||
<ac.mdiq.podcini.preferences.MaterialListPreference
|
||||
android:defaultValue="@string/keycode_media_rewind"
|
||||
android:entries="@array/button_action_options"
|
||||
android:entryValues="@array/button_action_values"
|
||||
android:key="prefHardwarePreviousButton"
|
||||
android:title="@string/pref_hardware_previous_button_title"
|
||||
android:summary="@string/pref_hardware_previous_button_summary"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/queue_label">
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:enabled="true"
|
||||
android:key="prefEnqueueDownloaded"
|
||||
android:summary="@string/pref_enqueue_downloaded_summary"
|
||||
android:title="@string/pref_enqueue_downloaded_title" />
|
||||
<ac.mdiq.podcini.preferences.MaterialListPreference
|
||||
android:defaultValue="BACK"
|
||||
android:entries="@array/enqueue_location_options"
|
||||
android:entryValues="@array/enqueue_location_values"
|
||||
android:key="prefEnqueueLocation"
|
||||
android:title="@string/pref_enqueue_location_title"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:enabled="true"
|
||||
android:key="prefFollowQueue"
|
||||
android:summary="@string/pref_followQueue_sum"
|
||||
android:title="@string/pref_followQueue_title"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:enabled="true"
|
||||
android:key="prefSkipKeepsEpisode"
|
||||
android:summary="@string/pref_skip_keeps_episodes_sum"
|
||||
android:title="@string/pref_skip_keeps_episodes_title"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:enabled="true"
|
||||
android:key="prefRemoveFromQueueMarkedPlayed"
|
||||
android:summary="@string/pref_mark_played_removes_from_queue_sum"
|
||||
android:title="@string/pref_mark_played_removes_from_queue_title"/>
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
|
@ -1,36 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<Preference
|
||||
android:key="preference_instant_sync"
|
||||
android:icon="@drawable/wifi_sync"
|
||||
android:summary="@string/wifi_sync_summary_unchoosen"/>
|
||||
|
||||
<Preference
|
||||
android:key="preference_synchronization_description"
|
||||
android:icon="@drawable/ic_notification_sync"
|
||||
android:summary="@string/synchronization_summary_unchoosen"/>
|
||||
|
||||
<Preference
|
||||
android:key="pref_gpodnet_setlogin_information"
|
||||
android:title="@string/pref_gpodnet_setlogin_information_title"
|
||||
android:summary="@string/pref_gpodnet_setlogin_information_sum"
|
||||
app:isPreferenceVisible="false"/>
|
||||
|
||||
<Preference
|
||||
android:key="pref_synchronization_sync"
|
||||
android:title="@string/synchronization_sync_changes_title"
|
||||
android:summary="@string/synchronization_sync_summary"/>
|
||||
|
||||
<Preference
|
||||
android:key="pref_synchronization_force_full_sync"
|
||||
android:title="@string/synchronization_full_sync_title"
|
||||
android:summary="@string/synchronization_force_sync_summary"/>
|
||||
|
||||
<Preference
|
||||
android:key="pref_synchronization_logout"
|
||||
android:title="@string/synchronization_logout"/>
|
||||
|
||||
</PreferenceScreen>
|
|
@ -1,91 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:search="http://schemas.android.com/apk/com.bytehamster.lib.preferencesearch">
|
||||
|
||||
<PreferenceCategory android:title="@string/appearance">
|
||||
<ac.mdiq.podcini.preferences.ThemePreference
|
||||
android:key="prefTheme" />
|
||||
<SwitchPreferenceCompat
|
||||
android:title="@string/pref_black_theme_title"
|
||||
android:key="prefThemeBlack"
|
||||
android:summary="@string/pref_black_theme_message"
|
||||
android:defaultValue="false" />
|
||||
<SwitchPreferenceCompat
|
||||
android:title="@string/pref_tinted_theme_title"
|
||||
android:key="prefTintedColors"
|
||||
android:summary="@string/pref_tinted_theme_message"
|
||||
android:defaultValue="false" />
|
||||
<Preference
|
||||
android:key="prefHiddenDrawerItems"
|
||||
android:summary="@string/pref_nav_drawer_items_sum"
|
||||
android:title="@string/pref_nav_drawer_items_title"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:title="@string/pref_episode_cover_title"
|
||||
android:key="prefEpisodeCover"
|
||||
android:summary="@string/pref_episode_cover_summary"
|
||||
android:defaultValue="true"
|
||||
android:enabled="true"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:title="@string/pref_show_remain_time_title"
|
||||
android:key="showTimeLeft"
|
||||
android:summary="@string/pref_show_remain_time_summary"
|
||||
android:defaultValue="false"
|
||||
android:enabled="true"/>
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory android:title="@string/subscriptions_label">
|
||||
<!-- <Preference-->
|
||||
<!-- android:title="@string/pref_nav_drawer_feed_order_title"-->
|
||||
<!-- android:key="prefDrawerFeedOrder"-->
|
||||
<!-- android:summary="@string/pref_nav_drawer_feed_order_sum"/>-->
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:enabled="true"
|
||||
android:key="prefSwipeToRefreshAll"
|
||||
android:summary="@string/pref_swipe_refresh_sum"
|
||||
android:title="@string/pref_swipe_refresh_title"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:enabled="true"
|
||||
android:key="prefFeedGridLayout"
|
||||
android:summary="@string/pref_feedGridLayout_sum"
|
||||
android:title="@string/pref_feedGridLayout_title"/>
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory android:title="@string/external_elements">
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:enabled="true"
|
||||
android:key="prefExpandNotify"
|
||||
android:summary="@string/pref_expandNotify_sum"
|
||||
android:title="@string/pref_expandNotify_title"
|
||||
search:ignore="true"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:enabled="true"
|
||||
android:key="prefPersistNotify"
|
||||
android:summary="@string/pref_persistNotify_sum"
|
||||
android:title="@string/pref_persistNotify_title"/>
|
||||
<Preference
|
||||
android:key="prefFullNotificationButtons"
|
||||
android:summary="@string/pref_full_notification_buttons_sum"
|
||||
android:title="@string/pref_full_notification_buttons_title"/>
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory android:title="@string/behavior">
|
||||
<ac.mdiq.podcini.preferences.MaterialListPreference
|
||||
android:entryValues="@array/default_page_values"
|
||||
android:entries="@array/default_page_titles"
|
||||
android:key="prefDefaultPage"
|
||||
android:title="@string/pref_default_page"
|
||||
android:summary="@string/pref_default_page_sum"
|
||||
android:defaultValue="SubscriptionsFragment"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:key="prefBackButtonOpensDrawer"
|
||||
android:title="@string/pref_back_button_opens_drawer"
|
||||
android:summary="@string/pref_back_button_opens_drawer_summary"
|
||||
android:defaultValue="false"/>
|
||||
<Preference
|
||||
android:key="prefSwipe"
|
||||
android:summary="@string/swipeactions_summary"
|
||||
android:title="@string/swipeactions_label"/>
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
10
changelog.md
10
changelog.md
|
@ -1,3 +1,13 @@
|
|||
# 6.14.8
|
||||
|
||||
* fixed issues in tags setting
|
||||
* fixed initial sort order not taking correctly in FeedEpisodes
|
||||
* fixed Compose switches not settable in Preferences
|
||||
* skip dialog is in Compose and features number input rather than multiple choice
|
||||
* made the numbers under rewind and forward buttons in PlayerUI react to changes
|
||||
* all preferences are in Compose except some dialogs
|
||||
* some class restructuring
|
||||
|
||||
# 6.14.7
|
||||
|
||||
* corrected some deeplinks in manifest file on OPMLActivity
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Version 6.14.6
|
||||
Version 6.14.7
|
||||
|
||||
* corrected some deeplinks in manifest file on OPMLActivity
|
||||
* added commentTime in Episode and Feed to record the time of comment/opinion added
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
Version 6.14.8
|
||||
|
||||
* fixed issues in tags setting
|
||||
* fixed initial sort order not taking correctly in FeedEpisodes
|
||||
* fixed Compose switches not settable in Preferences
|
||||
* skip dialog is in Compose and features number input rather than multiple choice
|
||||
* made the numbers under rewind and forward buttons in PlayerUI react to changes
|
||||
* all preferences are in Compose except some dialogs
|
||||
* some class restructuring
|
Loading…
Reference in New Issue