6.14.7 commit

This commit is contained in:
Xilin Jia 2024-11-25 13:38:53 +01:00
parent 309d7c98b9
commit 5f09864451
68 changed files with 847 additions and 1205 deletions

View File

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

View File

@ -148,28 +148,6 @@
android:windowSoftInputMode="stateAlwaysHidden"
android:launchMode="singleTask"
android:exported="true">
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.VIEW" />-->
<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
<!-- <data-->
<!-- android:host="podcini.org"-->
<!-- android:pathPrefix="/deeplink/main"-->
<!-- android:scheme="https" />-->
<!-- </intent-filter>-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.VIEW" />-->
<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
<!-- <data-->
<!-- android:host="podcini.org"-->
<!-- android:pathPrefix="/deeplink/search"-->
<!-- android:scheme="https" />-->
<!-- </intent-filter>-->
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="ac.mdiq.podcini.intents.MAIN_ACTIVITY" />
@ -233,9 +211,9 @@
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:host="*"/>
<data android:pathPattern=".*.xml" />
<data android:pathPattern=".*.opml" />
<!-- <data android:host="*"/>-->
<data android:pathPattern="/.*\\.xml" />
<data android:pathPattern="/.*\\.opml" />
</intent-filter>
</activity>
<activity
@ -272,34 +250,6 @@
android:name="android.support.PARENT_ACTIVITY"
android:value="ac.mdiq.podcini.ui.activity.MainActivity"/>
<!-- URLs ending with '.xml' or '.rss' -->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.VIEW"/>-->
<!-- <category android:name="android.intent.category.DEFAULT"/>-->
<!-- <category android:name="android.intent.category.BROWSABLE"/>-->
<!-- <data android:scheme="http"/>-->
<!-- <data android:scheme="https"/>-->
<!-- <data android:host="*"/>-->
<!-- <data android:pathPattern=".*\\.xml"/>-->
<!-- <data android:pathPattern=".*\\.rss"/>-->
<!-- <data android:pathPattern=".*\\.atom"/>-->
<!-- </intent-filter>-->
<!-- Feedburner URLs -->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.VIEW"/>-->
<!-- <category android:name="android.intent.category.DEFAULT"/>-->
<!-- <category android:name="android.intent.category.BROWSABLE"/>-->
<!-- <data android:scheme="http"/>-->
<!-- <data android:scheme="https"/>-->
<!-- <data android:host="feeds.feedburner.com"/>-->
<!-- <data android:host="feedproxy.google.com"/>-->
<!-- <data android:host="feeds2.feedburner.com"/>-->
<!-- <data android:host="feedsproxy.google.com"/>-->
<!-- </intent-filter>-->
<!-- Files with mimeType rss/xml/atom -->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
@ -326,44 +276,6 @@
<data android:scheme="podcini-subscribe"/>
</intent-filter>
<!-- Support for subscribeonandroid.com URLS -->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.VIEW" />-->
<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
<!-- <data android:pathPattern=".*\\..*/.*" />-->
<!-- <data android:host="subscribeonandroid.com" />-->
<!-- <data android:host="www.subscribeonandroid.com" />-->
<!-- <data android:host="*subscribeonandroid.com" />-->
<!-- <data android:scheme="http" />-->
<!-- <data android:scheme="https" />-->
<!-- </intent-filter>-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.VIEW" />-->
<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
<!-- <data android:host="podcini.org" />-->
<!-- <data android:host="www.podcini.org" />-->
<!-- <data android:pathPrefix="/deeplink/subscribe" />-->
<!-- <data android:scheme="http" />-->
<!-- <data android:scheme="https" />-->
<!-- </intent-filter>-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.VIEW" />-->
<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
<!-- <data android:pathPattern="/.*/podcast/.*" />-->
<!-- <data android:host="podcasts.apple.com" />-->
<!-- <data android:scheme="http" />-->
<!-- <data android:scheme="https" />-->
<!-- </intent-filter>-->
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>

View File

@ -34,7 +34,7 @@ import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
import ac.mdiq.podcini.storage.database.Episodes.hasAlmostEnded
import ac.mdiq.podcini.ui.utils.NotificationUtils
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent

View File

@ -14,7 +14,7 @@ import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.model.Rating
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
import ac.mdiq.podcini.storage.database.Episodes.hasAlmostEnded
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd

View File

@ -9,10 +9,10 @@ import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
import ac.mdiq.podcini.storage.database.Episodes
import ac.mdiq.podcini.storage.database.Episodes.setPlayStateSync
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.FlowEvent.PlayEvent.Action
@ -44,6 +44,7 @@ import kotlinx.coroutines.*
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.Throws
/**
* Manages the MediaPlayer object of the PlaybackService.
@ -195,7 +196,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
var item = media_.episodeOrFetch()
if (item != null && item.playState < PlayState.PROGRESS.code) item = runBlocking { setPlayStateSync(PlayState.PROGRESS.code, item, false) }
val eList = if (item?.feed?.preferences?.queue != null) curQueue.episodes else item?.feed?.getVirtualQueueItems() ?: listOf()
curIndexInQueue = EpisodeUtil.indexOfItemWithId(eList, media_.id)
curIndexInQueue = Episodes.indexOfItemWithId(eList, media_.id)
} else curIndexInQueue = -1
prevMedia = curMedia

View File

@ -32,8 +32,10 @@ import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
import ac.mdiq.podcini.preferences.UserPreferences.prefAdaptiveProgressUpdate
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
import ac.mdiq.podcini.receiver.MediaButtonReceiver
import ac.mdiq.podcini.storage.database.Episodes
import ac.mdiq.podcini.storage.database.Episodes.deleteMediaSync
import ac.mdiq.podcini.storage.database.Episodes.getEpisodeByGuidOrUrl
import ac.mdiq.podcini.storage.database.Episodes.hasAlmostEnded
import ac.mdiq.podcini.storage.database.Episodes.prefDeleteRemovesFromQueue
import ac.mdiq.podcini.storage.database.Episodes.prefRemoveFromQueueMarkedPlayed
import ac.mdiq.podcini.storage.database.Episodes.setPlayStateSync
@ -51,8 +53,6 @@ import ac.mdiq.podcini.storage.model.CurrentState.Companion.PLAYER_STATUS_PAUSED
import ac.mdiq.podcini.storage.model.CurrentState.Companion.PLAYER_STATUS_PLAYING
import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction
import ac.mdiq.podcini.storage.utils.ChapterUtils
import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
import ac.mdiq.podcini.ui.utils.NotificationUtils
@ -444,7 +444,7 @@ class PlaybackService : MediaLibraryService() {
}
Logd(TAG, "getNextInQueue eList: ${eList.size}")
var j = 0
val i = EpisodeUtil.indexOfItemWithId(eList, item.id)
val i = Episodes.indexOfItemWithId(eList, item.id)
Logd(TAG, "getNextInQueue current i: $i curIndexInQueue: $curIndexInQueue")
if (i < 0) {
j = if (curIndexInQueue >= 0 && curIndexInQueue < eList.size) curIndexInQueue else eList.size-1

View File

@ -7,7 +7,7 @@ import java.io.Writer
interface ExportWriter {
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
fun writeDocument(feeds: List<Feed?>?, writer: Writer?, context: Context)
fun writeDocument(feeds: List<Feed>, writer: Writer?, context: Context)
fun fileExtension(): String?
}

View File

@ -1,18 +1,31 @@
package ac.mdiq.podcini.preferences
import ac.mdiq.podcini.R
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.util.MiscFormatter.formatRfc822Date
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.MiscFormatter.formatRfc822Date
import android.Manifest
import android.content.Context
import android.content.DialogInterface
import android.content.pm.PackageManager
import android.net.Uri
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.util.Log
import android.util.Xml
import androidx.core.app.ActivityCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.apache.commons.io.input.BOMInputStream
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import org.xmlpull.v1.XmlPullParserFactory
import java.io.IOException
import java.io.InputStreamReader
import java.io.Reader
import java.io.Writer
import java.util.*
import kotlin.Throws
class OpmlTransporter {
@ -44,11 +57,10 @@ class OpmlTransporter {
/** Writes OPML documents. */
class OpmlWriter : ExportWriter {
/**
* Takes a list of feeds and a writer and writes those into an OPML
* document.
* Takes a list of feeds and a writer and writes those into an OPML document.
*/
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
override fun writeDocument(feeds: List<Feed?>?, writer: Writer?, context: Context) {
override fun writeDocument(feeds: List<Feed>, writer: Writer?, context: Context) {
val xs = Xml.newSerializer()
xs.setFeature(OpmlSymbols.XML_FEATURE_INDENT_OUTPUT, true)
xs.setOutput(writer)
@ -67,11 +79,10 @@ class OpmlTransporter {
xs.endTag(null, OpmlSymbols.HEAD)
xs.startTag(null, OpmlSymbols.BODY)
for (feed in feeds!!) {
if (feed == null) continue
Logd(TAG, "writeDocument ${feed?.title}")
for (feed in feeds) {
Logd(TAG, "writeDocument ${feed.title}")
xs.startTag(null, OpmlSymbols.OUTLINE)
xs.attribute(null, OpmlSymbols.TEXT, feed!!.title)
xs.attribute(null, OpmlSymbols.TEXT, feed.title)
xs.attribute(null, OpmlSymbols.TITLE, feed.title)
if (feed.type != null) xs.attribute(null, OpmlSymbols.TYPE, feed.type)
xs.attribute(null, OpmlSymbols.XMLURL, feed.downloadUrl)
@ -138,8 +149,7 @@ class OpmlTransporter {
}
}
// TODO: on first install app: java.io.IOException: Underlying input stream returned zero bytes
try {
eventType = xpp.next()
try { eventType = xpp.next()
} catch(e: Exception) {
Log.e(TAG, "xpp.next() invalid: $e")
break
@ -152,4 +162,56 @@ class OpmlTransporter {
private val TAG: String = OpmlReader::class.simpleName ?: "Anonymous"
}
}
companion object {
fun startImport(context: Context, uri: Uri) {
val TAG = "OpmlTransporter"
// CoroutineScope(Dispatchers.IO).launch {
try {
val opmlFileStream = context.contentResolver.openInputStream(uri)
val bomInputStream = BOMInputStream(opmlFileStream)
val bom = bomInputStream.bom
val charsetName = if (bom == null) "UTF-8" else bom.charsetName
val reader: Reader = InputStreamReader(bomInputStream, charsetName)
val opmlReader = OpmlReader()
val result = opmlReader.readDocument(reader)
reader.close()
// withContext(Dispatchers.Main) {
// binding.progressBar.visibility = View.GONE
Logd(TAG, "Parsing was successful")
// readElements = result
// }
} catch (e: Throwable) {
// withContext(Dispatchers.Main) {
Logd(TAG, Log.getStackTraceString(e))
val message = if (e.message == null) "" else e.message!!
if (message.lowercase().contains("permission")) {
val permission = ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE)
if (permission != PackageManager.PERMISSION_GRANTED) {
// requestPermission()
return
}
}
// binding.progressBar.visibility = View.GONE
val alert = MaterialAlertDialogBuilder(context)
alert.setTitle(R.string.error_label)
val userReadable = context.getString(R.string.opml_reader_error)
val details = e.message
val total = """
$userReadable
$details
""".trimIndent()
val errorMessage = SpannableString(total)
errorMessage.setSpan(ForegroundColorSpan(-0x77777778), userReadable.length, total.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
alert.setMessage(errorMessage)
alert.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
// finish()
}
alert.show()
// }
}
// }
}
}
}

View File

@ -82,9 +82,7 @@ class AutoDownloadPreferencesFragment : PreferenceFragmentCompat() {
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)
}
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
@ -130,10 +128,7 @@ class AutoDownloadPreferencesFragment : PreferenceFragmentCompat() {
private fun clearAutodownloadSelectedNetworsPreference() {
if (selectedNetworks != null) {
val prefScreen = preferenceScreen
for (network in selectedNetworks!!) {
if (network != null) prefScreen.removePreference(network)
}
for (network in selectedNetworks!!) if (network != null) prefScreen.removePreference(network)
}
}
@ -160,11 +155,7 @@ class AutoDownloadPreferencesFragment : PreferenceFragmentCompat() {
}
private fun setSelectedNetworksEnabled(b: Boolean) {
if (selectedNetworks != null) {
for (p in selectedNetworks!!) {
p!!.isEnabled = b
}
}
if (selectedNetworks != null) for (p in selectedNetworks!!) p!!.isEnabled = b
}
companion object {

View File

@ -1,8 +1,6 @@
package ac.mdiq.podcini.preferences.fragments
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.ChooseDataFolderDialogBinding
import ac.mdiq.podcini.databinding.ChooseDataFolderDialogEntryBinding
import ac.mdiq.podcini.databinding.ProxySettingsBinding
import ac.mdiq.podcini.net.download.service.PodciniHttpClient
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.newBuilder
@ -12,33 +10,43 @@ 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.storage.utils.FilesUtils.getDataFolder
import ac.mdiq.podcini.storage.utils.StorageUtils.getFreeSpaceAvailable
import ac.mdiq.podcini.storage.utils.StorageUtils.getTotalSpaceAvailable
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.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Build
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.text.format.Formatter
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.core.util.Consumer
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 androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -50,103 +58,152 @@ import okhttp3.Request
import okhttp3.Request.Builder
import okhttp3.Response
import okhttp3.Route
import java.io.File
import java.io.IOException
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.SocketAddress
import java.util.concurrent.TimeUnit
class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeListener {
class DownloadsPreferencesFragment : PreferenceFragmentCompat() {
private var blockAutoDeleteLocal = true
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_downloads)
setupNetworkScreen()
// addPreferencesFromResource(R.xml.preferences_downloads)
// setupNetworkScreen()
}
override fun onStart() {
super.onStart()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
(activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.downloads_pref)
appPrefs.registerOnSharedPreferenceChangeListener(this)
}
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") } }
)
}
override fun onStop() {
super.onStop()
appPrefs.unregisterOnSharedPreferenceChangeListener(this)
}
// override fun onResume() {
// super.onResume()
//// setDataFolderText()
// }
private fun setupNetworkScreen() {
findPreference<Preference>(Prefs.prefAutoDownloadSettings.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
(activity as PreferenceActivity).openScreen(R.xml.preferences_autodownload)
true
}
// validate and set correct value: number of downloads between 1 and 50 (inclusive)
findPreference<Preference>(Prefs.prefProxy.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
val dialog = ProxyDialog(requireContext())
dialog.show()
true
}
// findPreference<Preference>(PREF_CHOOSE_DATA_DIR)?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
// ChooseDataFolderDialog.showDialog(requireContext()) { path: String? ->
// setDataFolder(path!!)
//// setDataFolderText()
// }
// true
// }
findPreference<Preference>(Prefs.prefAutoDeleteLocal.name)!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
if (blockAutoDeleteLocal && newValue as Boolean) {
showAutoDeleteEnableDialog()
return@OnPreferenceChangeListener false
} else return@OnPreferenceChangeListener true
}
}
// private fun setDataFolderText() {
// val f = getDataFolder(null)
// if (f != null) findPreference<Preference>(PREF_CHOOSE_DATA_DIR)!!.summary = f.absolutePath
// }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
if (UserPreferences.Prefs.prefAutoUpdateIntervall.name == key) restartUpdateAlarm(requireContext(), true)
}
private fun 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
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)
}
}
}
}
.setNegativeButton(R.string.cancel_label, null)
.show()
}
}
object ChooseDataFolderDialog {
fun showDialog(context: Context, handlerFunc: Consumer<String?>) {
val content = View.inflate(context, R.layout.choose_data_folder_dialog, null)
val dialog = MaterialAlertDialogBuilder(context)
.setView(content)
.setTitle(R.string.choose_data_directory)
.setMessage(R.string.choose_data_directory_message)
.setNegativeButton(R.string.cancel_label, null)
.create()
val binding = ChooseDataFolderDialogBinding.bind(content)
val recyclerView = binding.recyclerView
recyclerView.layoutManager = LinearLayoutManager(context)
val adapter = DataFolderAdapter(context) { path: String? ->
dialog.dismiss()
handlerFunc.accept(path)
}
recyclerView.adapter = adapter
if (adapter.itemCount != 0) dialog.show()
}
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) {
@ -161,13 +218,7 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere
private val port: Int
get() {
val port = etPort.text.toString()
if (port.isNotEmpty()) {
try {
return port.toInt()
} catch (e: NumberFormatException) {
// ignore
}
}
if (port.isNotEmpty()) try { return port.toInt() } catch (e: NumberFormatException) { }
return 0
}
@ -203,7 +254,7 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere
val types: MutableList<String> = ArrayList()
types.add(Proxy.Type.DIRECT.name)
types.add(Proxy.Type.HTTP.name)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) types.add(Proxy.Type.SOCKS.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)
@ -327,23 +378,17 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere
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))
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()
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)
}
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))
@ -362,86 +407,10 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere
}
}
private class DataFolderAdapter(context: Context, selectionHandler: Consumer<String>) : RecyclerView.Adapter<DataFolderAdapter.ViewHolder?>() {
private val selectionHandler: Consumer<String>
private val currentPath: String?
private val entries: List<StoragePath>
private val freeSpaceString: String
init {
this.entries = getStorageEntries(context)
this.currentPath = getCurrentPath()
this.selectionHandler = selectionHandler
this.freeSpaceString = context.getString(R.string.choose_data_directory_available_space)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val entryView = inflater.inflate(R.layout.choose_data_folder_dialog_entry, parent, false)
return ViewHolder(entryView)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val storagePath = entries[position]
val context = holder.root.context
val freeSpace = Formatter.formatShortFileSize(context, storagePath.availableSpace)
val totalSpace = Formatter.formatShortFileSize(context, storagePath.totalSpace)
holder.path.text = storagePath.shortPath
holder.size.text = String.format(freeSpaceString, freeSpace, totalSpace)
holder.progressBar.progress = storagePath.usagePercentage
val selectListener = View.OnClickListener { _: View? ->
selectionHandler.accept(storagePath.fullPath)
}
holder.root.setOnClickListener(selectListener)
holder.radioButton.setOnClickListener(selectListener)
if (storagePath.fullPath == currentPath) holder.radioButton.toggle()
}
override fun getItemCount(): Int {
return entries.size
}
private fun getCurrentPath(): String? {
val dataFolder = getDataFolder(null)
return dataFolder?.absolutePath
}
private fun getStorageEntries(context: Context): List<StoragePath> {
val mediaDirs = context.getExternalFilesDirs(null)
val entries: MutableList<StoragePath> = ArrayList(mediaDirs.size)
for (dir in mediaDirs) {
if (!isWritable(dir)) continue
entries.add(StoragePath(dir.absolutePath))
}
if (entries.isEmpty() && isWritable(context.filesDir)) entries.add(StoragePath(context.filesDir.absolutePath))
return entries
}
private fun isWritable(dir: File?): Boolean {
return dir != null && dir.exists() && dir.canRead() && dir.canWrite()
}
private class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val binding = ChooseDataFolderDialogEntryBinding.bind(itemView)
val root: View = binding.root
val path: TextView = binding.path
val size: TextView = binding.size
val radioButton: RadioButton = binding.radioButton
val progressBar: ProgressBar = binding.usedSpace
}
private class StoragePath(val fullPath: String) {
val shortPath: String
get() {
val prefixIndex = fullPath.indexOf("Android")
return if ((prefixIndex > 0)) fullPath.substring(0, prefixIndex) else fullPath
}
val availableSpace: Long
get() = getFreeSpaceAvailable(fullPath)
val totalSpace: Long
get() = getTotalSpaceAvailable(fullPath)
val usagePercentage: Int
get() = 100 - (100 * availableSpace / totalSpace.toFloat()).toInt()
}
}
@Suppress("EnumEntryName")
private enum class Prefs {
prefAutoDownloadSettings,
prefAutoDeleteLocal,
prefProxy,
// prefChooseDataDir,
}
}

