diff --git a/app/build.gradle b/app/build.gradle index b489bec9..fbd2d28f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 = "" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8e52ff4d..51a74434 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -148,28 +148,6 @@ android:windowSoftInputMode="stateAlwaysHidden" android:launchMode="singleTask" android:exported="true"> - - - - - - - - - - - - - - - - - - - - - - @@ -233,9 +211,9 @@ - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -326,44 +276,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SyncService.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SyncService.kt index bfb432b5..e581f7c7 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SyncService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SyncService.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt index 9f5a2670..ad178d86 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/LocalMediaPlayer.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/LocalMediaPlayer.kt index c1862f93..9e616cac 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/LocalMediaPlayer.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/LocalMediaPlayer.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt index 5f66fd19..62c85261 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/ExportWriter.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/ExportWriter.kt index ac9d5312..3c31ed78 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/ExportWriter.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/ExportWriter.kt @@ -7,7 +7,7 @@ import java.io.Writer interface ExportWriter { @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) - fun writeDocument(feeds: List?, writer: Writer?, context: Context) + fun writeDocument(feeds: List, writer: Writer?, context: Context) fun fileExtension(): String? } \ No newline at end of file diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/OpmlTransporter.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/OpmlTransporter.kt index 4f37c9da..14eca8be 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/OpmlTransporter.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/OpmlTransporter.kt @@ -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?, writer: Writer?, context: Context) { + override fun writeDocument(feeds: List, 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() +// } + } +// } + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/AutoDownloadPreferencesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/AutoDownloadPreferencesFragment.kt index 410abbef..bc478ebf 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/AutoDownloadPreferencesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/AutoDownloadPreferencesFragment.kt @@ -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 { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/DownloadsPreferencesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/DownloadsPreferencesFragment.kt index a0257c3d..4cd5d25d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/DownloadsPreferencesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/DownloadsPreferencesFragment.kt @@ -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(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(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(Prefs.prefProxy.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - val dialog = ProxyDialog(requireContext()) - dialog.show() - true - } -// findPreference(PREF_CHOOSE_DATA_DIR)?.onPreferenceClickListener = Preference.OnPreferenceClickListener { -// ChooseDataFolderDialog.showDialog(requireContext()) { path: String? -> -// setDataFolder(path!!) -//// setDataFolderText() -// } -// true -// } - findPreference(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(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(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) { - 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 = 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) : RecyclerView.Adapter() { - private val selectionHandler: Consumer - private val currentPath: String? - private val entries: List - 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 { - val mediaDirs = context.getExternalFilesDirs(null) - val entries: MutableList = 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, } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt index e56a73e2..0476531d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt @@ -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(BackupDatabase()) { uri: Uri? -> this.backupDatabaseResult(uri) } private val chooseOpmlImportPathLauncher = registerForActivityResult(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?, writer: Writer?, context: Context) { + override fun writeDocument(feeds: List, writer: Writer?, context: Context) { Logd(TAG, "Starting to write document") val queuedEpisodeActions: MutableList = 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?, writer: Writer?, context: Context) { + override fun writeDocument(feeds: List, 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?, writer: Writer?, context: Context) { + override fun writeDocument(feeds: List, 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("
  • ") writer.append(feed.title) writer.append(" { - findPreference(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(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(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(Prefs.prefScreenInterface.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - (activity as PreferenceActivity).openScreen(R.xml.preferences_user_interface) - true - } - findPreference(Prefs.prefScreenPlayback.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - (activity as PreferenceActivity).openScreen(R.xml.preferences_playback) - true - } - findPreference(Prefs.prefScreenDownloads.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - (activity as PreferenceActivity).openScreen(R.xml.preferences_downloads) - true - } - findPreference(Prefs.prefScreenSynchronization.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - (activity as PreferenceActivity).openScreen(R.xml.preferences_synchronization) - true - } - findPreference(Prefs.prefScreenImportExport.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - (activity as PreferenceActivity).openScreen(Screens.preferences_import_export) - true - } - findPreference(Prefs.notifications.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - (activity as PreferenceActivity).openScreen(R.xml.preferences_notifications) - true - } - val switchPreference = findPreference("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(Prefs.prefAbout.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - parentFragmentManager.beginTransaction().replace(R.id.settingsContainer, AboutFragment()).addToBackStack(getString(R.string.about_pref)).commit() - true - } - findPreference(Prefs.prefDocumentation.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini") - true - } - findPreference(Prefs.prefViewForum.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini/discussions") - true - } - findPreference(Prefs.prefContribute.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini") - true - } - findPreference(Prefs.prefSendBugReport.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - startActivity(Intent(activity, BugReportActivity::class.java)) - true - } - } - - private fun setupSearch() { - val searchPreference = findPreference("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, - } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt index 5173d418..800b3298 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt @@ -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) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt index a5505c48..dd5bd554 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt @@ -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, 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, itemId: Long): Boolean { + return indexOfItemWithId(episodes, itemId) >= 0 + } + + @JvmStatic + fun indexOfItemWithDownloadUrl(items: List, 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 + } } \ No newline at end of file diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt index 63ac2215..070463fb 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt @@ -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() - 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): 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): Boolean { @@ -362,9 +348,7 @@ object Queues { private fun getCurrentlyPlayingPosition(queueItems: List, 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 } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt index 50ad7f72..590bbd92 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Episode.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Episode.kt index 43c84fa8..b5c94e3b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Episode.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Episode.kt @@ -92,6 +92,8 @@ class Episode : RealmObject { @FullText var comment: String = "" + var commentTime: Long = 0L + @Ignore val isNew: Boolean get() = playState == PlayState.NEW.code diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeSortOrder.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeSortOrder.kt index bfce83dd..e98f8d9b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeSortOrder.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeSortOrder.kt @@ -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 { + var comparator: java.util.Comparator? = null + var permutor: Permutor? = 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 { + override fun reorder(queue: MutableList?) { + if (!queue.isNullOrEmpty()) queue.shuffle() + } + } + SMART_SHUFFLE_OLD_NEW -> permutor = object : Permutor { + override fun reorder(queue: MutableList?) { + if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList, true) + } + } + SMART_SHUFFLE_NEW_OLD -> permutor = object : Permutor { + override fun reorder(queue: MutableList?) { + if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList, 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 = comparator + permutor = object : Permutor { + override fun reorder(queue: MutableList?) {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, ascending: Boolean) { + // Divide FeedItems into lists by feed + val map: MutableMap> = 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 = + 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> = ArrayList() + for ((_, value) in map) { + value.sortWith(itemComparator) + feeds.add(value) + } + + val emptySlots = ArrayList() + 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, f2: List -> 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 the type of elements in the list + */ + interface Permutor { + /** + * Reorders the specified list. + * @param queue A (modifiable) list of elements to be reordered + */ + fun reorder(queue: MutableList?) + } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt index 3f3659d7..dc5a17bb 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt @@ -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 { 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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodeUtil.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodeUtil.kt deleted file mode 100644 index f475ff47..00000000 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodeUtil.kt +++ /dev/null @@ -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, 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, itemId: Long): Boolean { - return indexOfItemWithId(episodes, itemId) >= 0 - } - - @JvmStatic - fun indexOfItemWithDownloadUrl(items: List, 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 - } -} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodesPermutors.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodesPermutors.kt deleted file mode 100644 index c3227df5..00000000 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodesPermutors.kt +++ /dev/null @@ -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 { - var comparator: Comparator? = null - var permutor: Permutor? = 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 { - override fun reorder(queue: MutableList?) { - if (!queue.isNullOrEmpty()) queue.shuffle() - } - } - EpisodeSortOrder.SMART_SHUFFLE_OLD_NEW -> permutor = object : Permutor { - override fun reorder(queue: MutableList?) { - if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList, true) - } - } - EpisodeSortOrder.SMART_SHUFFLE_NEW_OLD -> permutor = object : Permutor { - override fun reorder(queue: MutableList?) { - if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList, 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 = comparator - permutor = object : Permutor { - override fun reorder(queue: MutableList?) {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, ascending: Boolean) { - // Divide FeedItems into lists by feed - val map: MutableMap> = 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 = - 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> = ArrayList() - for ((_, value) in map) { - value.sortWith(itemComparator) - feeds.add(value) - } - - val emptySlots = ArrayList() - 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, f2: List -> 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 the type of elements in the list - */ - interface Permutor { - /** - * Reorders the specified list. - * @param queue A (modifiable) list of elements to be reordered - */ - fun reorder(queue: MutableList?) - } -} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/SwipeActions.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/SwipeActions.kt index 3b53bd1b..bfe0012c 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/SwipeActions.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/SwipeActions.kt @@ -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_) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/OpmlImportActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/OpmlImportActivity.kt index 43d5c6e6..2a0662b2 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/OpmlImportActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/OpmlImportActivity.kt @@ -54,11 +54,7 @@ class OpmlImportActivity : AppCompatActivity() { private val titleList: List get() { val result: MutableList = 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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt index 9e1dd5f2..0a2ca131 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Composables.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Composables.kt index fbbe8b7e..c909307e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Composables.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Composables.kt @@ -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) } } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt index 1513c3fe..8adcb2b0 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt @@ -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, 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, 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, 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, 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, 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(), diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Feeds.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Feeds.kt index 8fc75c5a..43ae0782 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Feeds.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Feeds.kt @@ -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, 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, 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 } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/BaseEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/BaseEpisodesFragment.kt index f4f08dab..f3713e31 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/BaseEpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/BaseEpisodesFragment.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt index 33dd149b..b55d8d1a 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt @@ -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 = 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() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt index 92e64f57..d3434d8b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt @@ -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) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt index a5d5e350..22337d42 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt index 8f6ce6bf..1ae6701e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt @@ -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) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt index ccf5ee38..50c102ac 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt index ee874be9..1aa6ce0d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt @@ -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 } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt index d2ba8110..6f64e705 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt @@ -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 } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt index f7c88ecc..6e4af6af 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt @@ -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() { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/util/MiscFormatter.kt b/app/src/main/kotlin/ac/mdiq/podcini/util/MiscFormatter.kt index c94abc91..a03a3647 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/util/MiscFormatter.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/util/MiscFormatter.kt @@ -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 "" diff --git a/app/src/main/res/layout/choose_data_folder_dialog.xml b/app/src/main/res/layout/choose_data_folder_dialog.xml deleted file mode 100644 index 15f372c2..00000000 --- a/app/src/main/res/layout/choose_data_folder_dialog.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/choose_data_folder_dialog_entry.xml b/app/src/main/res/layout/choose_data_folder_dialog_entry.xml deleted file mode 100644 index fe1c003c..00000000 --- a/app/src/main/res/layout/choose_data_folder_dialog_entry.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 6c3d9f8d..e67fbbff 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -352,7 +352,7 @@ تنزيلات الفترة الزمنية للتحديثات، بيانات الجوال، التنزيل التلقائي، الحذف التلقائي حدث البودكاستات - حدد الفترة الزمنية التي سيقوم Podcini فيها تلقائيا بالبحث عن حلقات جديدة + أبداً كل ساعة كل 2 ساعتين diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 47ae82ce..598f61cc 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -328,7 +328,7 @@ Baixades Intèrval d\'actualització, dades mòbils, baixades automàtiques, esborrat automàtic Actualitza els pòdcasts - Especifiqueu un interval segons el qual Podcini cercarà nous episodis automàticament + Mai Cada hora Cada 2 hores diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index cd285c40..63a9f391 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -357,7 +357,7 @@ Stahování Interval aktualizace, Mobilní data, Automatické stahování, Automatické mazání Aktualizovat podcasty - Zadejte interval, ve kterém bude Podcini automaticky vyhledávat nové epizody + Nikdy Každou hodinu Každé 2 hodiny diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index f96fac9b..6251e759 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -342,7 +342,7 @@ Overførsler Opdateringsinterval, Mobildata, Automatisk overførsel, Automatisk sletning Opdater podcasts - Angiv et interval, hvormed Podcini automatisk søger efter nye afsnit. + Aldrig Hver time Hver 2. time diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 43a6d999..a624b75f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -346,7 +346,7 @@ Downloads Aktualisierungsintervall, Mobile Daten, Automatischer Download, Automatisches Löschen Podcasts aktualisieren - Ein Intervall festlegen, in dem Podcini automatisch nach neuen Episoden sucht + Niemals Jede Stunde Alle 2 Stunden diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index cc36c3a6..59512322 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -348,7 +348,7 @@ Descargas Intervalo de actualización, datos móviles, descargas automáticas, borrado automático Actualizar pódcasts - Especifique un intervalo para buscar nuevos episodios automáticamente + Nunca Cada hora Cada 2 horas diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 2344e44b..b61a6ac1 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -329,7 +329,7 @@ بارگیری‌ها دورهٔ بارگیری، دادهٔ همراه، بارگیری خودکار، حذف خودکار پادکست ها را بازخوانی کنید - مشخّص کردن دوره‌ای برای گشتن خودکار آنتناپاد به دنبال قسمت‌های جدید + هرگز هر ساعت هر ۲ ساعت diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 2b5d7472..fae77d6f 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -319,7 +319,7 @@ Kuulokkeiden ohjaimet, ohitusaikavälit, jono Lataukset Päivitä podcastit - Määritä aikaväli tai tietty aika etsiä uusia jaksoja automaattisesti + Ei koskaan Tunnin välein 2 tunnin välein diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 826374f2..c9907082 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -350,7 +350,7 @@ Téléchargements Fréquence de mise à jour, utilisation de la connexion mobile, téléchargements et suppressions automatiques Actualiser les podcasts - Choisir la fréquence de mise à jour automatique des épisodes par Podcini + Jamais Toutes les heures Toutes les 2 heures diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 1ef2a02e..c2c3d303 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -341,7 +341,7 @@ Descargas Intervalo de actualización, Datos móbiles, Descarga automática, Eliminación automática Actualizar podcast - Indicar cada canto tempo debe Podcini comprobar se hai episodios novos + Nunca Cada hora Cada 2 horas diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 4f9d2c6a..1a68d403 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -266,7 +266,7 @@ Kontrol headphone, Jangka waktu lewati, Antrean Waktu pembaharuan, Jaringan seluler, Unduh otomatis, Hapus otomatis Segarkan podcast - Tentukan jangka waktu di mana Podcini secara otomatis mencari episode baru + Jangan pernah Setiap jam Tiap 2 jam diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index cd861c67..6473e2ce 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -350,7 +350,7 @@ Download Intervallo di aggiornamento, rete mobile, download ed eliminazione automatici Aggiorna i podcast - Definisce l\'intervallo di ricerca automatica dei nuovi episodi. + Mai Ogni ora Ogni 2 ore diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 51bbd190..659245f7 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -353,7 +353,7 @@ הורדות הפרש בין עדכונים, חיבור סלולרי, הורדה אוטומטית, מחיקה אוטומטית רענון פודקאסטים - נא לציין הפרש או מועד מסוים לחיפוש פרקים אוטומטית על ידי אנטנה־פּוֹד + אף פעם לא כל שעה כל שעתיים diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 10cfaa2f..e0889bde 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -317,7 +317,7 @@ 다운로드 업데이트 주기, 휴대전화 망 데이터, 자동 다운로드, 자동 삭제 팟캐스트 새로고침 - 새로운 에피소드가 있는지 자동으로 확인할 주기를 지정합니다 + 안 함 매 시간 매 2시간 diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 0f19f1c1..0bc35247 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -321,7 +321,7 @@ Nedlastinger Oppdaterings-intervall, Mobil data, Automatisk nedlasting, Automatisk sletting Oppdater podkaster - Spesifiser en intervall eller et spesifikt tidspunkt når det skal sjekkes automatisk for nye episoder + Aldri Hver time Hver andre time diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 9f130680..47790dff 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -330,7 +330,7 @@ Downloads Intervalo de atualização, Dados móveis, Download automático, Exclusão automática Atualizar podcasts - Especifique um intervalo em que o Podcini procura novos episódios automaticamente + Nunca A cada hora A cada 2 horas diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 8db3a89a..86d7fc7e 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -348,7 +348,7 @@ Descargas Intervalo de atualização, dados móveis, descargas e eliminação automática Recarregar podcasts - Defina um intervalo para que Podcini procure episódios automaticamente + Nunca A cada 1 hora A cada 2 horas diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index ed0a0ac3..a89a9543 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -347,7 +347,7 @@ Descărcări Interval actualizare, Date mobile, Descărcări automate, Ștergeri automate Reîmprospătează podcasturile - Specifică un anumit interval în care Podcini caută episoade noi automat + Nicio data La fiecare oră La fiecare 2 ore diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 71ddd9ea..e4fd113e 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -333,7 +333,7 @@ Загрузки Интервал обновления, мобильная сеть, автоматизация Обновить подкасты - Укажите интервал с которым Podcini будет автоматически искать обновления выпусков + Никогда Каждый час Каждые 2 часа diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index ad8e0c56..3d614dd2 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -352,7 +352,7 @@ Sťahovanie Interval aktualizácie, Mobilné dáta, Automatické sťahovanie, Automatické mazanie Aktualizovať podcasty - Zvoliť interval alebo čas v ktorom Podcini automaticky hľadá nové epizódy + Nikdy Každú hodinu Každé 2 hodiny diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 8b68b82e..918b2ff3 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -341,7 +341,7 @@ Nedladdningar Uppdateringsintervall, Mobildata, Automatisk nedladdning, Automatisk radering Uppdatera podcasts - Ange ett intervall med vilket Podcini letar efter nya episoder automatiskt + Aldrig Varje timme Varje 2 timmar diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 2b457e18..af776e49 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -328,7 +328,7 @@ İndirilenler Güncelleme aralığı, Mobil veri, Otomatik indirme, Otomatik silme Podcastleri güncelle - Podcini\'un yeni bölümleri otomatik olarak arayacağı zaman aralığını belirtin + Hiçbir zaman Saatte bir 2 saatte bir diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index f1997092..e252fc5e 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -353,7 +353,7 @@ Завантаження Інтервал оновлення, Мобільні дані, Автоматичне завантаження, Автоматичне видалення Оновити подкасти - Вкажіть інтервал, з яким Podcini автоматично шукатиме нові епізоди + Ніколи Щогодини Кожні 2 години diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index ea7e533b..b80ea450 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -341,7 +341,7 @@ 下载 更新间隔、移动数据、自动下载、自动删除 刷新播客 - 指定 Podcini 自动查找新节目的间隔 + 永不 每 1 小时 每 2 小时 diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 7f60e8d9..8511f856 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -1,25 +1,25 @@ - - @string/feed_refresh_never - @string/feed_every_hour - @string/feed_every_2_hours - @string/feed_every_4_hours - @string/feed_every_8_hours - @string/feed_every_12_hours - @string/feed_every_24_hours - @string/feed_every_72_hours - - - 0 - 1 - 2 - 4 - 8 - 12 - 24 - 72 - + + + + + + + + + + + + + + + + + + + + @@ -59,26 +59,25 @@ -1 - - @string/pref_mobileUpdate_refresh - @string/pref_mobileUpdate_episode_download - @string/pref_mobileUpdate_auto_download - @string/pref_mobileUpdate_streaming - @string/pref_mobileUpdate_images - @string/synchronization_pref - - - feed_refresh - episode_download - auto_download - streaming - images - sync - + + + + + + + + + + + + + + + + images - sync diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c52c41ea..56b5e863 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -388,6 +388,7 @@ View count Date Count + Comment date Played date Completed date Duration @@ -478,7 +479,7 @@ Downloads Update interval, Mobile data, Automatic download, Automatic deletion Refresh podcasts - Specify an interval at which Podcini looks for new episodes automatically + Specify an interval (in hours, 0 means never) at which Podcini looks for new episodes automatically. Never Every hour Every 2 hours diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml deleted file mode 100644 index aea0b12f..00000000 --- a/app/src/main/res/xml/preferences.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/xml/preferences_downloads.xml b/app/src/main/res/xml/preferences_downloads.xml deleted file mode 100644 index 57cb9bd6..00000000 --- a/app/src/main/res/xml/preferences_downloads.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/changelog.md b/changelog.md index b20d4f4d..79753f16 100644 --- a/changelog.md +++ b/changelog.md @@ -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