6.14.8 commit

This commit is contained in:
Xilin Jia 2024-11-27 14:24:02 +01:00
parent 6266412a48
commit 0e02b670f0
78 changed files with 3677 additions and 4262 deletions

View File

@ -26,8 +26,8 @@ android {
vectorDrawables.useSupportLibrary false vectorDrawables.useSupportLibrary false
vectorDrawables.generatedDensities = [] vectorDrawables.generatedDensities = []
versionCode 3020306 versionCode 3020307
versionName "6.14.7" versionName "6.14.8"
applicationId "ac.mdiq.podcini.R" applicationId "ac.mdiq.podcini.R"
def commit = "" def commit = ""

View File

@ -44,9 +44,7 @@ class PodciniApp : Application() {
super.onCreate() super.onCreate()
ClientConfig.USER_AGENT = "Podcini/" + BuildConfig.VERSION_NAME ClientConfig.USER_AGENT = "Podcini/" + BuildConfig.VERSION_NAME
ClientConfig.applicationCallbacks = ApplicationCallbacksImpl() ClientConfig.applicationCallbacks = ApplicationCallbacksImpl()
Thread.setDefaultUncaughtExceptionHandler(CrashReportWriter()) Thread.setDefaultUncaughtExceptionHandler(CrashReportWriter())
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
val builder: StrictMode.VmPolicy.Builder = StrictMode.VmPolicy.Builder() val builder: StrictMode.VmPolicy.Builder = StrictMode.VmPolicy.Builder()
.detectAll() .detectAll()
@ -56,7 +54,6 @@ class PodciniApp : Application() {
} }
singleton = this singleton = this
runBlocking { runBlocking {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
ClientConfigurator.initialize(this@PodciniApp) ClientConfigurator.initialize(this@PodciniApp)

View File

@ -33,8 +33,9 @@ object NetworkUtils {
setAllowMobileFor("auto_download", allow) setAllowMobileFor("auto_download", allow)
} }
// not using this
val isEnableAutodownloadWifiFilter: Boolean 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 @JvmStatic
val isAutoDownloadAllowed: Boolean val isAutoDownloadAllowed: Boolean

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,7 +26,6 @@ object UsageStatistics {
/** /**
* Sets up the UsageStatistics class. * Sets up the UsageStatistics class.
*
* @throws IllegalArgumentException if context is null * @throws IllegalArgumentException if context is null
*/ */
@JvmStatic @JvmStatic

View File

@ -23,13 +23,7 @@ import java.net.Proxy
object UserPreferences { object UserPreferences {
private val TAG: String = UserPreferences::class.simpleName ?: "Anonymous" private val TAG: String = UserPreferences::class.simpleName ?: "Anonymous"
// Experimental const val EPISODE_CACHE_SIZE_UNLIMITED: Int = 0
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 DEFAULT_PAGE_REMEMBER: String = "remember" 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 * 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 * EPISODE_CACHE_SIZE_UNLIMITED (0) if the cache size is set to 'unlimited'.
* 'unlimited'.
*/ */
val episodeCacheSize: Int val episodeCacheSize: Int
get() = appPrefs.getString(Prefs.prefEpisodeCacheSize.name, "20")!!.toInt() get() = appPrefs.getString(Prefs.prefEpisodeCacheSize.name, "20")!!.toInt()

View File

@ -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` ?: ""
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
package ac.mdiq.podcini.storage.algorithms package ac.mdiq.podcini.storage.algorithms
import ac.mdiq.podcini.preferences.UserPreferences 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.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.episodeCacheSize import ac.mdiq.podcini.preferences.UserPreferences.episodeCacheSize
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload 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.EpisodeFilter
import ac.mdiq.podcini.storage.model.EpisodeSortOrder import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.model.PlayState import ac.mdiq.podcini.storage.model.PlayState
import ac.mdiq.podcini.ui.activity.PreferenceActivity.AutoDownloadPreferencesFragment.EpisodeCleanupOptions
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
@ -25,7 +25,7 @@ object AutoCleanups {
private val TAG: String = AutoCleanups::class.simpleName ?: "Anonymous" private val TAG: String = AutoCleanups::class.simpleName ?: "Anonymous"
private var episodeCleanupValue: Int 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) { set(episodeCleanupValue) {
appPrefs.edit().putString(UserPreferences.Prefs.prefEpisodeCleanup.name, episodeCleanupValue.toString()).apply() appPrefs.edit().putString(UserPreferences.Prefs.prefEpisodeCleanup.name, episodeCleanupValue.toString()).apply()
} }
@ -46,9 +46,9 @@ object AutoCleanups {
if (!isEnableAutodownload) return APNullCleanupAlgorithm() if (!isEnableAutodownload) return APNullCleanupAlgorithm()
return when (val cleanupValue = episodeCleanupValue) { return when (val cleanupValue = episodeCleanupValue) {
UserPreferences.EPISODE_CLEANUP_EXCEPT_FAVORITE -> ExceptFavoriteCleanupAlgorithm() EpisodeCleanupOptions.ExceptFavorites.num -> ExceptFavoriteCleanupAlgorithm()
UserPreferences.EPISODE_CLEANUP_QUEUE -> APQueueCleanupAlgorithm() EpisodeCleanupOptions.NotInQueue.num -> APQueueCleanupAlgorithm()
EPISODE_CLEANUP_NULL -> APNullCleanupAlgorithm() EpisodeCleanupOptions.Never.num -> APNullCleanupAlgorithm()
else -> APCleanupAlgorithm(cleanupValue) else -> APCleanupAlgorithm(cleanupValue)
} }
} }
@ -61,9 +61,7 @@ object AutoCleanups {
get() { get() {
val candidates: MutableList<Episode> = ArrayList() val candidates: MutableList<Episode> = ArrayList()
val downloadedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.downloaded.name), EpisodeSortOrder.DATE_NEW_OLD) val downloadedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.downloaded.name), EpisodeSortOrder.DATE_NEW_OLD)
for (item in downloadedItems) { for (item in downloadedItems) if (item.media != null && item.media!!.downloaded && !item.isSUPER) candidates.add(item)
if (item.media != null && item.media!!.downloaded && !item.isSUPER) candidates.add(item)
}
return candidates return candidates
} }
override fun getReclaimableItems(): Int { override fun getReclaimableItems(): Int {
@ -82,13 +80,8 @@ object AutoCleanups {
val delete = if (candidates.size > numToRemove) candidates.subList(0, numToRemove) else candidates val delete = if (candidates.size > numToRemove) candidates.subList(0, numToRemove) else candidates
for (item in delete) { for (item in delete) {
if (item.media == null) continue if (item.media == null) continue
try { try { runBlocking { deleteEpisodeMedia(context, item).join() }
runBlocking { deleteEpisodeMedia(context, item).join() } } catch (e: InterruptedException) { e.printStackTrace() } catch (e: ExecutionException) { e.printStackTrace() }
} catch (e: InterruptedException) {
e.printStackTrace()
} catch (e: ExecutionException) {
e.printStackTrace()
}
} }
val counter = delete.size val counter = delete.size
Log.i(TAG, String.format(Locale.US, "Auto-delete deleted %d episodes (%d requested)", counter, numToRemove)) 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 { public override fun getDefaultCleanupParameter(): Int {
val cacheSize = episodeCacheSize 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)) val downloadedEpisodes = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name))
if (downloadedEpisodes > cacheSize) return downloadedEpisodes - cacheSize 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 downloadedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.downloaded.name), EpisodeSortOrder.DATE_NEW_OLD)
val idsInQueues = getInQueueEpisodeIds() val idsInQueues = getInQueueEpisodeIds()
for (item in downloadedItems) { for (item in downloadedItems) {
if (item.media != null && item.media!!.downloaded && !idsInQueues.contains(item.id) && !item.isSUPER) if (item.media != null && item.media!!.downloaded && !idsInQueues.contains(item.id) && !item.isSUPER) candidates.add(item)
candidates.add(item)
} }
return candidates return candidates
} }
@ -136,13 +128,8 @@ object AutoCleanups {
val delete = if (candidates.size > numToRemove) candidates.subList(0, numToRemove) else candidates val delete = if (candidates.size > numToRemove) candidates.subList(0, numToRemove) else candidates
for (item in delete) { for (item in delete) {
if (item.media == null) continue if (item.media == null) continue
try { try { runBlocking { deleteEpisodeMedia(context, item).join() }
runBlocking { deleteEpisodeMedia(context, item).join() } } catch (e: InterruptedException) { e.printStackTrace() } catch (e: ExecutionException) { e.printStackTrace() }
} catch (e: InterruptedException) {
e.printStackTrace()
} catch (e: ExecutionException) {
e.printStackTrace()
}
} }
val counter = delete.size val counter = delete.size
Log.i(TAG, String.format(Locale.US, "Auto-delete deleted %d episodes (%d requested)", counter, numToRemove)) 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 val delete = if (candidates.size > numToRemove) candidates.subList(0, numToRemove) else candidates
for (item in delete) { for (item in delete) {
try { try { runBlocking { deleteEpisodeMedia(context, item).join() }
runBlocking { deleteEpisodeMedia(context, item).join() } } catch (e: InterruptedException) { e.printStackTrace() } catch (e: ExecutionException) { e.printStackTrace() }
} catch (e: InterruptedException) {
e.printStackTrace()
} catch (e: ExecutionException) {
e.printStackTrace()
}
} }
val counter = delete.size val counter = delete.size
Log.i(TAG, String.format(Locale.US, "Auto-delete deleted %d episodes (%d requested)", counter, numToRemove)) 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 * @return the number of episodes to delete in order to make room
*/ */
fun getNumEpisodesToCleanup(amountOfRoomNeeded: Int): Int { 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)) val downloadedEpisodes = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name))
if (downloadedEpisodes + amountOfRoomNeeded >= episodeCacheSize) return (downloadedEpisodes + amountOfRoomNeeded - episodeCacheSize) if (downloadedEpisodes + amountOfRoomNeeded >= episodeCacheSize) return (downloadedEpisodes + amountOfRoomNeeded - episodeCacheSize)
} }

View File

@ -171,7 +171,7 @@ object AutoDownloads {
val autoDownloadableCount = candidates.size val autoDownloadableCount = candidates.size
val downloadedCount = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name)) val downloadedCount = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name))
val deletedCount = AutoCleanups.build().makeRoomForEpisodes(context, autoDownloadableCount) 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 = val allowedCount =
if (cacheIsUnlimited || episodeCacheSize >= downloadedCount + autoDownloadableCount) autoDownloadableCount if (cacheIsUnlimited || episodeCacheSize >= downloadedCount + autoDownloadableCount) autoDownloadableCount
else episodeCacheSize - (downloadedCount - deletedCount) else episodeCacheSize - (downloadedCount - deletedCount)

View File

@ -66,9 +66,7 @@ object Feeds {
fun buildTags() { fun buildTags() {
val tagsSet = mutableSetOf<String>() val tagsSet = mutableSetOf<String>()
val feedsCopy = getFeedList() val feedsCopy = getFeedList()
for (feed in feedsCopy) { for (feed in feedsCopy) if (feed.preferences != null) tagsSet.addAll(feed.preferences!!.tags.filter { it != TAG_ROOT })
if (feed.preferences != null) tagsSet.addAll(feed.preferences!!.tags.filter { it != TAG_ROOT })
}
val newTags = tagsSet - tags.toSet() val newTags = tagsSet - tags.toSet()
if (newTags.isNotEmpty()) { if (newTags.isNotEmpty()) {
tags.clear() tags.clear()
@ -160,8 +158,7 @@ object Feeds {
fun getFeed(feedId: Long, copy: Boolean = false): Feed? { fun getFeed(feedId: Long, copy: Boolean = false): Feed? {
val f = realm.query(Feed::class, "id == $feedId").first().find() val f = realm.query(Feed::class, "id == $feedId").first().find()
return if (f != null) { return if (f != null) {
if (copy) realm.copyFromRealm(f) if (copy) realm.copyFromRealm(f) else f
else f
} else null } else null
} }
@ -170,9 +167,7 @@ object Feeds {
if (feed.id != 0L) return getFeed(feed.id, copy) if (feed.id != 0L) return getFeed(feed.id, copy)
val feeds = getFeedList() val feeds = getFeedList()
val feedId = feed.identifyingValue val feedId = feed.identifyingValue
for (f in feeds) { for (f in feeds) if (f.identifyingValue == feedId) return if (copy) realm.copyFromRealm(f) else f
if (f.identifyingValue == feedId) return if (copy) realm.copyFromRealm(f) else f
}
return null return null
} }
@ -769,12 +764,7 @@ object Feeds {
} }
internal fun canonicalizeTitle(title: String?): String { internal fun canonicalizeTitle(title: String?): String {
if (title == null) return "" if (title == null) return ""
return title return title.trim { it <= ' ' }.replace('“', '"').replace('”', '"').replace('„', '"').replace('—', '-')
.trim { it <= ' ' }
.replace('“', '"')
.replace('”', '"')
.replace('„', '"')
.replace('—', '-')
} }
} }
} }

View File

@ -1,5 +1,6 @@
package ac.mdiq.podcini.storage.database package ac.mdiq.podcini.storage.database
import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.playback.base.InTheatre.curMedia import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.InTheatre.curQueue
@ -24,8 +25,11 @@ import java.util.*
object Queues { object Queues {
private val TAG: String = Queues::class.simpleName ?: "Anonymous" private val TAG: String = Queues::class.simpleName ?: "Anonymous"
enum class EnqueueLocation { enum class EnqueueLocation(val res: Int) {
BACK, FRONT, AFTER_CURRENTLY_PLAYING, RANDOM 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)
} }
/** /**

View File

@ -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.ChaptersDialog
import ac.mdiq.podcini.ui.compose.CustomTheme import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.ui.compose.PlaybackSpeedFullDialog 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.dialog.*
import ac.mdiq.podcini.ui.utils.ShownotesCleaner import ac.mdiq.podcini.ui.utils.ShownotesCleaner
import ac.mdiq.podcini.ui.view.ShownotesWebView import ac.mdiq.podcini.ui.view.ShownotesWebView
@ -771,14 +773,36 @@ class VideoplayerActivity : CastEnabledActivity() {
binding.sbPosition.setOnSeekBarChangeListener(this) binding.sbPosition.setOnSeekBarChangeListener(this)
binding.rewindButton.setOnClickListener { onRewind() } binding.rewindButton.setOnClickListener { onRewind() }
binding.rewindButton.setOnLongClickListener { 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 true
} }
binding.playButton.setIsVideoScreen(true) binding.playButton.setIsVideoScreen(true)
binding.playButton.setOnClickListener { onPlayPause() } binding.playButton.setOnClickListener { onPlayPause() }
binding.fastForwardButton.setOnClickListener { onFastForward() } binding.fastForwardButton.setOnClickListener { onFastForward() }
binding.fastForwardButton.setOnLongClickListener { 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 false
} }
// To suppress touches directly below the slider // To suppress touches directly below the slider

View File

@ -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)) { 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 textColor = MaterialTheme.colorScheme.onSurface
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
var sortIndex by remember { mutableIntStateOf(initOrder.ordinal) } var sortIndex by remember { mutableIntStateOf(initOrder.ordinal/2) }
var keepSorted by remember { mutableStateOf(false) } var keepSorted by remember { mutableStateOf(false) }
Column(Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp).verticalScroll(scrollState)) { Column(Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp).verticalScroll(scrollState)) {
NonlazyGrid(columns = 2, itemCount = orderList.size) { index -> 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), OutlinedButton(modifier = Modifier.padding(2.dp), elevation = null, border = BorderStroke(2.dp, if (sortIndex != index) textColor else Color.Green),
onClick = { onClick = {
sortIndex = index sortIndex = index

View File

@ -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.buildTags
import ac.mdiq.podcini.storage.database.Feeds.createSynthetic import ac.mdiq.podcini.storage.database.Feeds.createSynthetic
import ac.mdiq.podcini.storage.database.Feeds.deleteFeedSync 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.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.upsert
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.* import ac.mdiq.podcini.storage.model.*
@ -278,8 +280,9 @@ fun RenameOrCreateSyntheticFeed(feed_: Feed? = null, onDismissRequest: () -> Uni
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
@Composable @Composable
fun TagSettingDialog(feeds: List<Feed>, onDismiss: () -> Unit) { fun TagSettingDialog(feeds_: List<Feed>, onDismiss: () -> Unit) {
Dialog(onDismissRequest = onDismiss) { Dialog(onDismissRequest = onDismiss) {
val feeds = realm.query(Feed::class).query("id IN $0", feeds_.map {it.id}).find()
val suggestions = remember { getTags() } val suggestions = remember { getTags() }
val commonTags = remember { val commonTags = remember {
if (feeds.size == 1) feeds[0].preferences?.tags?.toMutableStateList()?: mutableStateListOf<String>() 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") } Button(onClick = { onDismiss() }) { Text("Cancel") }
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
Button(onClick = { Button(onClick = {
if ((tags.toSet() + commonTags.toSet()).isNotEmpty()) for (f in feeds) upsertBlk(f) { Logd("TagsSettingDialog", "tags: [${tags.joinToString()}] commonTags: [${commonTags.joinToString()}]")
Logd("TagsSettingDialog", "tags: [$tags] commonTags: [$commonTags]") if ((tags.toSet() + commonTags.toSet()).isNotEmpty() || text.isNotBlank()) {
// if (feeds.size == 1) it.preferences?.tags?.clear() for (f in feeds) upsertBlk(f) {
// else if (commonTags.isNotEmpty()) it.preferences?.tags?.removeAll(commonTags) 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 (tags.isNotEmpty()) it.preferences?.tags?.addAll(tags) if (text.isNotBlank()) it.preferences?.tags?.add(text)
if (text.isNotBlank()) it.preferences?.tags?.add(text) }
buildTags() buildTags()
} }
onDismiss() onDismiss()

View File

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

View File

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

View File

@ -40,6 +40,8 @@ import ac.mdiq.podcini.ui.compose.ChaptersDialog
import ac.mdiq.podcini.ui.compose.ChooseRatingDialog import ac.mdiq.podcini.ui.compose.ChooseRatingDialog
import ac.mdiq.podcini.ui.compose.CustomTheme import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.ui.compose.PlaybackSpeedFullDialog 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.dialog.*
import ac.mdiq.podcini.ui.utils.ShownotesCleaner import ac.mdiq.podcini.ui.utils.ShownotesCleaner
import ac.mdiq.podcini.ui.view.ShownotesWebView 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.foundation.shape.RoundedCornerShape
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
@ -97,7 +100,6 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import net.dankito.readability4j.Readability4J import net.dankito.readability4j.Readability4J
import org.apache.commons.lang3.StringUtils
import java.text.DecimalFormat import java.text.DecimalFormat
import java.text.NumberFormat import java.text.NumberFormat
import kotlin.math.cos import kotlin.math.cos
@ -259,16 +261,14 @@ class AudioPlayerFragment : Fragment() {
} }
Spacer(Modifier.weight(0.1f)) Spacer(Modifier.weight(0.1f))
Column(horizontalAlignment = Alignment.CenterHorizontally) { 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, Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_fast_rewind), tint = textColor,
contentDescription = "rewind", contentDescription = "rewind",
modifier = Modifier.width(43.dp).height(43.dp).combinedClickable(onClick = { modifier = Modifier.width(43.dp).height(43.dp).combinedClickable(
// TODO: the check appears not necessary and hurting cast onClick = { playbackService?.mPlayer?.seekDelta(-UserPreferences.rewindSecs * 1000) },
// if (controller != null && playbackService?.isServiceReady() == true) onLongClick = { showSkipDialog = true }))
playbackService?.mPlayer?.seekDelta(-UserPreferences.rewindSecs * 1000)
}, onLongClick = {
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND)
}))
val rewindSecs = remember { NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong()) }
Text(rewindSecs, color = textColor, style = MaterialTheme.typography.bodySmall) Text(rewindSecs, color = textColor, style = MaterialTheme.typography.bodySmall)
} }
Spacer(Modifier.weight(0.1f)) Spacer(Modifier.weight(0.1f))
@ -293,16 +293,14 @@ class AudioPlayerFragment : Fragment() {
})) }))
Spacer(Modifier.weight(0.1f)) Spacer(Modifier.weight(0.1f))
Column(horizontalAlignment = Alignment.CenterHorizontally) { 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, Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_fast_forward), tint = textColor,
contentDescription = "forward", contentDescription = "forward",
modifier = Modifier.width(43.dp).height(43.dp).combinedClickable(onClick = { modifier = Modifier.width(43.dp).height(43.dp).combinedClickable(
// TODO: the check appears not necessary and hurting cast onClick = { playbackService?.mPlayer?.seekDelta(UserPreferences.fastForwardSecs * 1000) },
// if (controller != null && playbackService?.isServiceReady() == true) onLongClick = { showSkipDialog = true }))
playbackService?.mPlayer?.seekDelta(UserPreferences.fastForwardSecs * 1000)
}, onLongClick = {
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_FORWARD)
}))
val fastForwardSecs = remember { NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong()) }
Text(fastForwardSecs, color = textColor, style = MaterialTheme.typography.bodySmall) Text(fastForwardSecs, color = textColor, style = MaterialTheme.typography.bodySmall)
} }
Spacer(Modifier.weight(0.1f)) Spacer(Modifier.weight(0.1f))

View File

@ -187,12 +187,13 @@ class FeedEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
} }
} }
if (showNewSynthetic) RenameOrCreateSyntheticFeed(feed) {showNewSynthetic = false} 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) { if (feed != null) {
Logd(TAG, "persist Episode SortOrder") Logd(TAG, "persist Episode SortOrder_")
sortOrder = sortOrder_
runOnIOScope { runOnIOScope {
val feed_ = realm.query(Feed::class, "id == ${feed!!.id}").first().find() 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_ }
} }
} }
} }

View File

@ -272,7 +272,7 @@ class FeedSettingsFragment : Fragment() {
// tags // tags
Column { Column {
var showDialog by remember { mutableStateOf(false) } 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()) { Row(Modifier.fillMaxWidth()) {
Icon(ImageVector.vectorResource(id = R.drawable.ic_tag), "", tint = textColor) Icon(ImageVector.vectorResource(id = R.drawable.ic_tag), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp)) Spacer(modifier = Modifier.width(20.dp))

View File

@ -4,10 +4,12 @@ import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.ComposeFragmentBinding import ac.mdiq.podcini.databinding.ComposeFragmentBinding
import ac.mdiq.podcini.databinding.DialogSwitchPreferenceBinding import ac.mdiq.podcini.databinding.DialogSwitchPreferenceBinding
import ac.mdiq.podcini.net.feed.FeedUpdateManager 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.OpmlTransporter.OpmlWriter
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs 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.getFeedList
import ac.mdiq.podcini.storage.database.Feeds.getTags import ac.mdiq.podcini.storage.database.Feeds.getTags
import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.realm
@ -611,7 +613,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
isExpanded = false isExpanded = false
selectMode = false selectMode = false
Logd(TAG, "baseline_import_export_24: ${selected.size}") 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 title = String.format(exportType.outputNameTemplate, SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date()))
val intentPickAction = Intent(Intent.ACTION_CREATE_DOCUMENT) val intentPickAction = Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE) .addCategory(Intent.CATEGORY_OPENABLE)

View File

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

View File

@ -110,8 +110,8 @@
<string name="episode_cleanup_never">ابدا</string> <string name="episode_cleanup_never">ابدا</string>
<string name="episode_cleanup_except_favorite_removal">عند عدم التفضيل</string> <string name="episode_cleanup_except_favorite">عند عدم التفضيل</string>
<string name="episode_cleanup_queue_removal">إذا لم يكن في لائحة الاستماع</string> <string name="episode_cleanup_not_in_queue">إذا لم يكن في لائحة الاستماع</string>
<string name="episode_cleanup_after_listening">بعد الانتهاء</string> <string name="episode_cleanup_after_listening">بعد الانتهاء</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="zero">%d ساعة بعد الأنتهاء</item> <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_title">التنزيل عند عدم الشحن الجهاز</string>
<string name="pref_automatic_download_on_battery_sum">السماح بالتنزيل التلقائي عندما لا يتم شحن البطارية</string> <string name="pref_automatic_download_on_battery_sum">السماح بالتنزيل التلقائي عندما لا يتم شحن البطارية</string>
<string name="pref_episode_cache_title">تخزين الحلقات</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_title">استخدم صورة غلاف الحلقة</string>
<string name="pref_episode_cover_summary">استخدم الغلاف المخصص للحلقة في القوائم إن وجد. إذا لم يتم تحديد هذا الاختيار ، فسيستخدم التطبيق صورة غلاف البودكاست.</string> <string name="pref_episode_cover_summary">استخدم الغلاف المخصص للحلقة في القوائم إن وجد. إذا لم يتم تحديد هذا الاختيار ، فسيستخدم التطبيق صورة غلاف البودكاست.</string>
<string name="pref_show_remain_time_title">أظهر الوقت المتبقي</string> <string name="pref_show_remain_time_title">أظهر الوقت المتبقي</string>

View File

@ -56,8 +56,8 @@
<string name="feed_auto_download_always">Siempres</string> <string name="feed_auto_download_always">Siempres</string>
<string name="feed_auto_download_never">Enxamás</string> <string name="feed_auto_download_never">Enxamás</string>
<string name="episode_cleanup_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_except_favorite">Al nun tar en Favoritos</string>
<string name="episode_cleanup_queue_removal">Al nun tar na cola</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> <string name="episode_cleanup_after_listening">Dempués d\'acabar</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 hora dempués d\'acabar</item> <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_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_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_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_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_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> <string name="pref_theme_title_light">Claridá</string>

View File

@ -99,8 +99,8 @@
<string name="feed_auto_download_always">Bepred</string> <string name="feed_auto_download_always">Bepred</string>
<string name="feed_auto_download_never">Morse</string> <string name="feed_auto_download_never">Morse</string>
<string name="episode_cleanup_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_except_favorite">Pa n\'emañ ket er sinedoù</string>
<string name="episode_cleanup_queue_removal">Pa n\'emañ ket el lost</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> <string name="episode_cleanup_after_listening">Goude bezañ echuet</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">%d eur goude bezañ selaouet</item> <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_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_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_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_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_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> <string name="pref_theme_title_light">Sklaer</string>

View File

@ -106,8 +106,8 @@
<string name="episode_cleanup_never">Mai</string> <string name="episode_cleanup_never">Mai</string>
<string name="episode_cleanup_except_favorite_removal">Quan no és favorit</string> <string name="episode_cleanup_except_favorite">Quan no és favorit</string>
<string name="episode_cleanup_queue_removal">Quan no està a la cua</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> <string name="episode_cleanup_after_listening">Després d\'acabar</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 hora després d\'acabar</item> <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_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_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_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_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_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> <string name="pref_show_remain_time_title">Mostra el temps restant</string>

View File

@ -115,8 +115,8 @@
<string name="episode_cleanup_never">Nikdy</string> <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_except_favorite">Pokud není mezi oblíbenými</string>
<string name="episode_cleanup_queue_removal">Pokud není ve frontě</string> <string name="episode_cleanup_not_in_queue">Pokud není ve frontě</string>
<string name="episode_cleanup_after_listening">Po dokončení</string> <string name="episode_cleanup_after_listening">Po dokončení</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">%d hodinu po dokončení</item> <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_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_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_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_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_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> <string name="pref_show_remain_time_title">Zobrazit zbývající čas</string>

View File

@ -115,8 +115,8 @@
<string name="episode_cleanup_never">Aldrig</string> <string name="episode_cleanup_never">Aldrig</string>
<string name="episode_cleanup_except_favorite_removal">Hvis ikke en favorit</string> <string name="episode_cleanup_except_favorite">Hvis ikke en favorit</string>
<string name="episode_cleanup_queue_removal">Når ikke i kø</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> <string name="episode_cleanup_after_listening">Efter færdig afspilning</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 time efter afslutning</item> <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_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_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_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_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_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> <string name="pref_show_remain_time_title">Vis resterende tid</string>

View File

@ -119,8 +119,8 @@
<string name="episode_cleanup_never">Nie</string> <string name="episode_cleanup_never">Nie</string>
<string name="episode_cleanup_except_favorite_removal">Wenn nicht favorisiert</string> <string name="episode_cleanup_except_favorite">Wenn nicht favorisiert</string>
<string name="episode_cleanup_queue_removal">Wenn nicht in der Warteschlange</string> <string name="episode_cleanup_not_in_queue">Wenn nicht in der Warteschlange</string>
<string name="episode_cleanup_after_listening">Wenn fertig gespielt</string> <string name="episode_cleanup_after_listening">Wenn fertig gespielt</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 Stunde nachdem fertig gespielt</item> <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_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_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_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_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_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> <string name="pref_show_remain_time_title">Verbleibende Zeit anzeigen</string>

View File

@ -118,8 +118,8 @@
<string name="episode_cleanup_never">Nunca</string> <string name="episode_cleanup_never">Nunca</string>
<string name="episode_cleanup_except_favorite_removal">Cuando no esté en Favoritos</string> <string name="episode_cleanup_except_favorite">Cuando no esté en Favoritos</string>
<string name="episode_cleanup_queue_removal">Cuando no esté en la cola</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> <string name="episode_cleanup_after_listening">Después de acabar</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 hora después de acabar</item> <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_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_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_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_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_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> <string name="pref_show_remain_time_title">Mostrar el tiempo restante</string>

View File

@ -95,8 +95,8 @@
<string name="feed_auto_download_always">Alati</string> <string name="feed_auto_download_always">Alati</string>
<string name="feed_auto_download_never">Mitte kunagi</string> <string name="feed_auto_download_never">Mitte kunagi</string>
<string name="episode_cleanup_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_except_favorite">Kui pole lemmik</string>
<string name="episode_cleanup_queue_removal">Kui pole järjekorras</string> <string name="episode_cleanup_not_in_queue">Kui pole järjekorras</string>
<string name="episode_cleanup_after_listening">Pärast lõpetamist</string> <string name="episode_cleanup_after_listening">Pärast lõpetamist</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 tund pärast lõpetamist</item> <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_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_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_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_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_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> <string name="pref_theme_title_light">Hele</string>

View File

@ -109,8 +109,8 @@
<string name="episode_cleanup_never">Inoiz ez</string> <string name="episode_cleanup_never">Inoiz ez</string>
<string name="episode_cleanup_except_favorite_removal">Gogoko ez denean</string> <string name="episode_cleanup_except_favorite">Gogoko ez denean</string>
<string name="episode_cleanup_queue_removal">Ilaran ez dagoenean</string> <string name="episode_cleanup_not_in_queue">Ilaran ez dagoenean</string>
<string name="episode_cleanup_after_listening">Bukatu ondoren</string> <string name="episode_cleanup_after_listening">Bukatu ondoren</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 ordu bukatu ondoren</item> <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_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_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_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_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_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> <string name="pref_show_remain_time_title">Erakutsi geratzen den denbora</string>

View File

@ -112,8 +112,8 @@
<string name="episode_cleanup_never">هرگز</string> <string name="episode_cleanup_never">هرگز</string>
<string name="episode_cleanup_except_favorite_removal">وقتی که جزو علاقه‌مندی‌ها نباشد</string> <string name="episode_cleanup_except_favorite">وقتی که جزو علاقه‌مندی‌ها نباشد</string>
<string name="episode_cleanup_queue_removal">وقتی که در صف نیست</string> <string name="episode_cleanup_not_in_queue">وقتی که در صف نیست</string>
<string name="episode_cleanup_after_listening">بعد از تمام شدن</string> <string name="episode_cleanup_after_listening">بعد از تمام شدن</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">۱ ساعت پس از پایان</item> <item quantity="one">۱ ساعت پس از پایان</item>
@ -367,7 +367,7 @@
<string name="pref_automatic_download_on_battery_title">بارگیری زمانی که سیستم شارژ نمیشود مجاز باشد</string> <string name="pref_automatic_download_on_battery_title">بارگیری زمانی که سیستم شارژ نمیشود مجاز باشد</string>
<string name="pref_automatic_download_on_battery_sum">وقتی باتری شارژ نمی شود ، امکان بارگیری خودکار وجود داشته باشد</string> <string name="pref_automatic_download_on_battery_sum">وقتی باتری شارژ نمی شود ، امکان بارگیری خودکار وجود داشته باشد</string>
<string name="pref_episode_cache_title">انبارهٔ قسمت</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_title">استفاده از جلد قسمت</string>
<string name="pref_episode_cover_summary">هر زمان که جلد مخصوص قسمت در دسترس بود از آن استفاده کن. در صورت لغو انتخاب ، برنامه همیشه از تصویر جلد پادکست استفاده می کند.</string> <string name="pref_episode_cover_summary">هر زمان که جلد مخصوص قسمت در دسترس بود از آن استفاده کن. در صورت لغو انتخاب ، برنامه همیشه از تصویر جلد پادکست استفاده می کند.</string>
<string name="pref_show_remain_time_title">نمایش زمان مانده</string> <string name="pref_show_remain_time_title">نمایش زمان مانده</string>

View File

@ -105,8 +105,8 @@
<string name="episode_cleanup_never">Ei koskaan</string> <string name="episode_cleanup_never">Ei koskaan</string>
<string name="episode_cleanup_except_favorite_removal">Kun ei ole suosikeissa</string> <string name="episode_cleanup_except_favorite">Kun ei ole suosikeissa</string>
<string name="episode_cleanup_queue_removal">Kun ei ole jonossa</string> <string name="episode_cleanup_not_in_queue">Kun ei ole jonossa</string>
<string name="episode_cleanup_after_listening">Lopetuksen jälkeen</string> <string name="episode_cleanup_after_listening">Lopetuksen jälkeen</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 hour after finishing</item> <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_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_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_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_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_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> <string name="pref_show_remain_time_title">Näytä jäljellä oleva aika</string>

View File

@ -119,8 +119,8 @@
<string name="episode_cleanup_never">Jamais</string> <string name="episode_cleanup_never">Jamais</string>
<string name="episode_cleanup_except_favorite_removal">Quand pas un favori</string> <string name="episode_cleanup_except_favorite">Quand pas un favori</string>
<string name="episode_cleanup_queue_removal">Quand pas dans la liste de lecture</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> <string name="episode_cleanup_after_listening">Après avoir été écouté</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 heure après avoir été écouté</item> <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_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_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_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_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_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> <string name="pref_show_remain_time_title">Afficher la durée restante</string>

View File

@ -115,8 +115,8 @@
<string name="episode_cleanup_never">Nunca</string> <string name="episode_cleanup_never">Nunca</string>
<string name="episode_cleanup_except_favorite_removal">Cando non favorito</string> <string name="episode_cleanup_except_favorite">Cando non favorito</string>
<string name="episode_cleanup_queue_removal">Cando non esté na cola</string> <string name="episode_cleanup_not_in_queue">Cando non esté na cola</string>
<string name="episode_cleanup_after_listening">Tras rematar</string> <string name="episode_cleanup_after_listening">Tras rematar</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 hora tras rematar</item> <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_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_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_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_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_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> <string name="pref_show_remain_time_title">Mostrar tempo restante</string>

View File

@ -98,8 +98,8 @@
<string name="feed_auto_download_never">Soha</string> <string name="feed_auto_download_never">Soha</string>
<string name="episode_cleanup_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_except_favorite">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_not_in_queue">Ha nincs sorbaállítva</string>
<string name="episode_cleanup_after_listening">Befejezés után</string> <string name="episode_cleanup_after_listening">Befejezés után</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">befejezés után 1 órával</item> <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_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_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_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_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_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> <string name="pref_theme_title_light">Világos</string>

View File

@ -92,8 +92,8 @@
<string name="feed_auto_download_always">Selalu</string> <string name="feed_auto_download_always">Selalu</string>
<string name="feed_auto_download_never">Tidak pernah</string> <string name="feed_auto_download_never">Tidak pernah</string>
<string name="episode_cleanup_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_except_favorite">Saat tidak difavoritkan</string>
<string name="episode_cleanup_queue_removal">Ketika tidak dalam antrian</string> <string name="episode_cleanup_not_in_queue">Ketika tidak dalam antrian</string>
<string name="episode_cleanup_after_listening">Setelah menyelesaikan</string> <string name="episode_cleanup_after_listening">Setelah menyelesaikan</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="other">%d jam setelah menyelesaikan</item> <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_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_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_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_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_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> <string name="pref_show_remain_time_title">Tampilkan waktu yang tersisa</string>

View File

@ -119,8 +119,8 @@
<string name="episode_cleanup_never">Mai</string> <string name="episode_cleanup_never">Mai</string>
<string name="episode_cleanup_except_favorite_removal">Quando non preferito</string> <string name="episode_cleanup_except_favorite">Quando non preferito</string>
<string name="episode_cleanup_queue_removal">Quando non è in coda</string> <string name="episode_cleanup_not_in_queue">Quando non è in coda</string>
<string name="episode_cleanup_after_listening">Dopo il completamento</string> <string name="episode_cleanup_after_listening">Dopo il completamento</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 ora dal completamento</item> <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_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_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_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_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_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> <string name="pref_show_remain_time_title">Mostra tempo residuo</string>

View File

@ -115,8 +115,8 @@
<string name="episode_cleanup_never">אף פעם</string> <string name="episode_cleanup_never">אף פעם</string>
<string name="episode_cleanup_except_favorite_removal">כאשר לא במועדפים</string> <string name="episode_cleanup_except_favorite">כאשר לא במועדפים</string>
<string name="episode_cleanup_queue_removal">כאשר לא בתור</string> <string name="episode_cleanup_not_in_queue">כאשר לא בתור</string>
<string name="episode_cleanup_after_listening">אחרי סיום</string> <string name="episode_cleanup_after_listening">אחרי סיום</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">שעה לאחר הסיום</item> <item quantity="one">שעה לאחר הסיום</item>
@ -394,7 +394,7 @@
<string name="pref_automatic_download_on_battery_title">להוריד שלא בזמן טעינה</string> <string name="pref_automatic_download_on_battery_title">להוריד שלא בזמן טעינה</string>
<string name="pref_automatic_download_on_battery_sum">לאפשר הורדה אוטומטית כאשר הסוללה אינה בטעינה</string> <string name="pref_automatic_download_on_battery_sum">לאפשר הורדה אוטומטית כאשר הסוללה אינה בטעינה</string>
<string name="pref_episode_cache_title">מטמון פרקים</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_title">להשתמש בעטיפת פרק</string>
<string name="pref_episode_cover_summary">להשתמש בעטיפת הפרק ברשימות כאשר ניתן. אם האפשרות לא סומנה היישומון ישתמש בתמונת העטיפה של הפודקאסט.</string> <string name="pref_episode_cover_summary">להשתמש בעטיפת הפרק ברשימות כאשר ניתן. אם האפשרות לא סומנה היישומון ישתמש בתמונת העטיפה של הפודקאסט.</string>
<string name="pref_show_remain_time_title">הצגת הזמן שנותר</string> <string name="pref_show_remain_time_title">הצגת הזמן שנותר</string>

View File

@ -85,8 +85,8 @@
<string name="feed_auto_download_always">常に</string> <string name="feed_auto_download_always">常に</string>
<string name="feed_auto_download_never">しない</string> <string name="feed_auto_download_never">しない</string>
<string name="episode_cleanup_never">しない</string> <string name="episode_cleanup_never">しない</string>
<string name="episode_cleanup_except_favorite_removal">お気に入りされていない</string> <string name="episode_cleanup_except_favorite">お気に入りされていない</string>
<string name="episode_cleanup_queue_removal">キューにない時</string> <string name="episode_cleanup_not_in_queue">キューにない時</string>
<string name="episode_cleanup_after_listening">完了後</string> <string name="episode_cleanup_after_listening">完了後</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="other">完了後 %d 時間</item> <item quantity="other">完了後 %d 時間</item>
@ -261,7 +261,7 @@
<string name="pref_autodl_wifi_filter_sum">選択したWi-Fiネットワークに対してのみ自動ダウンロードを許可します。</string> <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_title">充電時以外にダウンロード</string>
<string name="pref_automatic_download_on_battery_sum">バッテリーを充電していない時に自動ダウンロードを許可します</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_episode_cover_summary">エピソード固有のカバー画像が利用可能な場合は常にリストで使います。チェックを外すと、アプリは常にポッドキャストのカバー画像を使います。</string>
<string name="pref_show_remain_time_summary">チェックをすると、エピソードの残り時間を表示します。チェックを外すと、エピソードの合計時間を表示します。</string> <string name="pref_show_remain_time_summary">チェックをすると、エピソードの残り時間を表示します。チェックを外すと、エピソードの合計時間を表示します。</string>
<string name="pref_theme_title_light">ライト</string> <string name="pref_theme_title_light">ライト</string>

View File

@ -105,8 +105,8 @@
<string name="episode_cleanup_never">안 함</string> <string name="episode_cleanup_never">안 함</string>
<string name="episode_cleanup_except_favorite_removal">즐겨찾기 아닐 때</string> <string name="episode_cleanup_except_favorite">즐겨찾기 아닐 때</string>
<string name="episode_cleanup_queue_removal">대기열에 없을 때</string> <string name="episode_cleanup_not_in_queue">대기열에 없을 때</string>
<string name="episode_cleanup_after_listening">끝나고 나서</string> <string name="episode_cleanup_after_listening">끝나고 나서</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="other">끝마치고 나서 %d시간</item> <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_title">충전하지 않을 때 다운로드</string>
<string name="pref_automatic_download_on_battery_sum">배터리 충전 중이 아닐 때 자동 다운로드 허용</string> <string name="pref_automatic_download_on_battery_sum">배터리 충전 중이 아닐 때 자동 다운로드 허용</string>
<string name="pref_episode_cache_title">에피소드 캐시</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_title">에피소드 커버 사용</string>
<string name="pref_episode_cover_summary">에피소드마다 설정된 커버가 있으면 그 커버를 사용합니다. 사용하지 않으면, 앱에서는 항상 팟캐스트 커버 이미지를 사용합니다.</string> <string name="pref_episode_cover_summary">에피소드마다 설정된 커버가 있으면 그 커버를 사용합니다. 사용하지 않으면, 앱에서는 항상 팟캐스트 커버 이미지를 사용합니다.</string>
<string name="pref_show_remain_time_title">남은 시간 표시</string> <string name="pref_show_remain_time_title">남은 시간 표시</string>

View File

@ -72,8 +72,8 @@
<string name="feed_auto_download_always">Visada</string> <string name="feed_auto_download_always">Visada</string>
<string name="feed_auto_download_never">Niekada</string> <string name="feed_auto_download_never">Niekada</string>
<string name="episode_cleanup_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_except_favorite">Kai nėra mėgstamas</string>
<string name="episode_cleanup_queue_removal">Jei nėra eilėje</string> <string name="episode_cleanup_not_in_queue">Jei nėra eilėje</string>
<string name="episode_cleanup_after_listening">Pabaigus klausyti</string> <string name="episode_cleanup_after_listening">Pabaigus klausyti</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">Praėjus 1 valandai po perklausymo</item> <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_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_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_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_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_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> <string name="pref_theme_title_light">Šviesi</string>

View File

@ -105,8 +105,8 @@
<string name="episode_cleanup_never">Aldri</string> <string name="episode_cleanup_never">Aldri</string>
<string name="episode_cleanup_except_favorite_removal">Hvis ikke favoritt</string> <string name="episode_cleanup_except_favorite">Hvis ikke favoritt</string>
<string name="episode_cleanup_queue_removal">Når ikke i kø</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> <string name="episode_cleanup_after_listening">Etter fullført avspilling</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 time etter fullført avspilling</item> <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_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_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_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_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_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> <string name="pref_show_remain_time_title">Vis gjenværende tid</string>

View File

@ -111,8 +111,8 @@
<string name="episode_cleanup_never">Nooit</string> <string name="episode_cleanup_never">Nooit</string>
<string name="episode_cleanup_except_favorite_removal">Indien niet favoriet</string> <string name="episode_cleanup_except_favorite">Indien niet favoriet</string>
<string name="episode_cleanup_queue_removal">Indien niet in wachtrij</string> <string name="episode_cleanup_not_in_queue">Indien niet in wachtrij</string>
<string name="episode_cleanup_after_listening">Als aflevering is beluisterd</string> <string name="episode_cleanup_after_listening">Als aflevering is beluisterd</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 uur na afronden</item> <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_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_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_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_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_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> <string name="pref_show_remain_time_summary">Inschakelen om de resterende tijd te tonen. Anders wordt de totale duur van een aflevering getoond.</string>

View File

@ -109,8 +109,8 @@
<string name="episode_cleanup_never">Nigdy</string> <string name="episode_cleanup_never">Nigdy</string>
<string name="episode_cleanup_except_favorite_removal">Gdy nie oznaczone jako ulubione</string> <string name="episode_cleanup_except_favorite">Gdy nie oznaczone jako ulubione</string>
<string name="episode_cleanup_queue_removal">Kiedy nie są w kolejce</string> <string name="episode_cleanup_not_in_queue">Kiedy nie są w kolejce</string>
<string name="episode_cleanup_after_listening">Po odtworzeniu</string> <string name="episode_cleanup_after_listening">Po odtworzeniu</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 godzinę po odtworzeniu</item> <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_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_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_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_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_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> <string name="pref_show_remain_time_title">Pokaż pozostały czas</string>

View File

@ -108,8 +108,8 @@
<string name="episode_cleanup_never">Nunca</string> <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_except_favorite">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_not_in_queue">Quando não está na fila</string>
<string name="episode_cleanup_after_listening">Depois de concluído</string> <string name="episode_cleanup_after_listening">Depois de concluído</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 hora após finalizar</item> <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_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_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_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_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_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> <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>

View File

@ -118,8 +118,8 @@
<string name="episode_cleanup_never">Nunca</string> <string name="episode_cleanup_never">Nunca</string>
<string name="episode_cleanup_except_favorite_removal">Se não for favorito</string> <string name="episode_cleanup_except_favorite">Se não for favorito</string>
<string name="episode_cleanup_queue_removal">Se não estiver na fila</string> <string name="episode_cleanup_not_in_queue">Se não estiver na fila</string>
<string name="episode_cleanup_after_listening">Ao terminar</string> <string name="episode_cleanup_after_listening">Ao terminar</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 hora depois de terminar</item> <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_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_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_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_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_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> <string name="pref_show_remain_time_title">Mostrar tempo restante</string>

View File

@ -115,8 +115,8 @@
<string name="episode_cleanup_never">Niciodată</string> <string name="episode_cleanup_never">Niciodată</string>
<string name="episode_cleanup_except_favorite_removal">Cănd nu este preferată</string> <string name="episode_cleanup_except_favorite">Cănd nu este preferată</string>
<string name="episode_cleanup_queue_removal">Când nu e în coadă</string> <string name="episode_cleanup_not_in_queue">Când nu e în coadă</string>
<string name="episode_cleanup_after_listening">După terminare</string> <string name="episode_cleanup_after_listening">După terminare</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 oră după terminare</item> <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_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_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_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_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_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> <string name="pref_show_remain_time_title">Arată timpul rămas</string>

View File

@ -105,8 +105,8 @@
<string name="episode_cleanup_never">никогда</string> <string name="episode_cleanup_never">никогда</string>
<string name="episode_cleanup_except_favorite_removal">Когда не в избранном</string> <string name="episode_cleanup_except_favorite">Когда не в избранном</string>
<string name="episode_cleanup_queue_removal">Когда не в очереди</string> <string name="episode_cleanup_not_in_queue">Когда не в очереди</string>
<string name="episode_cleanup_after_listening">После прослушивания</string> <string name="episode_cleanup_after_listening">После прослушивания</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 час после прослушивания</item> <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_title">Загружать без зарядки</string>
<string name="pref_automatic_download_on_battery_sum">Разрешать автоматическую загрузку когда батарея не заряжается</string> <string name="pref_automatic_download_on_battery_sum">Разрешать автоматическую загрузку когда батарея не заряжается</string>
<string name="pref_episode_cache_title">Кэш выпусков</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_title">Использовать обложку выпуска</string>
<string name="pref_episode_cover_summary">Если выпуск содержит свою обложку, показывать в списках её. Если не выбрано, всегда используется обложка подкаста.</string> <string name="pref_episode_cover_summary">Если выпуск содержит свою обложку, показывать в списках её. Если не выбрано, всегда используется обложка подкаста.</string>
<string name="pref_show_remain_time_title">Показывать оставшееся время</string> <string name="pref_show_remain_time_title">Показывать оставшееся время</string>

View File

@ -115,8 +115,8 @@
<string name="episode_cleanup_never">Nikdy</string> <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_except_favorite">Ak nie je medzi obľúbenými</string>
<string name="episode_cleanup_queue_removal">Ak nie je v poradí</string> <string name="episode_cleanup_not_in_queue">Ak nie je v poradí</string>
<string name="episode_cleanup_after_listening">Po dokončení</string> <string name="episode_cleanup_after_listening">Po dokončení</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 hodinu po dokončení</item> <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_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_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_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_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_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> <string name="pref_show_remain_time_title">Zobraziť zostávajúci čas</string>

View File

@ -71,8 +71,8 @@
<string name="feed_auto_download_always">Vedno</string> <string name="feed_auto_download_always">Vedno</string>
<string name="feed_auto_download_never">Nikoli</string> <string name="feed_auto_download_never">Nikoli</string>
<string name="episode_cleanup_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_except_favorite">Ko ni dodan med priljubljene</string>
<string name="episode_cleanup_queue_removal">Ko ne čaka v čakalni vrsti</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> <string name="episode_cleanup_after_listening">Po končanem</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 uro po končanem</item> <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_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_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_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_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_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> <string name="pref_theme_title_light">Svetlo</string>

View File

@ -115,8 +115,8 @@
<string name="episode_cleanup_never">Aldrig</string> <string name="episode_cleanup_never">Aldrig</string>
<string name="episode_cleanup_except_favorite_removal">När ej favorit</string> <string name="episode_cleanup_except_favorite">När ej favorit</string>
<string name="episode_cleanup_queue_removal">Om inte köad</string> <string name="episode_cleanup_not_in_queue">Om inte köad</string>
<string name="episode_cleanup_after_listening">Efter färdigspelad</string> <string name="episode_cleanup_after_listening">Efter färdigspelad</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 timma efter klar</item> <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_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_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_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_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_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> <string name="pref_show_remain_time_title">Visa återstående tid</string>

View File

@ -110,8 +110,8 @@
<string name="episode_cleanup_never">Hiçbir zaman</string> <string name="episode_cleanup_never">Hiçbir zaman</string>
<string name="episode_cleanup_except_favorite_removal">Favorilenmemişken</string> <string name="episode_cleanup_except_favorite">Favorilenmemişken</string>
<string name="episode_cleanup_queue_removal">Sırada değilse</string> <string name="episode_cleanup_not_in_queue">Sırada değilse</string>
<string name="episode_cleanup_after_listening">Bittikten sonra</string> <string name="episode_cleanup_after_listening">Bittikten sonra</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">Bittikten 1 saat sonra</item> <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_title">Şarj olmuyorken indir</string>
<string name="pref_automatic_download_on_battery_sum">Pil şarj olmuyorken otomatik indirmeye izin ver</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_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_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_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> <string name="pref_show_remain_time_title">Kalan süreyi göster</string>

View File

@ -115,8 +115,8 @@
<string name="episode_cleanup_never">Ніколи</string> <string name="episode_cleanup_never">Ніколи</string>
<string name="episode_cleanup_except_favorite_removal">Коли не вибрано</string> <string name="episode_cleanup_except_favorite">Коли не вибрано</string>
<string name="episode_cleanup_queue_removal">Якщо не в черзі</string> <string name="episode_cleanup_not_in_queue">Якщо не в черзі</string>
<string name="episode_cleanup_after_listening">Після закінчення</string> <string name="episode_cleanup_after_listening">Після закінчення</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 година після завершення</item> <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_title">Завантаження без зарядного пристрою</string>
<string name="pref_automatic_download_on_battery_sum">Дозволити автозавантаження коли зарядний пристрій не підключений</string> <string name="pref_automatic_download_on_battery_sum">Дозволити автозавантаження коли зарядний пристрій не підключений</string>
<string name="pref_episode_cache_title">Кеш епізодів</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_title">Показувати обкладинку епізоду</string>
<string name="pref_episode_cover_summary">Використовуйте обкладинку для епізоду у списках, коли це можливо. Якщо цей прапорець не встановлено, програма завжди буде використовувати зображення обкладинки подкасту.</string> <string name="pref_episode_cover_summary">Використовуйте обкладинку для епізоду у списках, коли це можливо. Якщо цей прапорець не встановлено, програма завжди буде використовувати зображення обкладинки подкасту.</string>
<string name="pref_show_remain_time_title">Показати залишок часу</string> <string name="pref_show_remain_time_title">Показати залишок часу</string>

View File

@ -119,8 +119,8 @@
<string name="episode_cleanup_never">从不</string> <string name="episode_cleanup_never">从不</string>
<string name="episode_cleanup_except_favorite_removal">当未收藏时</string> <string name="episode_cleanup_except_favorite">当未收藏时</string>
<string name="episode_cleanup_queue_removal">当不在队列中</string> <string name="episode_cleanup_not_in_queue">当不在队列中</string>
<string name="episode_cleanup_after_listening">结束后</string> <string name="episode_cleanup_after_listening">结束后</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="other">结束后 %d 小时</item> <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_title">未充电时下载</string>
<string name="pref_automatic_download_on_battery_sum">未充电时允许自动下载</string> <string name="pref_automatic_download_on_battery_sum">未充电时允许自动下载</string>
<string name="pref_episode_cache_title">节目缓存</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_title">使用节目封面</string>
<string name="pref_episode_cover_summary">勾选后,在列表中使用一期节目特定的封面图。如果不勾选,应用程序将始终使用播客封面图像。</string> <string name="pref_episode_cover_summary">勾选后,在列表中使用一期节目特定的封面图。如果不勾选,应用程序将始终使用播客封面图像。</string>
<string name="pref_show_remain_time_title">显示剩余时间</string> <string name="pref_show_remain_time_title">显示剩余时间</string>

View File

@ -67,8 +67,8 @@
<string name="feed_auto_download_always">總是</string> <string name="feed_auto_download_always">總是</string>
<string name="feed_auto_download_never">不予下載</string> <string name="feed_auto_download_never">不予下載</string>
<string name="episode_cleanup_never">不予刪除</string> <string name="episode_cleanup_never">不予刪除</string>
<string name="episode_cleanup_except_favorite_removal">若未標記為最愛</string> <string name="episode_cleanup_except_favorite">若未標記為最愛</string>
<string name="episode_cleanup_queue_removal">若未列入待播清單</string> <string name="episode_cleanup_not_in_queue">若未列入待播清單</string>
<string name="episode_cleanup_after_listening">聽完後</string> <string name="episode_cleanup_after_listening">聽完後</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="other">聽完後 %d 小時</item> <item quantity="other">聽完後 %d 小時</item>
@ -210,7 +210,7 @@
<string name="pref_autodl_wifi_filter_sum">限定於特定 Wi-Fi 連線時自動下載</string> <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_title">未充電時下載</string>
<string name="pref_automatic_download_on_battery_sum">允許未充電時也自動下載</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_episode_cover_summary">在單集有專屬封面的情況下使用該封面圖。如果取消,則一律使用 Podcast 的封面圖</string>
<string name="pref_show_remain_time_summary">勾選時顯示剩餘播放時間,不勾選時顯示單集的總時間。</string> <string name="pref_show_remain_time_summary">勾選時顯示剩餘播放時間,不勾選時顯示單集的總時間。</string>
<string name="pref_theme_title_light">淡色</string> <string name="pref_theme_title_light">淡色</string>

View File

@ -1,147 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <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"> <string-array name="video_mode_options">
<item>@string/pref_video_mode_small_window</item> <item>@string/pref_video_mode_small_window</item>
<item>@string/pref_video_mode_full_screen</item> <item>@string/pref_video_mode_full_screen</item>
@ -158,25 +16,4 @@
<item>@string/next_chapter</item> <item>@string/next_chapter</item>
<item>@string/playback_speed</item> <item>@string/playback_speed</item>
</string-array> </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> </resources>

View File

@ -169,9 +169,10 @@
<string name="remove_from_current_feed">Remove from current feed</string> <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_never">Never</string>
<string name="episode_cleanup_except_favorite_removal">When not favorited</string> <string name="episode_cleanup_except_favorite">When not favorited</string>
<string name="episode_cleanup_queue_removal">When not in queue</string> <string name="episode_cleanup_not_in_queue">When not in queue</string>
<string name="episode_cleanup_after_listening">After finishing</string> <string name="episode_cleanup_after_listening">After finishing</string>
<plurals name="episode_cleanup_hours_after_listening"> <plurals name="episode_cleanup_hours_after_listening">
<item quantity="one">1 hour after finishing</item> <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_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_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_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_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_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> <string name="pref_episode_cover_title">Use episode cover</string>
@ -742,6 +743,7 @@
<string name="sleep_timer_enabled_label">Sleep timer enabled</string> <string name="sleep_timer_enabled_label">Sleep timer enabled</string>
<!-- Synchronisation --> <!-- 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="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_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> <string name="synchronization_summary_unchoosen">You can choose from multiple providers to synchronize your subscriptions and episode play state with</string>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 # 6.14.7
* corrected some deeplinks in manifest file on OPMLActivity * corrected some deeplinks in manifest file on OPMLActivity

View File

@ -1,4 +1,4 @@
Version 6.14.6 Version 6.14.7
* corrected some deeplinks in manifest file on OPMLActivity * corrected some deeplinks in manifest file on OPMLActivity
* added commentTime in Episode and Feed to record the time of comment/opinion added * added commentTime in Episode and Feed to record the time of comment/opinion added

View File

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