View File

@ -15,7 +15,7 @@ import ac.mdiq.podcini.storage.database.Feeds.getFeedList
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
import ac.mdiq.podcini.storage.database.Episodes.hasAlmostEnded
import ac.mdiq.podcini.storage.utils.FileNameGenerator.generateFileName
import ac.mdiq.podcini.storage.utils.FilesUtils.getDataFolder
import ac.mdiq.podcini.ui.activity.OpmlImportActivity
@ -23,7 +23,6 @@ import ac.mdiq.podcini.ui.activity.PreferenceActivity
import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.util.Logd
import android.app.Activity.RESULT_OK
import android.app.ProgressDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.DialogInterface
@ -42,20 +41,20 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.*
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.core.app.ShareCompat.IntentBuilder
import androidx.core.content.FileProvider
import androidx.documentfile.provider.DocumentFile
@ -80,30 +79,30 @@ import kotlin.Throws
class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
private val chooseOpmlExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
result: ActivityResult -> this.chooseOpmlExportPathResult(result) }
result: ActivityResult -> this.chooseOpmlExportPathResult(result) }
private val chooseHtmlExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
result: ActivityResult -> this.chooseHtmlExportPathResult(result) }
result: ActivityResult -> this.chooseHtmlExportPathResult(result) }
private val chooseFavoritesExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
result: ActivityResult -> this.chooseFavoritesExportPathResult(result) }
result: ActivityResult -> this.chooseFavoritesExportPathResult(result) }
private val chooseProgressExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
result: ActivityResult -> this.chooseProgressExportPathResult(result) }
result: ActivityResult -> this.chooseProgressExportPathResult(result) }
private val restoreProgressLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
result: ActivityResult -> this.restoreProgressResult(result) }
private val restoreDatabaseLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
result: ActivityResult -> this.restoreDatabaseResult(result) }
result: ActivityResult -> this.restoreDatabaseResult(result) }
private val backupDatabaseLauncher = registerForActivityResult<String, Uri>(BackupDatabase()) { uri: Uri? -> this.backupDatabaseResult(uri) }
private val chooseOpmlImportPathLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.GetContent()) {
uri: Uri? -> this.chooseOpmlImportPathResult(uri) }
uri: Uri? -> this.chooseOpmlImportPathResult(uri) }
private val restorePreferencesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
result: ActivityResult -> this.restorePreferencesResult(result) }
result: ActivityResult -> this.restorePreferencesResult(result) }
private val backupPreferencesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
@ -116,22 +115,28 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
result: ActivityResult -> this.restoreMediaFilesResult(result) }
private val backupMediaFilesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
result: ActivityResult -> this.exportMediaFilesResult(result)
}
result: ActivityResult -> this.exportMediaFilesResult(result) }
private var progressDialog: ProgressDialog? = null
private var showProgress by mutableStateOf(false)
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
(activity as PreferenceActivity).supportActionBar?.setTitle(R.string.import_export_pref)
progressDialog = ProgressDialog(context)
progressDialog!!.isIndeterminate = true
progressDialog!!.setMessage(requireContext().getString(R.string.please_wait))
return ComposeView(requireContext()).apply {
setContent {
CustomTheme(requireContext()) {
val textColor = MaterialTheme.colorScheme.onSurface
if (showProgress) {
Dialog(onDismissRequest = { showProgress = false }) {
Surface(modifier = Modifier.fillMaxSize(), shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp)) {
Box(contentAlignment = Alignment.Center) {
CircularProgressIndicator()
Text("Loading...", modifier = Modifier.align(Alignment.BottomCenter))
}
}
}
}
val scrollState = rememberScrollState()
Column(modifier = Modifier.fillMaxWidth().padding(16.dp).verticalScroll(scrollState)) {
Text(stringResource(R.string.database), color = textColor, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
@ -238,7 +243,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
private fun exportWithWriter(exportWriter: ExportWriter, uri: Uri?, exportType: Export) {
val context: Context? = activity
progressDialog!!.show()
showProgress = true
if (uri == null) {
lifecycleScope.launch(Dispatchers.IO) {
try {
@ -248,7 +253,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
showExportSuccessSnackbar(fileUri, exportType.contentType)
}
} catch (e: Exception) { showTransportErrorDialog(e)
} finally { progressDialog!!.dismiss() }
} finally { showProgress = false }
}
} else {
lifecycleScope.launch(Dispatchers.IO) {
@ -259,7 +264,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
showExportSuccessSnackbar(output.uri, exportType.contentType)
}
} catch (e: Exception) { showTransportErrorDialog(e)
} finally { progressDialog!!.dismiss() }
} finally { showProgress = false }
}
}
}
@ -355,7 +360,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
}
private fun showTransportErrorDialog(error: Throwable) {
progressDialog!!.dismiss()
showProgress = false
val alert = MaterialAlertDialogBuilder(requireContext())
alert.setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int -> dialog.dismiss() }
alert.setTitle(R.string.import_export_error_label)
@ -421,7 +426,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0
// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
if (isJsonFile(uri)) {
progressDialog!!.show()
showProgress = true
lifecycleScope.launch {
try {
withContext(Dispatchers.IO) {
@ -432,7 +437,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
}
withContext(Dispatchers.Main) {
showImportSuccessDialog()
progressDialog!!.dismiss()
showProgress = false
}
} catch (e: Throwable) { showTransportErrorDialog(e) }
}
@ -456,7 +461,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0
// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
if (isRealmFile(uri)) {
progressDialog!!.show()
showProgress = true
lifecycleScope.launch {
try {
withContext(Dispatchers.IO) {
@ -464,7 +469,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
}
withContext(Dispatchers.Main) {
showImportSuccessDialog()
progressDialog!!.dismiss()
showProgress = false
}
} catch (e: Throwable) { showTransportErrorDialog(e) }
}
@ -497,7 +502,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0
// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
if (isPrefDir(uri)) {
progressDialog!!.show()
showProgress = true
lifecycleScope.launch {
try {
withContext(Dispatchers.IO) {
@ -505,7 +510,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
}
withContext(Dispatchers.Main) {
showImportSuccessDialog()
progressDialog!!.dismiss()
showProgress = false
}
} catch (e: Throwable) { showTransportErrorDialog(e) }
}
@ -522,7 +527,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0
// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
if (isMediaFilesDir(uri)) {
progressDialog!!.show()
showProgress = true
lifecycleScope.launch {
try {
withContext(Dispatchers.IO) {
@ -530,7 +535,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
}
withContext(Dispatchers.Main) {
showImportSuccessDialog()
progressDialog!!.dismiss()
showProgress = false
}
} catch (e: Throwable) { showTransportErrorDialog(e) }
}
@ -546,7 +551,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
val uri = result.data!!.data!!
// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ?: 0
// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
progressDialog!!.show()
showProgress = true
lifecycleScope.launch {
try {
withContext(Dispatchers.IO) {
@ -554,7 +559,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
}
withContext(Dispatchers.Main) {
showExportSuccessSnackbar(uri, null)
progressDialog!!.dismiss()
showProgress = false
}
} catch (e: Throwable) { showTransportErrorDialog(e) }
}
@ -562,7 +567,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
private fun backupDatabaseResult(uri: Uri?) {
if (uri == null) return
progressDialog!!.show()
showProgress = true
lifecycleScope.launch {
try {
withContext(Dispatchers.IO) {
@ -570,7 +575,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
}
withContext(Dispatchers.Main) {
showExportSuccessSnackbar(uri, "application/x-sqlite3")
progressDialog!!.dismiss()
showProgress = false
}
} catch (e: Throwable) { showTransportErrorDialog(e) }
}
@ -578,6 +583,9 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
private fun chooseOpmlImportPathResult(uri: Uri?) {
if (uri == null) return
Logd(TAG, "chooseOpmlImportPathResult: uri: $uri")
// OpmlTransporter.startImport(requireContext(), uri)
// showImportSuccessDialog()
val intent = Intent(context, OpmlImportActivity::class.java)
intent.setData(uri)
startActivity(intent)
@ -1014,7 +1022,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
/** Writes saved favorites to file. */
class EpisodesProgressWriter : ExportWriter {
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
override fun writeDocument(feeds: List<Feed?>?, writer: Writer?, context: Context) {
override fun writeDocument(feeds: List<Feed>, writer: Writer?, context: Context) {
Logd(TAG, "Starting to write document")
val queuedEpisodeActions: MutableList<EpisodeAction> = mutableListOf()
val pausedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.paused.name), EpisodeSortOrder.DATE_NEW_OLD)
@ -1068,7 +1076,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
/** Writes saved favorites to file. */
class FavoritesWriter : ExportWriter {
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
override fun writeDocument(feeds: List<Feed?>?, writer: Writer?, context: Context) {
override fun writeDocument(feeds: List<Feed>, writer: Writer?, context: Context) {
Logd(TAG, "Starting to write document")
val templateStream = context.assets.open("html-export-template.html")
var template = IOUtils.toString(templateStream, UTF_8)
@ -1145,16 +1153,16 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
* Takes a list of feeds and a writer and writes those into an HTML document.
*/
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
override fun writeDocument(feeds: List<Feed?>?, writer: Writer?, context: Context) {
override fun writeDocument(feeds: List<Feed>, writer: Writer?, context: Context) {
Logd(TAG, "Starting to write document")
val templateStream = context.assets.open("html-export-template.html")
var template = IOUtils.toString(templateStream, "UTF-8")
template = template.replace("\\{TITLE\\}".toRegex(), "Subscriptions")
val templateParts = template.split("\\{FEEDS\\}".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
writer!!.append(templateParts[0])
for (feed in feeds!!) {
for (feed in feeds) {
writer.append("<li><div><img src=\"")
writer.append(feed!!.imageUrl)
writer.append(feed.imageUrl)
writer.append("\" /><p>")
writer.append(feed.title)
writer.append(" <span><a href=\"")

View File

@ -2,55 +2,37 @@ 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.Companion.getTitleOfPage
import ac.mdiq.podcini.ui.activity.PreferenceActivity.Screens
import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
import ac.mdiq.podcini.util.Logd
import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
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.appcompat.app.AppCompatActivity
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
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.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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
@ -63,8 +45,6 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import com.bytehamster.lib.preferencesearch.SearchPreference
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
@ -75,129 +55,150 @@ import java.io.InputStreamReader
import javax.xml.parsers.DocumentBuilderFactory
class MainPreferencesFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
Logd("MainPreferencesFragment", "onCreatePreferences")
// TODO: this can be expensive
addPreferencesFromResource(R.xml.preferences)
setupMainScreen()
setupSearch()
var copyrightNoticeText by mutableStateOf("")
// If you are writing a spin-off, please update the details on screens like "About" and "Report bug"
// and afterwards remove the following lines. Please keep in mind that Podcini is licensed under the GPL.
// This means that your application needs to be open-source under the GPL, too.
// It must also include a prominent copyright notice.
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()
// Logd("MainPreferencesFragment", "$packageHash ${"ac.mdiq.podcini.R".hashCode()}")
when {
packageHash != 1329568231 && packageHash != 1297601420 -> {
findPreference<Preference>(Prefs.project.name)!!.isVisible = false
val copyrightNotice = Preference(requireContext())
copyrightNotice.setIcon(R.drawable.ic_info_white)
copyrightNotice.icon!!.mutate().colorFilter = PorterDuffColorFilter(-0x340000, PorterDuff.Mode.MULTIPLY)
copyrightNotice.summary = ("This application is based on Podcini."
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).")
findPreference<Preference>(Prefs.project.name)!!.parent!!.addPreference(copyrightNotice)
}
packageHash == 1297601420 -> {
val debugNotice = Preference(requireContext())
debugNotice.setIcon(R.drawable.ic_info_white)
debugNotice.icon!!.mutate().colorFilter = PorterDuffColorFilter(-0x340000, PorterDuff.Mode.MULTIPLY)
debugNotice.order = -1
debugNotice.summary = "This is a development version of Podcini and not meant for daily use"
findPreference<Preference>(Prefs.project.name)!!.parent!!.addPreference(debugNotice)
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)
}
}
}
}
}
}
}
override fun onStart() {
super.onStart()
(activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.settings_label)
}
@SuppressLint("CommitTransaction")
private fun setupMainScreen() {
findPreference<Preference>(Prefs.prefScreenInterface.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
(activity as PreferenceActivity).openScreen(R.xml.preferences_user_interface)
true
}
findPreference<Preference>(Prefs.prefScreenPlayback.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
(activity as PreferenceActivity).openScreen(R.xml.preferences_playback)
true
}
findPreference<Preference>(Prefs.prefScreenDownloads.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
(activity as PreferenceActivity).openScreen(R.xml.preferences_downloads)
true
}
findPreference<Preference>(Prefs.prefScreenSynchronization.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
(activity as PreferenceActivity).openScreen(R.xml.preferences_synchronization)
true
}
findPreference<Preference>(Prefs.prefScreenImportExport.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
(activity as PreferenceActivity).openScreen(Screens.preferences_import_export)
true
}
findPreference<Preference>(Prefs.notifications.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
(activity as PreferenceActivity).openScreen(R.xml.preferences_notifications)
true
}
val switchPreference = findPreference<SwitchPreferenceCompat>("prefOPMLBackup")
switchPreference?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
if (newValue is Boolean) {
// Restart the app
val intent = context?.packageManager?.getLaunchIntentForPackage(requireContext().packageName)
intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
context?.startActivity(intent)
}
true
}
findPreference<Preference>(Prefs.prefAbout.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
parentFragmentManager.beginTransaction().replace(R.id.settingsContainer, AboutFragment()).addToBackStack(getString(R.string.about_pref)).commit()
true
}
findPreference<Preference>(Prefs.prefDocumentation.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini")
true
}
findPreference<Preference>(Prefs.prefViewForum.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini/discussions")
true
}
findPreference<Preference>(Prefs.prefContribute.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini")
true
}
findPreference<Preference>(Prefs.prefSendBugReport.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
startActivity(Intent(activity, BugReportActivity::class.java))
true
}
}
private fun setupSearch() {
val searchPreference = findPreference<SearchPreference>("searchPreference")
val config = searchPreference!!.searchConfiguration
config.setActivity((activity as AppCompatActivity))
config.setFragmentContainerViewId(R.id.settingsContainer)
config.setBreadcrumbsEnabled(true)
config.index(R.xml.preferences_user_interface).addBreadcrumb(getTitleOfPage(R.xml.preferences_user_interface))
config.index(R.xml.preferences_playback).addBreadcrumb(getTitleOfPage(R.xml.preferences_playback))
config.index(R.xml.preferences_downloads).addBreadcrumb(getTitleOfPage(R.xml.preferences_downloads))
// config.index(R.xml.preferences_import_export).addBreadcrumb(getTitleOfPage(R.xml.preferences_import_export))
config.index(R.xml.preferences_autodownload)
.addBreadcrumb(getTitleOfPage(R.xml.preferences_downloads))
.addBreadcrumb(R.string.automation)
.addBreadcrumb(getTitleOfPage(R.xml.preferences_autodownload))
config.index(R.xml.preferences_synchronization).addBreadcrumb(getTitleOfPage(R.xml.preferences_synchronization))
config.index(R.xml.preferences_notifications).addBreadcrumb(getTitleOfPage(R.xml.preferences_notifications))
// config.index(R.xml.feed_settings).addBreadcrumb(getTitleOfPage(R.xml.feed_settings))
// config.index(R.xml.preferences_swipe)
// .addBreadcrumb(getTitleOfPage(R.xml.preferences_user_interface))
// .addBreadcrumb(getTitleOfPage(R.xml.preferences_swipe))
}
class AboutFragment : PreferenceFragmentCompat() {
@SuppressLint("CommitTransaction")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {}
@ -208,7 +209,7 @@ class MainPreferencesFragment : PreferenceFragmentCompat() {
setContent {
CustomTheme(requireContext()) {
val textColor = MaterialTheme.colorScheme.onSurface
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
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)
@ -324,20 +325,4 @@ class MainPreferencesFragment : PreferenceFragmentCompat() {
private class LicenseItem(val title: String, val subtitle: String, val licenseUrl: String, val licenseTextFile: String)
}
}
@Suppress("EnumEntryName")
private enum class Prefs {
prefScreenInterface,
prefScreenPlayback,
prefScreenDownloads,
prefScreenImportExport,
prefScreenSynchronization,
prefDocumentation,
prefViewForum,
prefSendBugReport,
project,
prefAbout,
notifications,
prefContribute,
}
}

View File

@ -109,9 +109,7 @@ class UserInterfacePreferencesFragment : PreferenceFragmentCompat() {
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 completeListener = DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> fullNotificationButtons = preferredButtons }
val title = context.resources.getString(
R.string.pref_full_notification_buttons_title)
@ -136,9 +134,7 @@ class UserInterfacePreferencesFragment : PreferenceFragmentCompat() {
if (!isValid) preferredButtons.removeAt(i)
}
for (i in checked.indices) {
if (preferredButtons.contains(buttonIds[i])) checked[i] = true
}
for (i in checked.indices) if (preferredButtons.contains(buttonIds[i])) checked[i] = true
val builder = MaterialAlertDialogBuilder(context!!)
builder.setTitle(title)

View File

@ -18,7 +18,7 @@ import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.getPermutor
import ac.mdiq.podcini.storage.utils.FilesUtils.getMediafilename
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
@ -41,6 +41,8 @@ import kotlin.math.min
object Episodes {
private val TAG: String = Episodes::class.simpleName ?: "Anonymous"
private val smartMarkAsPlayedPercent: Int = 95
val prefRemoveFromQueueMarkedPlayed by lazy { appPrefs.getBoolean(Prefs.prefRemoveFromQueueMarkedPlayed.name, true) }
val prefDeleteRemovesFromQueue by lazy { appPrefs.getBoolean(Prefs.prefDeleteRemovesFromQueue.name, false) }
@ -265,4 +267,32 @@ object Episodes {
e.media = m
return e
}
@JvmStatic
fun indexOfItemWithId(episodes: List<Episode?>, id: Long): Int {
for (i in episodes.indices) {
val episode = episodes[i]
if (episode?.id == id) return i
}
return -1
}
@JvmStatic
fun episodeListContains(episodes: List<Episode?>, itemId: Long): Boolean {
return indexOfItemWithId(episodes, itemId) >= 0
}
@JvmStatic
fun indexOfItemWithDownloadUrl(items: List<Episode?>, downloadUrl: String): Int {
for (i in items.indices) {
val item = items[i]
if (item?.media?.downloadUrl == downloadUrl) return i
}
return -1
}
@JvmStatic
fun hasAlmostEnded(media: Playable): Boolean {
return media.getDuration() > 0 && media.getPosition() >= media.getDuration() * smartMarkAsPlayedPercent * 0.01
}
}

View File

@ -5,14 +5,14 @@ import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.database.Episodes.indexOfItemWithId
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.utils.EpisodeUtil.indexOfItemWithId
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.getPermutor
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
@ -73,9 +73,7 @@ object Queues {
Logd(TAG, "getQueueIDList() called")
val queues = realm.query(PlayQueue::class).find()
val ids = mutableSetOf<Long>()
for (queue in queues) {
ids.addAll(queue.episodeIds)
}
for (queue in queues) ids.addAll(queue.episodeIds)
return ids
}
@ -180,9 +178,7 @@ object Queues {
it.episodeIds.clear()
it.update()
}
for (e in curQueue.episodes) {
if (e.playState < PlayState.SKIPPED.code) setPlayState(PlayState.SKIPPED.code, false, e)
}
for (e in curQueue.episodes) if (e.playState < PlayState.SKIPPED.code) setPlayState(PlayState.SKIPPED.code, false, e)
curQueue.episodes.clear()
EventFlow.postEvent(FlowEvent.QueueEvent.cleared())
}
@ -191,9 +187,7 @@ object Queues {
fun removeFromAllQueuesSync(vararg episodes: Episode) {
Logd(TAG, "removeFromAllQueuesSync called ")
val queues = realm.query(PlayQueue::class).find()
for (q in queues) {
if (q.id != curQueue.id) removeFromQueueSync(q, *episodes)
}
for (q in queues) if (q.id != curQueue.id) removeFromQueueSync(q, *episodes)
// ensure curQueue is last updated
if (curQueue.size() > 0) removeFromQueueSync(curQueue, *episodes)
else upsertBlk(curQueue) { it.update() }
@ -250,9 +244,7 @@ object Queues {
idsInQueuesToRemove = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet()
if (idsInQueuesToRemove.isNotEmpty()) {
val eList = realm.query(Episode::class).query("id IN $0", idsInQueuesToRemove).find()
for (e in eList) {
if (e.playState < PlayState.SKIPPED.code) setPlayState(PlayState.SKIPPED.code, false, e)
}
for (e in eList) if (e.playState < PlayState.SKIPPED.code) setPlayState(PlayState.SKIPPED.code, false, e)
upsert(q) {
it.idsBinList.removeAll(idsInQueuesToRemove)
it.idsBinList.addAll(idsInQueuesToRemove)
@ -272,9 +264,7 @@ object Queues {
idsInQueuesToRemove = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet()
if (idsInQueuesToRemove.isNotEmpty()) {
val eList = realm.query(Episode::class).query("id IN $0", idsInQueuesToRemove).find()
for (e in eList) {
if (e.playState < PlayState.SKIPPED.code) setPlayState(PlayState.SKIPPED.code, false, e)
}
for (e in eList) if (e.playState < PlayState.SKIPPED.code) setPlayState(PlayState.SKIPPED.code, false, e)
curQueue = upsert(q) {
it.idsBinList.removeAll(idsInQueuesToRemove)
it.idsBinList.addAll(idsInQueuesToRemove)
@ -313,13 +303,11 @@ object Queues {
}
}
fun inAnyQueue(episode: Episode): Boolean {
val queues = realm.query(PlayQueue::class).find()
for (q in queues) {
if (q.contains(episode)) return true
}
return false
}
// fun inAnyQueue(episode: Episode): Boolean {
// val queues = realm.query(PlayQueue::class).find()
// for (q in queues) if (q.contains(episode)) return true
// return false
// }
class EnqueuePositionPolicy(private val enqueueLocation: EnqueueLocation) {
/**
@ -349,9 +337,7 @@ object Queues {
}
private fun getPositionOfFirstNonDownloadingItem(startPosition: Int, queueItems: List<Episode>): Int {
val curQueueSize = queueItems.size
for (i in startPosition until curQueueSize) {
if (!isItemAtPositionDownloading(i, queueItems)) return i
}
for (i in startPosition until curQueueSize) if (!isItemAtPositionDownloading(i, queueItems)) return i
return curQueueSize
}
private fun isItemAtPositionDownloading(position: Int, queueItems: List<Episode>): Boolean {
@ -362,9 +348,7 @@ object Queues {
private fun getCurrentlyPlayingPosition(queueItems: List<Episode>, currentPlaying: Playable?): Int {
if (currentPlaying !is EpisodeMedia) return -1
val curPlayingItemId = currentPlaying.episodeOrFetch()?.id
for (i in queueItems.indices) {
if (curPlayingItemId == queueItems[i].id) return i
}
for (i in queueItems.indices) if (curPlayingItemId == queueItems[i].id) return i
return -1
}
}

View File

@ -40,7 +40,7 @@ object RealmDB {
SubscriptionLog::class,
Chapter::class))
.name("Podcini.realm")
.schemaVersion(34)
.schemaVersion(35)
.migration({ mContext ->
val oldRealm = mContext.oldRealm // old realm using the previous schema
val newRealm = mContext.newRealm // new realm using the new schema

View File

@ -92,6 +92,8 @@ class Episode : RealmObject {
@FullText
var comment: String = ""
var commentTime: Long = 0L
@Ignore
val isNew: Boolean
get() = playState == PlayState.NEW.code

View File

@ -1,6 +1,8 @@
package ac.mdiq.podcini.storage.model
import ac.mdiq.podcini.R
import java.util.Date
import java.util.Locale
enum class EpisodeSortOrder(val code: Int, val res: Int) {
DATE_OLD_NEW(1, R.string.publish_date),
@ -21,6 +23,8 @@ enum class EpisodeSortOrder(val code: Int, val res: Int) {
DOWNLOAD_DATE_NEW_OLD(16, R.string.download_date),
VIEWS_LOW_HIGH(17, R.string.view_count),
VIEWS_HIGH_LOW(18, R.string.view_count),
COMMENT_DATE_OLD_NEW(19, R.string.last_comment_date),
COMMENT_DATE_NEW_OLD(20, R.string.last_comment_date),
FEED_TITLE_A_Z(101, R.string.feed_title),
FEED_TITLE_Z_A(102, R.string.feed_title),
@ -60,5 +64,187 @@ enum class EpisodeSortOrder(val code: Int, val res: Int) {
for (i in stringValues.indices) values[i] = valueOf(stringValues[i]!!)
return values
}
/**
* Returns a Permutor that sorts a list appropriate to the given sort order.
* @return Permutor that sorts a list appropriate to the given sort order.
*/
@JvmStatic
fun getPermutor(sortOrder: EpisodeSortOrder): Permutor<Episode> {
var comparator: java.util.Comparator<Episode>? = null
var permutor: Permutor<Episode>? = null
when (sortOrder) {
EPISODE_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemTitle(f1).compareTo(itemTitle(f2)) }
EPISODE_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemTitle(f2).compareTo(itemTitle(f1)) }
DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> pubDate(f1).compareTo(pubDate(f2)) }
DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> pubDate(f2).compareTo(pubDate(f1)) }
DURATION_SHORT_LONG -> comparator = Comparator { f1: Episode?, f2: Episode? -> duration(f1).compareTo(duration(f2)) }
DURATION_LONG_SHORT -> comparator = Comparator { f1: Episode?, f2: Episode? -> duration(f2).compareTo(duration(f1)) }
EPISODE_FILENAME_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemLink(f1).compareTo(itemLink(f2)) }
EPISODE_FILENAME_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemLink(f2).compareTo(itemLink(f1)) }
PLAYED_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> playDate(f1).compareTo(playDate(f2)) }
PLAYED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> playDate(f2).compareTo(playDate(f1)) }
COMPLETED_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> completeDate(f1).compareTo(completeDate(f2)) }
COMPLETED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> completeDate(f2).compareTo(completeDate(f1)) }
DOWNLOAD_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> downloadDate(f1).compareTo(downloadDate(f2)) }
DOWNLOAD_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> downloadDate(f2).compareTo(downloadDate(f1)) }
VIEWS_LOW_HIGH -> comparator = Comparator { f1: Episode?, f2: Episode? -> viewCount(f1).compareTo(viewCount(f2)) }
VIEWS_HIGH_LOW -> comparator = Comparator { f1: Episode?, f2: Episode? -> viewCount(f2).compareTo(viewCount(f1)) }
COMMENT_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> commentDate(f1).compareTo(commentDate(f2)) }
COMMENT_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> commentDate(f2).compareTo(playDate(f1)) }
FEED_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f1).compareTo(feedTitle(f2)) }
FEED_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f2).compareTo(feedTitle(f1)) }
RANDOM, RANDOM1 -> permutor = object : Permutor<Episode> {
override fun reorder(queue: MutableList<Episode>?) {
if (!queue.isNullOrEmpty()) queue.shuffle()
}
}
SMART_SHUFFLE_OLD_NEW -> permutor = object : Permutor<Episode> {
override fun reorder(queue: MutableList<Episode>?) {
if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList<Episode?>, true)
}
}
SMART_SHUFFLE_NEW_OLD -> permutor = object : Permutor<Episode> {
override fun reorder(queue: MutableList<Episode>?) {
if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList<Episode?>, false)
}
}
SIZE_SMALL_LARGE -> comparator = Comparator { f1: Episode?, f2: Episode? -> size(f1).compareTo(size(f2)) }
SIZE_LARGE_SMALL -> comparator = Comparator { f1: Episode?, f2: Episode? -> size(f2).compareTo(size(f1)) }
}
if (comparator != null) {
val comparator2: java.util.Comparator<Episode> = comparator
permutor = object : Permutor<Episode> {
override fun reorder(queue: MutableList<Episode>?) {if (!queue.isNullOrEmpty()) queue.sortWith(comparator2)}
}
}
return permutor!!
}
private fun pubDate(item: Episode?): Date {
return if (item == null) Date() else Date(item.pubDate)
}
private fun playDate(item: Episode?): Long {
return item?.media?.getLastPlayedTime() ?: 0
}
private fun commentDate(item: Episode?): Long {
return item?.commentTime ?: 0
}
private fun downloadDate(item: Episode?): Long {
return item?.media?.downloadTime ?: 0
}
private fun completeDate(item: Episode?): Date {
return item?.media?.playbackCompletionDate ?: Date(0)
}
private fun itemTitle(item: Episode?): String {
return (item?.title ?: "").lowercase(Locale.getDefault())
}
private fun duration(item: Episode?): Int {
return item?.media?.getDuration() ?: 0
}
private fun size(item: Episode?): Long {
return item?.media?.size ?: 0
}
private fun itemLink(item: Episode?): String {
return (item?.link ?: "").lowercase(Locale.getDefault())
}
private fun feedTitle(item: Episode?): String {
return (item?.feed?.title ?: "").lowercase(Locale.getDefault())
}
private fun viewCount(item: Episode?): Int {
return item?.viewCount ?: 0
}
/**
* Implements a reordering by pubdate that avoids consecutive episodes from the same feed in the queue.
* A listener might want to hear episodes from any given feed in pubdate order, but would
* prefer a more balanced ordering that avoids having to listen to clusters of consecutive
* episodes from the same feed. This is what "Smart Shuffle" tries to accomplish.
* Assume the queue looks like this: `ABCDDEEEEEEEEEE`.
* This method first starts with a queue of the final size, where each slot is empty (null).
* It takes the podcast with most episodes (`E`) and places the episodes spread out in the queue: `EE_E_EE_E_EE_EE`.
* The podcast with the second-most number of episodes (`D`) is then
* placed spread-out in the *available* slots: `EE_EDEE_EDEE_EE`.
* This continues, until we end up with: `EEBEDEECEDEEAEE`.
* Note that episodes aren't strictly ordered in terms of pubdate, but episodes of each feed are.
*
* @param queue A (modifiable) list of FeedItem elements to be reordered.
* @param ascending `true` to use ascending pubdate in the reordering;
* `false` for descending.
*/
private fun smartShuffle(queue: MutableList<Episode?>, ascending: Boolean) {
// Divide FeedItems into lists by feed
val map: MutableMap<Long, MutableList<Episode>> = HashMap()
for (item in queue) {
if (item == null) continue
val id = item.feedId
if (id != null) {
if (!map.containsKey(id)) map[id] = ArrayList()
map[id]!!.add(item)
}
}
// Sort each individual list by PubDate (ascending/descending)
val itemComparator: java.util.Comparator<Episode> =
if (ascending) Comparator { f1: Episode, f2: Episode -> f1.pubDate.compareTo(f2.pubDate) }
else Comparator { f1: Episode, f2: Episode -> f2.pubDate.compareTo(f1.pubDate) }
val feeds: MutableList<List<Episode>> = ArrayList()
for ((_, value) in map) {
value.sortWith(itemComparator)
feeds.add(value)
}
val emptySlots = ArrayList<Int>()
for (i in queue.indices) {
queue[i] = null
emptySlots.add(i)
}
// Starting with the largest feed, place items spread out through the empty slots in the queue
feeds.sortWith { f1: List<Episode>, f2: List<Episode> -> f2.size.compareTo(f1.size) }
for (feedItems in feeds) {
val spread = emptySlots.size.toDouble() / (feedItems.size + 1)
val emptySlotIterator = emptySlots.iterator()
var skipped = 0
var placed = 0
while (emptySlotIterator.hasNext()) {
val nextEmptySlot = emptySlotIterator.next()
skipped++
if (skipped >= spread * (placed + 1)) {
if (queue[nextEmptySlot] != null) throw RuntimeException("Slot to be placed in not empty")
queue[nextEmptySlot] = feedItems[placed]
emptySlotIterator.remove()
placed++
if (placed == feedItems.size) break
}
}
}
}
/**
* Interface for passing around list permutor method. This is used for cases where a simple comparator
* won't work (e.g. Random, Smart Shuffle, etc)
* @param <E> the type of elements in the list
</E> */
interface Permutor<E> {
/**
* Reorders the specified list.
* @param queue A (modifiable) list of elements to be reordered
*/
fun reorder(queue: MutableList<E>?)
}
}
}

View File

@ -3,7 +3,7 @@ package ac.mdiq.podcini.storage.model
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.model.FeedFunding.Companion.extractPaymentLinks
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.fromCode
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.getPermutor
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@ -102,6 +102,8 @@ class Feed : RealmObject {
@FullText
var comment: String = ""
var commentTime: Long = 0L
/**
* Returns the value that uniquely identifies this Feed. If the
* feedIdentifier attribute is not null, it will be returned. Else it will
@ -288,6 +290,10 @@ class Feed : RealmObject {
paymentLinks.add(funding)
}
fun isSynthetic(): Boolean {
return id <= MAX_SYNTHETIC_ID
}
fun getVirtualQueueItems(): List<Episode> {
var qString = "feedId == $id AND (playState < ${PlayState.SKIPPED.code} OR playState == ${PlayState.AGAIN.code} OR playState == ${PlayState.FOREVER.code})"
// TODO: perhaps need to set prefStreamOverDownload for youtube feeds

View File

@ -1,40 +0,0 @@
package ac.mdiq.podcini.storage.utils
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.Playable
object EpisodeUtil {
private val TAG: String = EpisodeUtil::class.simpleName ?: "Anonymous"
// val smartMarkAsPlayedSecs: Int
// get() = appPrefs.getString(UserPreferences.Prefs.prefSmartMarkAsPlayedSecs.name, "30")!!.toInt()
private val smartMarkAsPlayedPercent: Int = 95
@JvmStatic
fun indexOfItemWithId(episodes: List<Episode?>, id: Long): Int {
for (i in episodes.indices) {
val episode = episodes[i]
if (episode?.id == id) return i
}
return -1
}
@JvmStatic
fun episodeListContains(episodes: List<Episode?>, itemId: Long): Boolean {
return indexOfItemWithId(episodes, itemId) >= 0
}
@JvmStatic
fun indexOfItemWithDownloadUrl(items: List<Episode?>, downloadUrl: String): Int {
for (i in items.indices) {
val item = items[i]
if (item?.media?.downloadUrl == downloadUrl) return i
}
return -1
}
@JvmStatic
fun hasAlmostEnded(media: Playable): Boolean {
return media.getDuration() > 0 && media.getPosition() >= media.getDuration() * smartMarkAsPlayedPercent * 0.01
}
}

View File

@ -1,186 +0,0 @@
package ac.mdiq.podcini.storage.utils
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import java.util.*
/**
* Provides method for sorting the a list of [Episode] according to rules.
*/
object EpisodesPermutors {
/**
* Returns a Permutor that sorts a list appropriate to the given sort order.
* @return Permutor that sorts a list appropriate to the given sort order.
*/
@JvmStatic
fun getPermutor(sortOrder: EpisodeSortOrder): Permutor<Episode> {
var comparator: Comparator<Episode>? = null
var permutor: Permutor<Episode>? = null
when (sortOrder) {
EpisodeSortOrder.EPISODE_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemTitle(f1).compareTo(itemTitle(f2)) }
EpisodeSortOrder.EPISODE_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemTitle(f2).compareTo(itemTitle(f1)) }
EpisodeSortOrder.DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> pubDate(f1).compareTo(pubDate(f2)) }
EpisodeSortOrder.DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> pubDate(f2).compareTo(pubDate(f1)) }
EpisodeSortOrder.DURATION_SHORT_LONG -> comparator = Comparator { f1: Episode?, f2: Episode? -> duration(f1).compareTo(duration(f2)) }
EpisodeSortOrder.DURATION_LONG_SHORT -> comparator = Comparator { f1: Episode?, f2: Episode? -> duration(f2).compareTo(duration(f1)) }
EpisodeSortOrder.EPISODE_FILENAME_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemLink(f1).compareTo(itemLink(f2)) }
EpisodeSortOrder.EPISODE_FILENAME_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemLink(f2).compareTo(itemLink(f1)) }
EpisodeSortOrder.PLAYED_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> playDate(f1).compareTo(playDate(f2)) }
EpisodeSortOrder.PLAYED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> playDate(f2).compareTo(playDate(f1)) }
EpisodeSortOrder.COMPLETED_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> completeDate(f1).compareTo(completeDate(f2)) }
EpisodeSortOrder.COMPLETED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> completeDate(f2).compareTo(completeDate(f1)) }
EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> downloadDate(f1).compareTo(downloadDate(f2)) }
EpisodeSortOrder.DOWNLOAD_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> downloadDate(f2).compareTo(downloadDate(f1)) }
EpisodeSortOrder.VIEWS_LOW_HIGH -> comparator = Comparator { f1: Episode?, f2: Episode? -> viewCount(f1).compareTo(viewCount(f2)) }
EpisodeSortOrder.VIEWS_HIGH_LOW -> comparator = Comparator { f1: Episode?, f2: Episode? -> viewCount(f2).compareTo(viewCount(f1)) }
EpisodeSortOrder.FEED_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f1).compareTo(feedTitle(f2)) }
EpisodeSortOrder.FEED_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f2).compareTo(feedTitle(f1)) }
EpisodeSortOrder.RANDOM, EpisodeSortOrder.RANDOM1 -> permutor = object : Permutor<Episode> {
override fun reorder(queue: MutableList<Episode>?) {
if (!queue.isNullOrEmpty()) queue.shuffle()
}
}
EpisodeSortOrder.SMART_SHUFFLE_OLD_NEW -> permutor = object : Permutor<Episode> {
override fun reorder(queue: MutableList<Episode>?) {
if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList<Episode?>, true)
}
}
EpisodeSortOrder.SMART_SHUFFLE_NEW_OLD -> permutor = object : Permutor<Episode> {
override fun reorder(queue: MutableList<Episode>?) {
if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList<Episode?>, false)
}
}
EpisodeSortOrder.SIZE_SMALL_LARGE -> comparator = Comparator { f1: Episode?, f2: Episode? -> size(f1).compareTo(size(f2)) }
EpisodeSortOrder.SIZE_LARGE_SMALL -> comparator = Comparator { f1: Episode?, f2: Episode? -> size(f2).compareTo(size(f1)) }
}
if (comparator != null) {
val comparator2: Comparator<Episode> = comparator
permutor = object : Permutor<Episode> {
override fun reorder(queue: MutableList<Episode>?) {if (!queue.isNullOrEmpty()) queue.sortWith(comparator2)}
}
}
return permutor!!
}
private fun pubDate(item: Episode?): Date {
return if (item == null) Date() else Date(item.pubDate)
}
private fun playDate(item: Episode?): Long {
return item?.media?.getLastPlayedTime() ?: 0
}
private fun downloadDate(item: Episode?): Long {
return item?.media?.downloadTime ?: 0
}
private fun completeDate(item: Episode?): Date {
return item?.media?.playbackCompletionDate ?: Date(0)
}
private fun itemTitle(item: Episode?): String {
return (item?.title ?: "").lowercase(Locale.getDefault())
}
private fun duration(item: Episode?): Int {
return item?.media?.getDuration() ?: 0
}
private fun size(item: Episode?): Long {
return item?.media?.size ?: 0
}
private fun itemLink(item: Episode?): String {
return (item?.link ?: "").lowercase(Locale.getDefault())
}
private fun feedTitle(item: Episode?): String {
return (item?.feed?.title ?: "").lowercase(Locale.getDefault())
}
private fun viewCount(item: Episode?): Int {
return item?.viewCount ?: 0
}
/**
* Implements a reordering by pubdate that avoids consecutive episodes from the same feed in the queue.
* A listener might want to hear episodes from any given feed in pubdate order, but would
* prefer a more balanced ordering that avoids having to listen to clusters of consecutive
* episodes from the same feed. This is what "Smart Shuffle" tries to accomplish.
* Assume the queue looks like this: `ABCDDEEEEEEEEEE`.
* This method first starts with a queue of the final size, where each slot is empty (null).
* It takes the podcast with most episodes (`E`) and places the episodes spread out in the queue: `EE_E_EE_E_EE_EE`.
* The podcast with the second-most number of episodes (`D`) is then
* placed spread-out in the *available* slots: `EE_EDEE_EDEE_EE`.
* This continues, until we end up with: `EEBEDEECEDEEAEE`.
* Note that episodes aren't strictly ordered in terms of pubdate, but episodes of each feed are.
*
* @param queue A (modifiable) list of FeedItem elements to be reordered.
* @param ascending `true` to use ascending pubdate in the reordering;
* `false` for descending.
*/
private fun smartShuffle(queue: MutableList<Episode?>, ascending: Boolean) {
// Divide FeedItems into lists by feed
val map: MutableMap<Long, MutableList<Episode>> = HashMap()
for (item in queue) {
if (item == null) continue
val id = item.feedId
if (id != null) {
if (!map.containsKey(id)) map[id] = ArrayList()
map[id]!!.add(item)
}
}
// Sort each individual list by PubDate (ascending/descending)
val itemComparator: Comparator<Episode> =
if (ascending) Comparator { f1: Episode, f2: Episode -> f1.pubDate.compareTo(f2.pubDate) }
else Comparator { f1: Episode, f2: Episode -> f2.pubDate.compareTo(f1.pubDate) }
val feeds: MutableList<List<Episode>> = ArrayList()
for ((_, value) in map) {
value.sortWith(itemComparator)
feeds.add(value)
}
val emptySlots = ArrayList<Int>()
for (i in queue.indices) {
queue[i] = null
emptySlots.add(i)
}
// Starting with the largest feed, place items spread out through the empty slots in the queue
feeds.sortWith { f1: List<Episode>, f2: List<Episode> -> f2.size.compareTo(f1.size) }
for (feedItems in feeds) {
val spread = emptySlots.size.toDouble() / (feedItems.size + 1)
val emptySlotIterator = emptySlots.iterator()
var skipped = 0
var placed = 0
while (emptySlotIterator.hasNext()) {
val nextEmptySlot = emptySlotIterator.next()
skipped++
if (skipped >= spread * (placed + 1)) {
if (queue[nextEmptySlot] != null) throw RuntimeException("Slot to be placed in not empty")
queue[nextEmptySlot] = feedItems[placed]
emptySlotIterator.remove()
placed++
if (placed == feedItems.size) break
}
}
}
}
/**
* Interface for passing around list permutor method. This is used for cases where a simple comparator
* won't work (e.g. Random, Smart Shuffle, etc)
* @param <E> the type of elements in the list
</E> */
interface Permutor<E> {
/**
* Reorders the specified list.
* @param queue A (modifiable) list of elements to be reordered
*/
fun reorder(queue: MutableList<E>?)
}
}

View File

@ -14,13 +14,15 @@ import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.model.PlayState
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
import ac.mdiq.podcini.storage.database.Episodes.hasAlmostEnded
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.compose.*
import ac.mdiq.podcini.ui.fragment.*
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.MiscFormatter.fullDateTimeString
import ac.mdiq.podcini.util.MiscFormatter.localDateTimeString
import android.content.Context
import android.content.DialogInterface
import android.content.SharedPreferences
@ -293,19 +295,18 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
setContent {
CustomTheme(fragment.requireContext()) {
if (showEditComment) {
ChooseRatingDialog(listOf(item)) {
showEditComment = false
(fragment.view as? ViewGroup)?.removeView(this@apply)
}
var commentTextState by remember { mutableStateOf(TextFieldValue(item.comment)) }
val localTime = remember { System.currentTimeMillis() }
val initCommentText = remember { (if (item.comment.isBlank()) "" else item.comment + "\n") + fullDateTimeString(localTime) + ":\n" }
var commentTextState by remember { mutableStateOf(TextFieldValue(initCommentText)) }
LargeTextEditingDialog(textState = commentTextState, onTextChange = { commentTextState = it },
onDismissRequest = {
showEditComment = false
(fragment.view as? ViewGroup)?.removeView(this@apply)
},
onSave = {
runOnIOScope { upsert(item) { it.comment = commentTextState.text } }
})
onSave = { runOnIOScope { upsert(item) {
it.comment = commentTextState.text
it.commentTime = localTime
} } })
}
}
}
@ -510,7 +511,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
// delay(ceil((duration * 1.05f).toDouble()).toLong())
// val media: EpisodeMedia? = item.media
// val shouldAutoDelete = if (item.feed == null) false else shouldAutoDeleteItem(item.feed!!)
// if (media != null && EpisodeUtil.hasAlmostEnded(media) && shouldAutoDelete) {
// if (media != null && Episodes.hasAlmostEnded(media) && shouldAutoDelete) {
//// deleteMediaOfEpisode(fragment.requireContext(), item)
// val item_ = deleteMediaSync(fragment.requireContext(), item)
// if (prefDeleteRemovesFromQueue) removeFromQueueSync(null, item_) }

View File

@ -54,11 +54,7 @@ class OpmlImportActivity : AppCompatActivity() {
private val titleList: List<String>
get() {
val result: MutableList<String> = ArrayList()
if (!readElements.isNullOrEmpty()) {
for (element in readElements!!) {
if (element.text != null) result.add(element.text!!)
}
}
if (!readElements.isNullOrEmpty()) for (element in readElements!!) if (element.text != null) result.add(element.text!!)
return result
}
@ -85,9 +81,7 @@ class OpmlImportActivity : AppCompatActivity() {
binding.feedlist.onItemClickListener = OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, _: Long ->
val checked = binding.feedlist.checkedItemPositions
var checkedCount = 0
for (i in 0 until checked.size()) {
if (checked.valueAt(i)) checkedCount++
}
for (i in 0 until checked.size()) if (checked.valueAt(i)) checkedCount++
if (listAdapter != null) {
if (checkedCount == listAdapter!!.count) {
selectAll.isVisible = false
@ -104,10 +98,10 @@ class OpmlImportActivity : AppCompatActivity() {
}
binding.butConfirm.setOnClickListener {
binding.progressBar.visibility = View.VISIBLE
val checked = binding.feedlist.checkedItemPositions
lifecycleScope.launch {
try {
withContext(Dispatchers.IO) {
val checked = binding.feedlist.checkedItemPositions
for (i in 0 until checked.size()) {
if (!checked.valueAt(i)) continue
@ -144,10 +138,7 @@ class OpmlImportActivity : AppCompatActivity() {
private fun importUri(uri: Uri?) {
if (uri == null) {
MaterialAlertDialogBuilder(this)
.setMessage(R.string.opml_import_error_no_file)
.setPositiveButton(android.R.string.ok, null)
.show()
MaterialAlertDialogBuilder(this).setMessage(R.string.opml_import_error_no_file).setPositiveButton(android.R.string.ok, null).show()
return
}
this.uri = uri

View File

@ -78,7 +78,7 @@ class PreferenceActivity : AppCompatActivity(), SearchPreferenceResultListener {
fun openScreen(screen: Int): PreferenceFragmentCompat {
val fragment = when (screen) {
R.xml.preferences_user_interface -> UserInterfacePreferencesFragment()
R.xml.preferences_downloads -> DownloadsPreferencesFragment()
// R.xml.preferences_downloads -> DownloadsPreferencesFragment()
// R.xml.preferences_import_export -> ImportExportPreferencesFragment()
R.xml.preferences_autodownload -> AutoDownloadPreferencesFragment()
R.xml.preferences_synchronization -> SynchronizationPreferencesFragment()
@ -228,7 +228,7 @@ class PreferenceActivity : AppCompatActivity(), SearchPreferenceResultListener {
@JvmStatic
fun getTitleOfPage(preferences: Int): Int {
return when (preferences) {
R.xml.preferences_downloads -> R.string.downloads_pref
// R.xml.preferences_downloads -> R.string.downloads_pref
R.xml.preferences_autodownload -> R.string.pref_automatic_download_title
R.xml.preferences_playback -> R.string.playback_pref
// R.xml.preferences_import_export -> R.string.import_export_pref

View File

@ -1,80 +1,24 @@
package ac.mdiq.podcini.ui.compose
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.center
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PaintingStyle.Companion.Stroke
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import kotlinx.coroutines.delay
import kotlin.math.cos
import kotlin.math.sin
@Composable
private fun CustomTextField(
modifier: Modifier = Modifier,
leadingIcon: (@Composable () -> Unit)? = null,
trailingIcon: (@Composable () -> Unit)? = null,
placeholderText: String = "Placeholder",
fontSize: TextUnit = MaterialTheme.typography.bodyMedium.fontSize
) {
var text by rememberSaveable { mutableStateOf("") }
BasicTextField(
modifier = modifier.background(MaterialTheme.colorScheme.surface, MaterialTheme.shapes.small).fillMaxWidth(),
value = text,
onValueChange = { text = it },
singleLine = true,
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
textStyle = LocalTextStyle.current.copy(
color = MaterialTheme.colorScheme.onSurface,
fontSize = fontSize
),
decorationBox = { innerTextField ->
Row(modifier, verticalAlignment = Alignment.CenterVertically) {
if (leadingIcon != null) leadingIcon()
Box(Modifier.weight(1f)) {
if (text.isEmpty())
Text(text = placeholderText, style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f), fontSize = fontSize))
innerTextField()
}
if (trailingIcon != null) trailingIcon()
}
}
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -168,8 +112,6 @@ fun CustomToast(message: String, durationMillis: Long = 2000L, onDismiss: () ->
delay(durationMillis)
onDismiss()
}
// Box to display the toast message at the bottom of the screen
Box(modifier = Modifier.fillMaxWidth().padding(16.dp), contentAlignment = Alignment.BottomCenter) {
Box(modifier = Modifier.background(Color.Black, RoundedCornerShape(8.dp)).padding(8.dp)) {
Text(text = message, color = Color.White, style = MaterialTheme.typography.bodyMedium)
@ -190,15 +132,11 @@ fun LargeTextEditingDialog(textState: TextFieldValue, onTextChange: (TextFieldVa
)
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) {
TextButton(onClick = { onDismissRequest() }) {
Text("Cancel")
}
TextButton(onClick = { onDismissRequest() }) { Text("Cancel") }
TextButton(onClick = {
onSave(textState.text)
onDismissRequest()
}) {
Text("Save")
}
}) { Text("Save") }
}
}
}
@ -221,9 +159,7 @@ fun NonlazyGrid(columns: Int, itemCount: Int, modifier: Modifier = Modifier, con
Row {
for (columnId in 0 until columns) {
val index = firstIndex + columnId
Box(modifier = Modifier.fillMaxWidth().weight(1f)) {
if (index < itemCount) content(index)
}
Box(modifier = Modifier.fillMaxWidth().weight(1f)) { if (index < itemCount) content(index) }
}
}
}

View File

@ -34,7 +34,7 @@ import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.model.Feed.Companion.MAX_SYNTHETIC_ID
import ac.mdiq.podcini.storage.model.Feed.Companion.newId
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
import ac.mdiq.podcini.storage.database.Episodes.hasAlmostEnded
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
import ac.mdiq.podcini.ui.actions.EpisodeActionButton
import ac.mdiq.podcini.ui.actions.NullActionButton
@ -47,6 +47,7 @@ import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.MiscFormatter.formatDateTimeFlex
import ac.mdiq.podcini.util.MiscFormatter.formatNumber
import ac.mdiq.podcini.util.MiscFormatter.localDateTimeString
import ac.mdiq.vista.extractor.Vista
import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeServiceURL
import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeURL
@ -395,7 +396,7 @@ fun EraseEpisodesDialog(selected: List<Episode>, feed: Feed?, onDismissRequest:
Dialog(onDismissRequest = onDismissRequest) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
if (feed == null || feed.id > MAX_SYNTHETIC_ID) Text(stringResource(R.string.not_erase_message), modifier = Modifier.padding(10.dp))
if (feed == null || !feed.isSynthetic()) Text(stringResource(R.string.not_erase_message), modifier = Modifier.padding(10.dp))
else Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(message + ": ${selected.size}")
Text(stringResource(R.string.feed_delete_reason_msg))
@ -410,8 +411,8 @@ fun EraseEpisodesDialog(selected: List<Episode>, feed: Feed?, onDismissRequest:
val sLog = SubscriptionLog(e.id, e.title?:"", e.media?.downloadUrl?:"", e.link?:"", SubscriptionLog.Type.Media.name)
upsert(sLog) {
it.rating = e.rating
it.comment = e.comment
it.comment += "\nReason to remove:\n" + textState.text
it.comment = if (e.comment.isBlank()) "" else (e.comment + "\n")
it.comment += localDateTimeString() + "\nReason to remove:\n" + textState.text
it.cancelDate = Date().time
}
}
@ -594,7 +595,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
Text(stringResource(id = R.string.reserve_episodes_label))
}
}
if (feed != null && feed.id <= MAX_SYNTHETIC_ID) {
if (feed != null && feed.isSynthetic()) {
options.add {
Row(modifier = Modifier.padding(horizontal = 16.dp).clickable {
isExpanded = false
@ -719,8 +720,11 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
modifier = Modifier.width(imageWidth).height(imageHeight)
.clickable(onClick = {
Logd(TAG, "icon clicked!")
if (selectMode) toggleSelected(vm)
else if (vm.episode.feed != null) activity.loadChildFragment(FeedInfoFragment.newInstance(vm.episode.feed!!))
when {
selectMode -> toggleSelected(vm)
vm.episode.feed != null && vm.episode.feed?.isSynthetic() != true -> activity.loadChildFragment(FeedInfoFragment.newInstance(vm.episode.feed!!))
else -> activity.loadChildFragment(EpisodeInfoFragment.newInstance(vm.episode))
}
}))
Box(Modifier.weight(1f).height(imageHeight)) {
TitleColumn(vm, index, modifier = Modifier.fillMaxWidth())
@ -735,9 +739,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
Logd(TAG, "LaunchedEffect $index isPlayingState: ${vms[index].isPlayingState} ${vm.episode.playState} ${vms[index].episode.title}")
Logd(TAG, "LaunchedEffect $index downloadState: ${vms[index].downloadState} ${vm.episode.media?.downloaded} ${vm.dlPercent}")
vm.actionButton = vm.actionButton.forItem(vm.episode)
if (vm.actionButton.getLabel() != actionButton.getLabel()) {
actionButton = vm.actionButton
}
if (vm.actionButton.getLabel() != actionButton.getLabel()) actionButton = vm.actionButton
}
} else {
LaunchedEffect(Unit) {
@ -1013,18 +1015,16 @@ fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: Mutable
val selectedList = remember { MutableList(item.values.size) { mutableStateOf(false)} }
var expandRow by remember { mutableStateOf(false) }
Row {
Text(stringResource(item.nameRes) + ".. :", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineSmall, color = textColor, modifier = Modifier.clickable {
expandRow = !expandRow
})
Text(stringResource(item.nameRes) + ".. :", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineSmall, color = textColor,
modifier = Modifier.clickable { expandRow = !expandRow })
var lowerSelected by remember { mutableStateOf(false) }
var higherSelected by remember { mutableStateOf(false) }
Spacer(Modifier.weight(1f))
if (expandRow) Text("<<<", color = if (lowerSelected) Color.Green else buttonColor, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.clickable {
val hIndex = selectedList.indexOfLast { it.value }
if (hIndex < 0) return@clickable
if (!lowerSelected) {
for (i in 0..hIndex) selectedList[i].value = true
} else {
if (!lowerSelected) for (i in 0..hIndex) selectedList[i].value = true
else {
for (i in 0..hIndex) selectedList[i].value = false
selectedList[hIndex].value = true
}
@ -1067,9 +1067,7 @@ fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: Mutable
if (expandRow) NonlazyGrid(columns = 3, itemCount = item.values.size) { index ->
if (selectNone) selectedList[index].value = false
LaunchedEffect(Unit) {
if (filter != null) {
if (item.values[index].filterId in filter.properties) selectedList[index].value = true
}
if (filter != null && item.values[index].filterId in filter.properties) selectedList[index].value = true
}
OutlinedButton(
modifier = Modifier.padding(0.dp).heightIn(min = 20.dp).widthIn(min = 20.dp).wrapContentWidth(),

View File

@ -28,6 +28,7 @@ import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.MiscFormatter
import ac.mdiq.podcini.util.MiscFormatter.localDateTimeString
import android.util.Log
import android.view.Gravity
import androidx.compose.foundation.*
@ -115,12 +116,12 @@ fun RemoveFeedDialog(feeds: List<Feed>, onDismissRequest: () -> Unit, callback:
CoroutineScope(Dispatchers.IO).launch {
try {
for (f in feeds) {
if (f.id > MAX_SYNTHETIC_ID) {
if (!f.isSynthetic()) {
val sLog = SubscriptionLog(f.id, f.title ?: "", f.downloadUrl ?: "", f.link ?: "", SubscriptionLog.Type.Feed.name)
upsert(sLog) {
it.rating = f.rating
it.comment = f.comment
it.comment += "\nReason to remove:\n" + textState.text
it.comment = if (f.comment.isBlank()) "" else (f.comment + "\n")
it.comment += localDateTimeString() + "\nReason to remove:\n" + textState.text
it.cancelDate = Date().time
}
} else {
@ -128,8 +129,8 @@ fun RemoveFeedDialog(feeds: List<Feed>, onDismissRequest: () -> Unit, callback:
val sLog = SubscriptionLog(e.id, e.title ?: "", e.media?.downloadUrl ?: "", e.link ?: "", SubscriptionLog.Type.Media.name)
upsert(sLog) {
it.rating = e.rating
it.comment = e.comment
it.comment += "\nReason to remove:\n" + textState.text
it.comment = if (e.comment.isBlank()) "" else (e.comment + "\n")
it.comment += localDateTimeString() + "\nReason to remove:\n" + textState.text
it.cancelDate = Date().time
}
}

View File

@ -3,16 +3,15 @@ package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.ComposeFragmentBinding
import ac.mdiq.podcini.net.download.DownloadStatus
import ac.mdiq.podcini.storage.database.Episodes
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.ui.actions.SwipeAction
import ac.mdiq.podcini.ui.actions.SwipeActions
import ac.mdiq.podcini.ui.actions.SwipeActions.NoActionSwipeAction
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.compose.*
import ac.mdiq.podcini.ui.fragment.DownloadsFragment.Companion.downloadsSortedOrder
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
@ -28,7 +27,6 @@ import androidx.compose.runtime.setValue
import androidx.core.util.Pair
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.google.android.material.appbar.MaterialToolbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -164,7 +162,7 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
if (loadItemsRunning) return
for (url in event.urls) {
// if (!event.isCompleted(url)) continue
val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(episodes, url)
val pos: Int = Episodes.indexOfItemWithDownloadUrl(episodes, url)
if (pos >= 0) {
// episodes[pos].downloadState.value = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
vms[pos].downloadState = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal

View File

@ -5,6 +5,7 @@ import ac.mdiq.podcini.databinding.ComposeFragmentBinding
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.database.Episodes
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
@ -14,7 +15,6 @@ import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.ui.actions.DeleteActionButton
import ac.mdiq.podcini.ui.actions.SwipeAction
import ac.mdiq.podcini.ui.actions.SwipeActions
@ -240,7 +240,7 @@ import java.util.*
return // Refreshed anyway
}
// for (downloadUrl in event.urls) {
// val pos = EpisodeUtil.indexOfItemWithDownloadUrl(episodes.toList(), downloadUrl)
// val pos = Episodes.indexOfItemWithDownloadUrl(episodes.toList(), downloadUrl)
// if (pos >= 0) adapter.notifyItemChangedCompat(pos)
// }
}
@ -295,7 +295,7 @@ import java.util.*
val size: Int = event.episodes.size
while (i < size) {
val item: Episode = event.episodes[i++]
val pos = EpisodeUtil.indexOfItemWithId(episodes, item.id)
val pos = Episodes.indexOfItemWithId(episodes, item.id)
if (pos >= 0) {
episodes.removeAt(pos)
vms.removeAt(pos)
@ -317,7 +317,7 @@ import java.util.*
val size: Int = event.episodes.size
while (i < size) {
val item: Episode = event.episodes[i++]
val pos = EpisodeUtil.indexOfItemWithId(episodes, item.id)
val pos = Episodes.indexOfItemWithId(episodes, item.id)
if (pos >= 0) {
episodes.removeAt(pos)
vms.removeAt(pos)
@ -356,7 +356,7 @@ import java.util.*
} else {
val mediaUrls: MutableList<String> = ArrayList()
for (url in runningDownloads) {
if (EpisodeUtil.indexOfItemWithDownloadUrl(downloadedItems, url) != -1) continue
if (Episodes.indexOfItemWithDownloadUrl(downloadedItems, url) != -1) continue
mediaUrls.add(url)
}
val currentDownloads = getEpisdesWithUrl(mediaUrls).toMutableList()

View File

@ -33,6 +33,7 @@ import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.IntentUtils
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.MiscFormatter.formatDateTimeFlex
import ac.mdiq.podcini.util.MiscFormatter.fullDateTimeString
import android.content.Context
import android.os.Bundle
import android.speech.tts.TextToSpeech
@ -154,11 +155,17 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@Composable
fun MainView() {
val textColor = MaterialTheme.colorScheme.onSurface
var showEditComment by remember { mutableStateOf(false) }
val localTime = remember { System.currentTimeMillis() }
var editCommentText by remember { mutableStateOf(TextFieldValue((if (episode?.comment.isNullOrBlank()) "" else episode!!.comment + "\n") + fullDateTimeString(localTime) + ":\n")) }
var commentTextState by remember { mutableStateOf(TextFieldValue(episode?.comment?:"")) }
if (showEditComment) LargeTextEditingDialog(textState = commentTextState, onTextChange = { commentTextState = it }, onDismissRequest = {showEditComment = false},
if (showEditComment) LargeTextEditingDialog(textState = editCommentText, onTextChange = { editCommentText = it }, onDismissRequest = {showEditComment = false},
onSave = {
runOnIOScope { if (episode != null) episode = upsert(episode!!) { it.comment = commentTextState.text } }
runOnIOScope { if (episode != null) episode = upsert(episode!!) {
it.comment = editCommentText.text
it.commentTime = localTime
} }
})
var showChooseRatingDialog by remember { mutableStateOf(false) }

View File

@ -16,7 +16,7 @@ import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.fromCode
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.model.Rating
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.getPermutor
import ac.mdiq.podcini.ui.actions.SwipeAction
import ac.mdiq.podcini.ui.actions.SwipeActions
import ac.mdiq.podcini.ui.actions.SwipeActions.NoActionSwipeAction

View File

@ -13,7 +13,6 @@ import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.model.Feed.Companion.MAX_SYNTHETIC_ID
import ac.mdiq.podcini.storage.model.FeedFunding
import ac.mdiq.podcini.storage.model.Rating
import ac.mdiq.podcini.ui.activity.MainActivity
@ -24,6 +23,7 @@ import ac.mdiq.podcini.ui.compose.RemoveFeedDialog
import ac.mdiq.podcini.ui.fragment.StatisticsFragment.Companion.FeedStatisticsDialog
import ac.mdiq.podcini.ui.utils.TransitionEffect
import ac.mdiq.podcini.util.*
import ac.mdiq.podcini.util.MiscFormatter.fullDateTimeString
import android.R.string
import android.app.Activity
import android.content.*
@ -214,12 +214,17 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
fun DetailUI() {
val scrollState = rememberScrollState()
var showEditComment by remember { mutableStateOf(false) }
val localTime = remember { System.currentTimeMillis() }
var editCommentText by remember { mutableStateOf(TextFieldValue((if (feed.comment.isBlank()) "" else feed.comment + "\n") + fullDateTimeString(localTime) + ":\n")) }
var commentTextState by remember { mutableStateOf(TextFieldValue(feed.comment)) }
if (showEditComment) LargeTextEditingDialog(textState = commentTextState, onTextChange = { commentTextState = it }, onDismissRequest = {showEditComment = false},
if (showEditComment) LargeTextEditingDialog(textState = editCommentText, onTextChange = { editCommentText = it }, onDismissRequest = {showEditComment = false},
onSave = {
runOnIOScope {
feed = upsert(feed) { it.comment = commentTextState.text }
rating = feed.rating
feed = upsert(feed) {
it.comment = editCommentText.text
it.commentTime = localTime
}
rating = feed.rating
}
})
var showFeedStats by remember { mutableStateOf(false) }
@ -236,7 +241,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
modifier = Modifier.padding(start = 15.dp, top = 10.dp, bottom = 5.dp).clickable { showEditComment = true })
Text(commentTextState.text, color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(start = 15.dp, bottom = 10.dp))
if (feed.id > MAX_SYNTHETIC_ID) {
if (!feed.isSynthetic()) {
Text(stringResource(R.string.url_label), color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(top = 16.dp, bottom = 4.dp))
Text(text = txtvUrl ?: "", color = textColor, modifier = Modifier.clickable {
if (feed.downloadUrl != null) {

View File

@ -7,7 +7,7 @@ import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.getPermutor
import ac.mdiq.podcini.ui.dialog.ConfirmationDialog
import ac.mdiq.podcini.ui.dialog.DatesFilterDialog
import ac.mdiq.podcini.util.EventFlow

View File

@ -12,6 +12,7 @@ import ac.mdiq.podcini.playback.service.PlaybackService.Companion.mediaBrowser
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.database.Episodes
import ac.mdiq.podcini.storage.database.Queues.clearQueue
import ac.mdiq.podcini.storage.database.Queues.isQueueKeepSorted
import ac.mdiq.podcini.storage.database.Queues.moveInQueueSync
@ -21,9 +22,8 @@ import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.getPermutor
import ac.mdiq.podcini.storage.utils.DurationConverter
import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
import ac.mdiq.podcini.ui.actions.SwipeAction
import ac.mdiq.podcini.ui.actions.SwipeActions
import ac.mdiq.podcini.ui.actions.SwipeActions.NoActionSwipeAction
@ -356,7 +356,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
FlowEvent.QueueEvent.Action.REMOVED, FlowEvent.QueueEvent.Action.IRREVERSIBLE_REMOVED -> {
if (event.episodes.isNotEmpty()) {
for (e in event.episodes) {
val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems, e.id)
val pos: Int = Episodes.indexOfItemWithId(queueItems, e.id)
if (pos >= 0) {
Logd(TAG, "removing episode $pos ${queueItems[pos].title} $e")
// queueItems[pos].stopMonitoring.value = true
@ -390,7 +390,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
private fun onPlayEvent(event: FlowEvent.PlayEvent) {
val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems, event.episode.id)
val pos: Int = Episodes.indexOfItemWithId(queueItems, event.episode.id)
Logd(TAG, "onPlayEvent action: ${event.action} pos: $pos ${event.episode.title}")
if (pos >= 0) vms[pos].isPlayingState = event.isPlaying()
}
@ -400,7 +400,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
if (loadItemsRunning) return
for (url in event.urls) {
// if (!event.isCompleted(url)) continue
val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(queueItems.toList(), url)
val pos: Int = Episodes.indexOfItemWithDownloadUrl(queueItems.toList(), url)
if (pos >= 0) vms[pos].downloadState = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
}

View File

@ -4,12 +4,12 @@ import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.ComposeFragmentBinding
import ac.mdiq.podcini.net.download.DownloadStatus
import ac.mdiq.podcini.net.feed.searcher.CombinedSearcher
import ac.mdiq.podcini.storage.database.Episodes
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.model.Rating
import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.ui.actions.SwipeAction
import ac.mdiq.podcini.ui.actions.SwipeActions
import ac.mdiq.podcini.ui.actions.SwipeActions.NoActionSwipeAction
@ -230,7 +230,7 @@ class SearchFragment : Fragment() {
private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) {
for (url in event.urls) {
val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(results, url)
val pos: Int = Episodes.indexOfItemWithDownloadUrl(results, url)
if (pos >= 0) vms[pos].downloadState = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal
}
}

View File

@ -579,29 +579,6 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
selectMode = false
Logd(TAG, "ic_playback_speed: ${selected.size}")
showSpeedDialog = true
// val vBinding = PlaybackSpeedFeedSettingDialogBinding.inflate(activity.layoutInflater)
// vBinding.seekBar.setProgressChangedListener { speed: Float? ->
// vBinding.currentSpeedLabel.text = String.format(Locale.getDefault(), "%.2fx", speed)
// }
// vBinding.useGlobalCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
// vBinding.seekBar.isEnabled = !isChecked
// vBinding.seekBar.alpha = if (isChecked) 0.4f else 1f
// vBinding.currentSpeedLabel.alpha = if (isChecked) 0.4f else 1f
// }
// vBinding.seekBar.updateSpeed(1.0f)
// MaterialAlertDialogBuilder(activity)
// .setTitle(R.string.playback_speed)
// .setView(vBinding.root)
// .setPositiveButton("OK") { _: DialogInterface?, _: Int ->
// val newSpeed = if (vBinding.useGlobalCheckbox.isChecked) FeedPreferences.SPEED_USE_GLOBAL
// else vBinding.seekBar.currentSpeed
// saveFeedPreferences { it: FeedPreferences ->
// it.playSpeed = newSpeed
// }
// }
// .setNegativeButton(R.string.cancel_label, null)
// .show()
}) {
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playback_speed), "")
Text(stringResource(id = R.string.playback_speed)) } },
@ -1454,10 +1431,6 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
class PreferenceSwitchDialog(private var context: Context, private val title: String, private val text: String) {
private var onPreferenceChangedListener: OnPreferenceChangedListener? = null
interface OnPreferenceChangedListener {
/**
* Notified when user confirms preference
* @param enabled The preference
*/
fun preferenceChanged(enabled: Boolean)
}
fun openDialog() {

View File

@ -4,11 +4,12 @@ import android.content.Context
import android.text.format.DateUtils
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.*
/**
* Formats dates.
*/
object MiscFormatter {
@JvmStatic
fun formatRfc822Date(date: Date?): String {
@ -16,6 +17,19 @@ object MiscFormatter {
return format.format(date?: Date(0))
}
fun localDateTimeString(): String {
val currentTime = LocalDateTime.now()
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
return currentTime.format(formatter)
}
fun fullDateTimeString(time: Long): String {
val instant = Instant.ofEpochMilli(time)
val localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault())
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
return localDateTime.format(formatter)
}
@JvmStatic
fun formatAbbrev(context: Context?, date: Date?): String {
if (date == null) return ""

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/choose_data_folder_dialog"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/recyclerView" />
</LinearLayout>

View File

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/choose_data_folder_dialog_entry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:background="?attr/selectableItemBackground">
<RadioButton
android:id="@+id/radio_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:padding="4dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_toEndOf="@+id/radio_button"
android:layout_toRightOf="@+id/radio_button"
android:orientation="vertical">
<TextView
android:id="@+id/path"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="/storage/sdcard0" />
<TextView
android:id="@+id/size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="2 GB" />
<ProgressBar
android:id="@+id/used_space"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</RelativeLayout>

View File

@ -352,7 +352,7 @@
<string name="downloads_pref">تنزيلات</string>
<string name="downloads_pref_sum">الفترة الزمنية للتحديثات، بيانات الجوال، التنزيل التلقائي، الحذف التلقائي</string>
<string name="feed_refresh_title">حدث البودكاستات</string>
<string name="feed_refresh_sum">حدد الفترة الزمنية التي سيقوم Podcini فيها تلقائيا بالبحث عن حلقات جديدة </string>
<string name="feed_refresh_never">أبداً</string>
<string name="feed_every_hour">كل ساعة</string>
<string name="feed_every_2_hours">كل 2 ساعتين</string>

View File

@ -328,7 +328,7 @@
<string name="downloads_pref">Baixades</string>
<string name="downloads_pref_sum">Intèrval d\'actualització, dades mòbils, baixades automàtiques, esborrat automàtic</string>
<string name="feed_refresh_title">Actualitza els pòdcasts</string>
<string name="feed_refresh_sum">Especifiqueu un interval segons el qual Podcini cercarà nous episodis automàticament</string>
<string name="feed_refresh_never">Mai</string>
<string name="feed_every_hour">Cada hora</string>
<string name="feed_every_2_hours">Cada 2 hores</string>

View File

@ -357,7 +357,7 @@
<string name="downloads_pref">Stahování</string>
<string name="downloads_pref_sum">Interval aktualizace, Mobilní data, Automatické stahování, Automatické mazání</string>
<string name="feed_refresh_title">Aktualizovat podcasty</string>
<string name="feed_refresh_sum">Zadejte interval, ve kterém bude Podcini automaticky vyhledávat nové epizody</string>
<string name="feed_refresh_never">Nikdy</string>
<string name="feed_every_hour">Každou hodinu</string>
<string name="feed_every_2_hours">Každé 2 hodiny</string>

View File

@ -342,7 +342,7 @@
<string name="downloads_pref">Overførsler</string>
<string name="downloads_pref_sum">Opdateringsinterval, Mobildata, Automatisk overførsel, Automatisk sletning</string>
<string name="feed_refresh_title">Opdater podcasts</string>
<string name="feed_refresh_sum">Angiv et interval, hvormed Podcini automatisk søger efter nye afsnit.</string>
<string name="feed_refresh_never">Aldrig</string>
<string name="feed_every_hour">Hver time</string>
<string name="feed_every_2_hours">Hver 2. time</string>

View File

@ -346,7 +346,7 @@
<string name="downloads_pref">Downloads</string>
<string name="downloads_pref_sum">Aktualisierungsintervall, Mobile Daten, Automatischer Download, Automatisches Löschen</string>
<string name="feed_refresh_title">Podcasts aktualisieren</string>
<string name="feed_refresh_sum">Ein Intervall festlegen, in dem Podcini automatisch nach neuen Episoden sucht</string>
<string name="feed_refresh_never">Niemals</string>
<string name="feed_every_hour">Jede Stunde</string>
<string name="feed_every_2_hours">Alle 2 Stunden</string>

View File

@ -348,7 +348,7 @@
<string name="downloads_pref">Descargas</string>
<string name="downloads_pref_sum">Intervalo de actualización, datos móviles, descargas automáticas, borrado automático</string>
<string name="feed_refresh_title">Actualizar pódcasts</string>
<string name="feed_refresh_sum">Especifique un intervalo para buscar nuevos episodios automáticamente</string>
<string name="feed_refresh_never">Nunca</string>
<string name="feed_every_hour">Cada hora</string>
<string name="feed_every_2_hours">Cada 2 horas</string>

View File

@ -329,7 +329,7 @@
<string name="downloads_pref">بارگیری‌ها</string>
<string name="downloads_pref_sum">دورهٔ بارگیری، دادهٔ همراه، بارگیری خودکار، حذف خودکار</string>
<string name="feed_refresh_title">پادکست ها را بازخوانی کنید</string>
<string name="feed_refresh_sum">مشخّص کردن دوره‌ای برای گشتن خودکار آنتناپاد به دنبال قسمت‌های جدید</string>
<string name="feed_refresh_never">هرگز</string>
<string name="feed_every_hour">هر ساعت</string>
<string name="feed_every_2_hours">هر ۲ ساعت</string>

View File

@ -319,7 +319,7 @@
<string name="playback_pref_sum">Kuulokkeiden ohjaimet, ohitusaikavälit, jono</string>
<string name="downloads_pref">Lataukset</string>
<string name="feed_refresh_title">Päivitä podcastit</string>
<string name="feed_refresh_sum">Määritä aikaväli tai tietty aika etsiä uusia jaksoja automaattisesti</string>
<string name="feed_refresh_never">Ei koskaan</string>
<string name="feed_every_hour">Tunnin välein</string>
<string name="feed_every_2_hours">2 tunnin välein</string>

View File

@ -350,7 +350,7 @@
<string name="downloads_pref">Téléchargements</string>
<string name="downloads_pref_sum">Fréquence de mise à jour, utilisation de la connexion mobile, téléchargements et suppressions automatiques</string>
<string name="feed_refresh_title">Actualiser les podcasts</string>
<string name="feed_refresh_sum">Choisir la fréquence de mise à jour automatique des épisodes par Podcini</string>
<string name="feed_refresh_never">Jamais</string>
<string name="feed_every_hour">Toutes les heures</string>
<string name="feed_every_2_hours">Toutes les 2 heures</string>

View File

@ -341,7 +341,7 @@
<string name="downloads_pref">Descargas</string>
<string name="downloads_pref_sum">Intervalo de actualización, Datos móbiles, Descarga automática, Eliminación automática</string>
<string name="feed_refresh_title">Actualizar podcast</string>
<string name="feed_refresh_sum">Indicar cada canto tempo debe Podcini comprobar se hai episodios novos</string>
<string name="feed_refresh_never">Nunca</string>
<string name="feed_every_hour">Cada hora</string>
<string name="feed_every_2_hours">Cada 2 horas</string>

View File

@ -266,7 +266,7 @@
<string name="playback_pref_sum">Kontrol headphone, Jangka waktu lewati, Antrean</string>
<string name="downloads_pref_sum">Waktu pembaharuan, Jaringan seluler, Unduh otomatis, Hapus otomatis</string>
<string name="feed_refresh_title">Segarkan podcast</string>
<string name="feed_refresh_sum">Tentukan jangka waktu di mana Podcini secara otomatis mencari episode baru</string>
<string name="feed_refresh_never">Jangan pernah</string>
<string name="feed_every_hour">Setiap jam</string>
<string name="feed_every_2_hours">Tiap 2 jam</string>

View File

@ -350,7 +350,7 @@
<string name="downloads_pref">Download</string>
<string name="downloads_pref_sum">Intervallo di aggiornamento, rete mobile, download ed eliminazione automatici</string>
<string name="feed_refresh_title">Aggiorna i podcast</string>
<string name="feed_refresh_sum">Definisce l\'intervallo di ricerca automatica dei nuovi episodi.</string>
<string name="feed_refresh_never">Mai</string>
<string name="feed_every_hour">Ogni ora</string>
<string name="feed_every_2_hours">Ogni 2 ore</string>

View File

@ -353,7 +353,7 @@
<string name="downloads_pref">הורדות</string>
<string name="downloads_pref_sum">הפרש בין עדכונים, חיבור סלולרי, הורדה אוטומטית, מחיקה אוטומטית</string>
<string name="feed_refresh_title">רענון פודקאסטים</string>
<string name="feed_refresh_sum">נא לציין הפרש או מועד מסוים לחיפוש פרקים אוטומטית על ידי אנטנה־פּוֹד</string>
<string name="feed_refresh_never">אף פעם לא</string>
<string name="feed_every_hour">כל שעה</string>
<string name="feed_every_2_hours">כל שעתיים</string>

View File

@ -317,7 +317,7 @@
<string name="downloads_pref">다운로드</string>
<string name="downloads_pref_sum">업데이트 주기, 휴대전화 망 데이터, 자동 다운로드, 자동 삭제</string>
<string name="feed_refresh_title">팟캐스트 새로고침</string>
<string name="feed_refresh_sum">새로운 에피소드가 있는지 자동으로 확인할 주기를 지정합니다</string>
<string name="feed_refresh_never">안 함</string>
<string name="feed_every_hour">매 시간</string>
<string name="feed_every_2_hours">매 2시간</string>

View File

@ -321,7 +321,7 @@
<string name="downloads_pref">Nedlastinger</string>
<string name="downloads_pref_sum">Oppdaterings-intervall, Mobil data, Automatisk nedlasting, Automatisk sletting</string>
<string name="feed_refresh_title">Oppdater podkaster</string>
<string name="feed_refresh_sum">Spesifiser en intervall eller et spesifikt tidspunkt når det skal sjekkes automatisk for nye episoder</string>
<string name="feed_refresh_never">Aldri</string>
<string name="feed_every_hour">Hver time</string>
<string name="feed_every_2_hours">Hver andre time</string>

View File

@ -330,7 +330,7 @@
<string name="downloads_pref">Downloads</string>
<string name="downloads_pref_sum">Intervalo de atualização, Dados móveis, Download automático, Exclusão automática</string>
<string name="feed_refresh_title">Atualizar podcasts</string>
<string name="feed_refresh_sum">Especifique um intervalo em que o Podcini procura novos episódios automaticamente</string>
<string name="feed_refresh_never">Nunca</string>
<string name="feed_every_hour">A cada hora</string>
<string name="feed_every_2_hours">A cada 2 horas</string>

View File

@ -348,7 +348,7 @@
<string name="downloads_pref">Descargas</string>
<string name="downloads_pref_sum">Intervalo de atualização, dados móveis, descargas e eliminação automática</string>
<string name="feed_refresh_title">Recarregar podcasts</string>
<string name="feed_refresh_sum">Defina um intervalo para que Podcini procure episódios automaticamente</string>
<string name="feed_refresh_never">Nunca</string>
<string name="feed_every_hour">A cada 1 hora</string>
<string name="feed_every_2_hours">A cada 2 horas</string>

View File

@ -347,7 +347,7 @@
<string name="downloads_pref">Descărcări</string>
<string name="downloads_pref_sum">Interval actualizare, Date mobile, Descărcări automate, Ștergeri automate</string>
<string name="feed_refresh_title">Reîmprospătează podcasturile</string>
<string name="feed_refresh_sum">Specifică un anumit interval în care Podcini caută episoade noi automat</string>
<string name="feed_refresh_never">Nicio data</string>
<string name="feed_every_hour">La fiecare oră</string>
<string name="feed_every_2_hours">La fiecare 2 ore</string>

View File

@ -333,7 +333,7 @@
<string name="downloads_pref">Загрузки</string>
<string name="downloads_pref_sum">Интервал обновления, мобильная сеть, автоматизация</string>
<string name="feed_refresh_title">Обновить подкасты</string>
<string name="feed_refresh_sum">Укажите интервал с которым Podcini будет автоматически искать обновления выпусков</string>
<string name="feed_refresh_never">Никогда</string>
<string name="feed_every_hour">Каждый час</string>
<string name="feed_every_2_hours">Каждые 2 часа</string>

View File

@ -352,7 +352,7 @@
<string name="downloads_pref">Sťahovanie</string>
<string name="downloads_pref_sum">Interval aktualizácie, Mobilné dáta, Automatické sťahovanie, Automatické mazanie</string>
<string name="feed_refresh_title">Aktualizovať podcasty</string>
<string name="feed_refresh_sum">Zvoliť interval alebo čas v ktorom Podcini automaticky hľadá nové epizódy</string>
<string name="feed_refresh_never">Nikdy</string>
<string name="feed_every_hour">Každú hodinu</string>
<string name="feed_every_2_hours">Každé 2 hodiny</string>

View File

@ -341,7 +341,7 @@
<string name="downloads_pref">Nedladdningar</string>
<string name="downloads_pref_sum">Uppdateringsintervall, Mobildata, Automatisk nedladdning, Automatisk radering</string>
<string name="feed_refresh_title">Uppdatera podcasts</string>
<string name="feed_refresh_sum">Ange ett intervall med vilket Podcini letar efter nya episoder automatiskt</string>
<string name="feed_refresh_never">Aldrig</string>
<string name="feed_every_hour">Varje timme</string>
<string name="feed_every_2_hours">Varje 2 timmar</string>

View File

@ -328,7 +328,7 @@
<string name="downloads_pref">İndirilenler</string>
<string name="downloads_pref_sum">Güncelleme aralığı, Mobil veri, Otomatik indirme, Otomatik silme</string>
<string name="feed_refresh_title">Podcastleri güncelle</string>
<string name="feed_refresh_sum">Podcini\'un yeni bölümleri otomatik olarak arayacağı zaman aralığını belirtin</string>
<string name="feed_refresh_never">Hiçbir zaman</string>
<string name="feed_every_hour">Saatte bir</string>
<string name="feed_every_2_hours">2 saatte bir</string>

View File

@ -353,7 +353,7 @@
<string name="downloads_pref">Завантаження</string>
<string name="downloads_pref_sum">Інтервал оновлення, Мобільні дані, Автоматичне завантаження, Автоматичне видалення</string>
<string name="feed_refresh_title">Оновити подкасти</string>
<string name="feed_refresh_sum">Вкажіть інтервал, з яким Podcini автоматично шукатиме нові епізоди</string>
<string name="feed_refresh_never">Ніколи</string>
<string name="feed_every_hour">Щогодини</string>
<string name="feed_every_2_hours">Кожні 2 години</string>

View File

@ -341,7 +341,7 @@
<string name="downloads_pref">下载</string>
<string name="downloads_pref_sum">更新间隔、移动数据、自动下载、自动删除</string>
<string name="feed_refresh_title">刷新播客</string>
<string name="feed_refresh_sum">指定 Podcini 自动查找新节目的间隔</string>
<string name="feed_refresh_never">永不</string>
<string name="feed_every_hour">每 1 小时</string>
<string name="feed_every_2_hours">每 2 小时</string>

View File

@ -1,25 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="feed_refresh_interval_entries">
<item>@string/feed_refresh_never</item>
<item>@string/feed_every_hour</item>
<item>@string/feed_every_2_hours</item>
<item>@string/feed_every_4_hours</item>
<item>@string/feed_every_8_hours</item>
<item>@string/feed_every_12_hours</item>
<item>@string/feed_every_24_hours</item>
<item>@string/feed_every_72_hours</item>
</string-array>
<string-array name="feed_refresh_interval_values">
<item>0</item>
<item>1</item>
<item>2</item>
<item>4</item>
<item>8</item>
<item>12</item>
<item>24</item>
<item>72</item>
</string-array>
<!-- <string-array name="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>-->
@ -59,26 +59,25 @@
<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_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>
<item>sync</item>
</string-array>
<string-array name="episode_cleanup_entries">

View File

@ -388,6 +388,7 @@
<string name="view_count">View count</string>
<string name="date">Date</string>
<string name="count">Count</string>
<string name="last_comment_date">Comment date</string>
<string name="last_played_date">Played date</string>
<string name="completed_date">Completed date</string>
<string name="duration">Duration</string>
@ -478,7 +479,7 @@
<string name="downloads_pref">Downloads</string>
<string name="downloads_pref_sum">Update interval, Mobile data, Automatic download, Automatic deletion</string>
<string name="feed_refresh_title">Refresh podcasts</string>
<string name="feed_refresh_sum">Specify an interval at which Podcini looks for new episodes automatically</string>
<string name="feed_refresh_sum">Specify an interval (in hours, 0 means never) at which Podcini looks for new episodes automatically.</string>
<string name="feed_refresh_never">Never</string>
<string name="feed_every_hour">Every hour</string>
<string name="feed_every_2_hours">Every 2 hours</string>

View File

@ -1,78 +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/res-auto">
<com.bytehamster.lib.preferencesearch.SearchPreference
android:key="searchPreference"
search:textHint="@string/preference_search_hint"
search:textNoResults="@string/preference_search_no_results"
search:textClearHistory="@string/preference_search_clear_history" />
<Preference
android:key="prefScreenInterface"
android:title="@string/user_interface_label"
android:summary="@string/user_interface_sum"
android:icon="@drawable/ic_appearance" />
<Preference
android:key="prefScreenPlayback"
android:title="@string/playback_pref"
android:summary="@string/playback_pref_sum"
android:icon="@drawable/ic_play_24dp" />
<Preference
android:key="prefScreenDownloads"
android:title="@string/downloads_pref"
android:summary="@string/downloads_pref_sum"
android:icon="@drawable/ic_download" />
<Preference
android:key="prefScreenSynchronization"
android:title="@string/synchronization_pref"
android:summary="@string/synchronization_sum"
android:icon="@drawable/ic_cloud" />
<Preference
android:key="prefScreenImportExport"
android:title="@string/import_export_pref"
android:summary="@string/import_export_summary"
android:icon="@drawable/ic_storage" />
<Preference
android:key="notifications"
android:title="@string/notification_pref_fragment"
android:icon="@drawable/ic_notifications"/>
<SwitchPreferenceCompat
android:defaultValue="true"
android:enabled="true"
android:key="prefOPMLBackup"
android:summary="@string/pref_backup_on_google_sum"
android:title="@string/pref_backup_on_google_title"/>
<PreferenceCategory
android:key="project"
android:title="@string/project_pref">
<Preference
android:key="prefDocumentation"
android:title="@string/documentation_support"
android:icon="@drawable/ic_questionmark" />
<Preference
android:key="prefViewForum"
android:title="@string/visit_user_forum"
android:icon="@drawable/ic_chat" />
<Preference
android:key="prefContribute"
android:title="@string/pref_contribute"
android:icon="@drawable/ic_contribute" />
<Preference
android:key="prefSendBugReport"
android:title="@string/bug_report_title"
android:icon="@drawable/ic_bug" />
<Preference
android:key="prefAbout"
android:title="@string/about_pref"
android:icon="@drawable/ic_info" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -1,62 +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">
<!-- <Preference-->
<!-- android:title="@string/choose_data_directory"-->
<!-- android:key="prefChooseDataDir"/>-->
<PreferenceCategory android:title="@string/automation">
<ac.mdiq.podcini.preferences.MaterialListPreference
android:entryValues="@array/feed_refresh_interval_values"
android:entries="@array/feed_refresh_interval_entries"
android:key="prefAutoUpdateIntervall"
android:title="@string/feed_refresh_title"
android:summary="@string/feed_refresh_sum"
android:defaultValue="12"/>
<Preference
android:summary="@string/pref_automatic_download_sum"
android:key="prefAutoDownloadSettings"
android:title="@string/pref_automatic_download_title"
search:ignore="true" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:enabled="true"
android:key="prefAutoDelete"
android:summary="@string/pref_auto_delete_sum"
android:title="@string/pref_auto_delete_title"/>
<SwitchPreferenceCompat
android:defaultValue="false"
android:enabled="true"
android:key="prefAutoDeleteLocal"
android:summary="@string/pref_auto_local_delete_sum"
android:title="@string/pref_auto_local_delete_title"/>
<SwitchPreferenceCompat
android:defaultValue="true"
android:enabled="true"
android:key="prefFavoriteKeepsEpisode"
android:summary="@string/pref_keeps_important_episodes_sum"
android:title="@string/pref_keeps_important_episodes_title"/>
<SwitchPreferenceCompat
android:defaultValue="true"
android:enabled="true"
android:key="prefDeleteRemovesFromQueue"
android:summary="@string/pref_delete_removes_from_queue_sum"
android:title="@string/pref_delete_removes_from_queue_title"/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/download_pref_details">
<ac.mdiq.podcini.preferences.MaterialMultiSelectListPreference
android:defaultValue="@array/mobile_update_default_value"
android:entries="@array/mobile_update_entries"
android:entryValues="@array/mobile_update_values"
android:key="prefMobileUpdateTypes"
android:summary="@string/pref_mobileUpdate_sum"
android:title="@string/pref_metered_network_title"/>
<Preference
android:key="prefProxy"
android:summary="@string/pref_proxy_sum"
android:title="@string/pref_proxy_title"/>
</PreferenceCategory>
</PreferenceScreen>

View File

@ -1,3 +1,14 @@
# 6.14.7
* corrected some deeplinks in manifest file on OPMLActivity
* added commentTime in Episode and Feed to record the time of comment/opinion added
* when adding/editing a comment/opinion, a time stamp is automatically added in the text field
* when removing a feed or an episode, a time stamp is automatically added in the text field
* a new sorting item on episodes based on commentTime
* in episodes list, if an episode belongs to a synthetic feed, tapping on the image will bring up EpisodeInfo instead of FeedInfo
* some class restructuring
* more preferences fragments are in Compose
# 6.14.6
* fixed issue of unable to input in rename feed