6.14.7 commit
This commit is contained in:
parent
309d7c98b9
commit
5f09864451
|
@ -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 = ""
|
||||
|
|
|
@ -148,28 +148,6 @@
|
|||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true">
|
||||
<!-- <intent-filter>-->
|
||||
<!-- <action android:name="android.intent.action.VIEW" />-->
|
||||
|
||||
<!-- <category android:name="android.intent.category.DEFAULT" />-->
|
||||
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
|
||||
|
||||
<!-- <data-->
|
||||
<!-- android:host="podcini.org"-->
|
||||
<!-- android:pathPrefix="/deeplink/main"-->
|
||||
<!-- android:scheme="https" />-->
|
||||
<!-- </intent-filter>-->
|
||||
<!-- <intent-filter>-->
|
||||
<!-- <action android:name="android.intent.action.VIEW" />-->
|
||||
|
||||
<!-- <category android:name="android.intent.category.DEFAULT" />-->
|
||||
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
|
||||
|
||||
<!-- <data-->
|
||||
<!-- android:host="podcini.org"-->
|
||||
<!-- android:pathPrefix="/deeplink/search"-->
|
||||
<!-- android:scheme="https" />-->
|
||||
<!-- </intent-filter>-->
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="ac.mdiq.podcini.intents.MAIN_ACTIVITY" />
|
||||
|
@ -233,9 +211,9 @@
|
|||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
|
||||
<data android:host="*"/>
|
||||
<data android:pathPattern=".*.xml" />
|
||||
<data android:pathPattern=".*.opml" />
|
||||
<!-- <data android:host="*"/>-->
|
||||
<data android:pathPattern="/.*\\.xml" />
|
||||
<data android:pathPattern="/.*\\.opml" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
|
@ -272,34 +250,6 @@
|
|||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="ac.mdiq.podcini.ui.activity.MainActivity"/>
|
||||
|
||||
<!-- URLs ending with '.xml' or '.rss' -->
|
||||
<!-- <intent-filter>-->
|
||||
<!-- <action android:name="android.intent.action.VIEW"/>-->
|
||||
<!-- <category android:name="android.intent.category.DEFAULT"/>-->
|
||||
<!-- <category android:name="android.intent.category.BROWSABLE"/>-->
|
||||
|
||||
<!-- <data android:scheme="http"/>-->
|
||||
<!-- <data android:scheme="https"/>-->
|
||||
<!-- <data android:host="*"/>-->
|
||||
<!-- <data android:pathPattern=".*\\.xml"/>-->
|
||||
<!-- <data android:pathPattern=".*\\.rss"/>-->
|
||||
<!-- <data android:pathPattern=".*\\.atom"/>-->
|
||||
<!-- </intent-filter>-->
|
||||
|
||||
<!-- Feedburner URLs -->
|
||||
<!-- <intent-filter>-->
|
||||
<!-- <action android:name="android.intent.action.VIEW"/>-->
|
||||
<!-- <category android:name="android.intent.category.DEFAULT"/>-->
|
||||
<!-- <category android:name="android.intent.category.BROWSABLE"/>-->
|
||||
|
||||
<!-- <data android:scheme="http"/>-->
|
||||
<!-- <data android:scheme="https"/>-->
|
||||
<!-- <data android:host="feeds.feedburner.com"/>-->
|
||||
<!-- <data android:host="feedproxy.google.com"/>-->
|
||||
<!-- <data android:host="feeds2.feedburner.com"/>-->
|
||||
<!-- <data android:host="feedsproxy.google.com"/>-->
|
||||
<!-- </intent-filter>-->
|
||||
|
||||
<!-- Files with mimeType rss/xml/atom -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
@ -326,44 +276,6 @@
|
|||
<data android:scheme="podcini-subscribe"/>
|
||||
</intent-filter>
|
||||
|
||||
<!-- Support for subscribeonandroid.com URLS -->
|
||||
<!-- <intent-filter>-->
|
||||
<!-- <action android:name="android.intent.action.VIEW" />-->
|
||||
<!-- <category android:name="android.intent.category.DEFAULT" />-->
|
||||
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
|
||||
|
||||
<!-- <data android:pathPattern=".*\\..*/.*" />-->
|
||||
<!-- <data android:host="subscribeonandroid.com" />-->
|
||||
<!-- <data android:host="www.subscribeonandroid.com" />-->
|
||||
<!-- <data android:host="*subscribeonandroid.com" />-->
|
||||
<!-- <data android:scheme="http" />-->
|
||||
<!-- <data android:scheme="https" />-->
|
||||
<!-- </intent-filter>-->
|
||||
|
||||
<!-- <intent-filter>-->
|
||||
<!-- <action android:name="android.intent.action.VIEW" />-->
|
||||
|
||||
<!-- <category android:name="android.intent.category.DEFAULT" />-->
|
||||
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
|
||||
|
||||
<!-- <data android:host="podcini.org" />-->
|
||||
<!-- <data android:host="www.podcini.org" />-->
|
||||
<!-- <data android:pathPrefix="/deeplink/subscribe" />-->
|
||||
<!-- <data android:scheme="http" />-->
|
||||
<!-- <data android:scheme="https" />-->
|
||||
<!-- </intent-filter>-->
|
||||
|
||||
<!-- <intent-filter>-->
|
||||
<!-- <action android:name="android.intent.action.VIEW" />-->
|
||||
<!-- <category android:name="android.intent.category.DEFAULT" />-->
|
||||
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
|
||||
|
||||
<!-- <data android:pathPattern="/.*/podcast/.*" />-->
|
||||
<!-- <data android:host="podcasts.apple.com" />-->
|
||||
<!-- <data android:scheme="http" />-->
|
||||
<!-- <data android:scheme="https" />-->
|
||||
<!-- </intent-filter>-->
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -7,7 +7,7 @@ import java.io.Writer
|
|||
|
||||
interface ExportWriter {
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
fun writeDocument(feeds: List<Feed?>?, writer: Writer?, context: Context)
|
||||
fun writeDocument(feeds: List<Feed>, writer: Writer?, context: Context)
|
||||
|
||||
fun fileExtension(): String?
|
||||
}
|
|
@ -1,18 +1,31 @@
|
|||
package ac.mdiq.podcini.preferences
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
import ac.mdiq.podcini.util.MiscFormatter.formatRfc822Date
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.MiscFormatter.formatRfc822Date
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.util.Log
|
||||
import android.util.Xml
|
||||
import androidx.core.app.ActivityCompat
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.apache.commons.io.input.BOMInputStream
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
import org.xmlpull.v1.XmlPullParserFactory
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
import java.io.Reader
|
||||
import java.io.Writer
|
||||
import java.util.*
|
||||
import kotlin.Throws
|
||||
|
||||
class OpmlTransporter {
|
||||
|
||||
|
@ -44,11 +57,10 @@ class OpmlTransporter {
|
|||
/** Writes OPML documents. */
|
||||
class OpmlWriter : ExportWriter {
|
||||
/**
|
||||
* Takes a list of feeds and a writer and writes those into an OPML
|
||||
* document.
|
||||
* Takes a list of feeds and a writer and writes those into an OPML document.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
override fun writeDocument(feeds: List<Feed?>?, writer: Writer?, context: Context) {
|
||||
override fun writeDocument(feeds: List<Feed>, writer: Writer?, context: Context) {
|
||||
val xs = Xml.newSerializer()
|
||||
xs.setFeature(OpmlSymbols.XML_FEATURE_INDENT_OUTPUT, true)
|
||||
xs.setOutput(writer)
|
||||
|
@ -67,11 +79,10 @@ class OpmlTransporter {
|
|||
xs.endTag(null, OpmlSymbols.HEAD)
|
||||
|
||||
xs.startTag(null, OpmlSymbols.BODY)
|
||||
for (feed in feeds!!) {
|
||||
if (feed == null) continue
|
||||
Logd(TAG, "writeDocument ${feed?.title}")
|
||||
for (feed in feeds) {
|
||||
Logd(TAG, "writeDocument ${feed.title}")
|
||||
xs.startTag(null, OpmlSymbols.OUTLINE)
|
||||
xs.attribute(null, OpmlSymbols.TEXT, feed!!.title)
|
||||
xs.attribute(null, OpmlSymbols.TEXT, feed.title)
|
||||
xs.attribute(null, OpmlSymbols.TITLE, feed.title)
|
||||
if (feed.type != null) xs.attribute(null, OpmlSymbols.TYPE, feed.type)
|
||||
xs.attribute(null, OpmlSymbols.XMLURL, feed.downloadUrl)
|
||||
|
@ -138,8 +149,7 @@ class OpmlTransporter {
|
|||
}
|
||||
}
|
||||
// TODO: on first install app: java.io.IOException: Underlying input stream returned zero bytes
|
||||
try {
|
||||
eventType = xpp.next()
|
||||
try { eventType = xpp.next()
|
||||
} catch(e: Exception) {
|
||||
Log.e(TAG, "xpp.next() invalid: $e")
|
||||
break
|
||||
|
@ -152,4 +162,56 @@ class OpmlTransporter {
|
|||
private val TAG: String = OpmlReader::class.simpleName ?: "Anonymous"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun startImport(context: Context, uri: Uri) {
|
||||
val TAG = "OpmlTransporter"
|
||||
// CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val opmlFileStream = context.contentResolver.openInputStream(uri)
|
||||
val bomInputStream = BOMInputStream(opmlFileStream)
|
||||
val bom = bomInputStream.bom
|
||||
val charsetName = if (bom == null) "UTF-8" else bom.charsetName
|
||||
val reader: Reader = InputStreamReader(bomInputStream, charsetName)
|
||||
val opmlReader = OpmlReader()
|
||||
val result = opmlReader.readDocument(reader)
|
||||
reader.close()
|
||||
// withContext(Dispatchers.Main) {
|
||||
// binding.progressBar.visibility = View.GONE
|
||||
Logd(TAG, "Parsing was successful")
|
||||
// readElements = result
|
||||
// }
|
||||
} catch (e: Throwable) {
|
||||
// withContext(Dispatchers.Main) {
|
||||
Logd(TAG, Log.getStackTraceString(e))
|
||||
val message = if (e.message == null) "" else e.message!!
|
||||
if (message.lowercase().contains("permission")) {
|
||||
val permission = ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
if (permission != PackageManager.PERMISSION_GRANTED) {
|
||||
// requestPermission()
|
||||
return
|
||||
}
|
||||
}
|
||||
// binding.progressBar.visibility = View.GONE
|
||||
val alert = MaterialAlertDialogBuilder(context)
|
||||
alert.setTitle(R.string.error_label)
|
||||
val userReadable = context.getString(R.string.opml_reader_error)
|
||||
val details = e.message
|
||||
val total = """
|
||||
$userReadable
|
||||
|
||||
$details
|
||||
""".trimIndent()
|
||||
val errorMessage = SpannableString(total)
|
||||
errorMessage.setSpan(ForegroundColorSpan(-0x77777778), userReadable.length, total.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
alert.setMessage(errorMessage)
|
||||
alert.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
// finish()
|
||||
}
|
||||
alert.show()
|
||||
// }
|
||||
}
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package ac.mdiq.podcini.preferences.fragments
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.ChooseDataFolderDialogBinding
|
||||
import ac.mdiq.podcini.databinding.ChooseDataFolderDialogEntryBinding
|
||||
import ac.mdiq.podcini.databinding.ProxySettingsBinding
|
||||
import ac.mdiq.podcini.net.download.service.PodciniHttpClient
|
||||
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.newBuilder
|
||||
|
@ -12,33 +10,43 @@ import ac.mdiq.podcini.preferences.UserPreferences
|
|||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.proxyConfig
|
||||
import ac.mdiq.podcini.storage.model.ProxyConfig
|
||||
import ac.mdiq.podcini.storage.utils.FilesUtils.getDataFolder
|
||||
import ac.mdiq.podcini.storage.utils.StorageUtils.getFreeSpaceAvailable
|
||||
import ac.mdiq.podcini.storage.utils.StorageUtils.getTotalSpaceAvailable
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.SharedPreferences
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.text.format.Formatter
|
||||
import android.util.Patterns
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.util.Consumer
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.TwoStatePreference
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -50,103 +58,152 @@ import okhttp3.Request
|
|||
import okhttp3.Request.Builder
|
||||
import okhttp3.Response
|
||||
import okhttp3.Route
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.net.SocketAddress
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeListener {
|
||||
class DownloadsPreferencesFragment : PreferenceFragmentCompat() {
|
||||
private var blockAutoDeleteLocal = true
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.preferences_downloads)
|
||||
setupNetworkScreen()
|
||||
// addPreferencesFromResource(R.xml.preferences_downloads)
|
||||
// setupNetworkScreen()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
(activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.downloads_pref)
|
||||
appPrefs.registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
return ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
val scrollState = rememberScrollState()
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(16.dp).verticalScroll(scrollState)) {
|
||||
Text(stringResource(R.string.automation), color = textColor, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(stringResource(R.string.feed_refresh_title), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f))
|
||||
var interval by remember { mutableStateOf(appPrefs.getString(UserPreferences.Prefs.prefAutoUpdateIntervall.name, "12")!!) }
|
||||
TextField(value = interval, onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) interval = it },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), label = { Text("(hours)") },
|
||||
singleLine = true, modifier = Modifier.weight(0.5f),
|
||||
trailingIcon = {
|
||||
Icon(imageVector = Icons.Filled.Settings, contentDescription = "Settings icon",
|
||||
modifier = Modifier.size(30.dp).padding(start = 10.dp).clickable(onClick = {
|
||||
if (interval.isEmpty()) interval = "0"
|
||||
appPrefs.edit().putString(UserPreferences.Prefs.prefAutoUpdateIntervall.name, interval).apply()
|
||||
restartUpdateAlarm(requireContext(), true)
|
||||
}))
|
||||
})
|
||||
}
|
||||
Text(stringResource(R.string.feed_refresh_sum), color = textColor)
|
||||
}
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp).clickable(onClick = {
|
||||
(activity as PreferenceActivity).openScreen(R.xml.preferences_autodownload)
|
||||
})) {
|
||||
Text(stringResource(R.string.pref_automatic_download_title), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.pref_automatic_download_sum), color = textColor)
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp)) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(stringResource(R.string.pref_auto_delete_title), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.pref_auto_delete_sum), color = textColor)
|
||||
}
|
||||
Switch(checked = false, onCheckedChange = { appPrefs.edit().putBoolean(UserPreferences.Prefs.prefAutoDelete.name, it).apply() })
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp)) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(stringResource(R.string.pref_auto_local_delete_title), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.pref_auto_local_delete_sum), color = textColor)
|
||||
}
|
||||
Switch(checked = false, onCheckedChange = {
|
||||
if (blockAutoDeleteLocal && it) {
|
||||
// showAutoDeleteEnableDialog()
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(R.string.pref_auto_local_delete_dialog_body)
|
||||
.setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int ->
|
||||
blockAutoDeleteLocal = false
|
||||
(findPreference<Preference>(Prefs.prefAutoDeleteLocal.name) as TwoStatePreference?)!!.isChecked = true
|
||||
blockAutoDeleteLocal = true
|
||||
}
|
||||
.setNegativeButton(R.string.cancel_label, null)
|
||||
.show()
|
||||
}
|
||||
})
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp)) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(stringResource(R.string.pref_keeps_important_episodes_title), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.pref_keeps_important_episodes_sum), color = textColor)
|
||||
}
|
||||
Switch(checked = true, onCheckedChange = { appPrefs.edit().putBoolean(UserPreferences.Prefs.prefFavoriteKeepsEpisode.name, it).apply() })
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp)) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(stringResource(R.string.pref_delete_removes_from_queue_title), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.pref_delete_removes_from_queue_sum), color = textColor)
|
||||
}
|
||||
Switch(checked = true, onCheckedChange = { appPrefs.edit().putBoolean(UserPreferences.Prefs.prefDeleteRemovesFromQueue.name, it).apply() })
|
||||
}
|
||||
Text(stringResource(R.string.download_pref_details), color = textColor, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(top = 10.dp))
|
||||
var showMeteredNetworkOptions by remember { mutableStateOf(false) }
|
||||
var tempSelectedOptions by remember { mutableStateOf(appPrefs.getStringSet(UserPreferences.Prefs.prefMobileUpdateTypes.name, setOf("images"))!!) }
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp).clickable(onClick = { showMeteredNetworkOptions = true })) {
|
||||
Text(stringResource(R.string.pref_metered_network_title), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.pref_mobileUpdate_sum), color = textColor)
|
||||
}
|
||||
if (showMeteredNetworkOptions) {
|
||||
AlertDialog(onDismissRequest = { showMeteredNetworkOptions = false },
|
||||
title = { Text(stringResource(R.string.pref_metered_network_title), style = MaterialTheme.typography.headlineSmall) },
|
||||
text = {
|
||||
Column {
|
||||
MobileUpdateOptions.entries.forEach { option ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(5.dp)
|
||||
.clickable {
|
||||
tempSelectedOptions = if (tempSelectedOptions.contains(option.name)) tempSelectedOptions - option.name
|
||||
else tempSelectedOptions + option.name
|
||||
}) {
|
||||
Checkbox(checked = tempSelectedOptions.contains(option.name),
|
||||
onCheckedChange = {
|
||||
tempSelectedOptions = if (tempSelectedOptions.contains(option.name)) tempSelectedOptions - option.name
|
||||
else tempSelectedOptions + option.name
|
||||
})
|
||||
Text(stringResource(option.res), modifier = Modifier.padding(start = 16.dp), style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
appPrefs.edit().putStringSet(UserPreferences.Prefs.prefMobileUpdateTypes.name, tempSelectedOptions).apply()
|
||||
showMeteredNetworkOptions = false
|
||||
}) { Text(text = "OK") }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { showMeteredNetworkOptions = false }) { Text(text = "Cancel") } }
|
||||
)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
appPrefs.unregisterOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
// override fun onResume() {
|
||||
// super.onResume()
|
||||
//// setDataFolderText()
|
||||
// }
|
||||
|
||||
private fun setupNetworkScreen() {
|
||||
findPreference<Preference>(Prefs.prefAutoDownloadSettings.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
(activity as PreferenceActivity).openScreen(R.xml.preferences_autodownload)
|
||||
true
|
||||
}
|
||||
// validate and set correct value: number of downloads between 1 and 50 (inclusive)
|
||||
findPreference<Preference>(Prefs.prefProxy.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
val dialog = ProxyDialog(requireContext())
|
||||
dialog.show()
|
||||
true
|
||||
}
|
||||
// findPreference<Preference>(PREF_CHOOSE_DATA_DIR)?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
// ChooseDataFolderDialog.showDialog(requireContext()) { path: String? ->
|
||||
// setDataFolder(path!!)
|
||||
//// setDataFolderText()
|
||||
// }
|
||||
// true
|
||||
// }
|
||||
findPreference<Preference>(Prefs.prefAutoDeleteLocal.name)!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
|
||||
if (blockAutoDeleteLocal && newValue as Boolean) {
|
||||
showAutoDeleteEnableDialog()
|
||||
return@OnPreferenceChangeListener false
|
||||
} else return@OnPreferenceChangeListener true
|
||||
}
|
||||
}
|
||||
|
||||
// private fun setDataFolderText() {
|
||||
// val f = getDataFolder(null)
|
||||
// if (f != null) findPreference<Preference>(PREF_CHOOSE_DATA_DIR)!!.summary = f.absolutePath
|
||||
// }
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
||||
if (UserPreferences.Prefs.prefAutoUpdateIntervall.name == key) restartUpdateAlarm(requireContext(), true)
|
||||
}
|
||||
|
||||
private fun showAutoDeleteEnableDialog() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(R.string.pref_auto_local_delete_dialog_body)
|
||||
.setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int ->
|
||||
blockAutoDeleteLocal = false
|
||||
(findPreference<Preference>(Prefs.prefAutoDeleteLocal.name) as TwoStatePreference?)!!.isChecked = true
|
||||
blockAutoDeleteLocal = true
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp).clickable(onClick = {
|
||||
ProxyDialog(requireContext()).show()
|
||||
})) {
|
||||
Text(stringResource(R.string.pref_proxy_title), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.pref_proxy_sum), color = textColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel_label, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
object ChooseDataFolderDialog {
|
||||
fun showDialog(context: Context, handlerFunc: Consumer<String?>) {
|
||||
val content = View.inflate(context, R.layout.choose_data_folder_dialog, null)
|
||||
val dialog = MaterialAlertDialogBuilder(context)
|
||||
.setView(content)
|
||||
.setTitle(R.string.choose_data_directory)
|
||||
.setMessage(R.string.choose_data_directory_message)
|
||||
.setNegativeButton(R.string.cancel_label, null)
|
||||
.create()
|
||||
val binding = ChooseDataFolderDialogBinding.bind(content)
|
||||
val recyclerView = binding.recyclerView
|
||||
recyclerView.layoutManager = LinearLayoutManager(context)
|
||||
val adapter = DataFolderAdapter(context) { path: String? ->
|
||||
dialog.dismiss()
|
||||
handlerFunc.accept(path)
|
||||
}
|
||||
recyclerView.adapter = adapter
|
||||
if (adapter.itemCount != 0) dialog.show()
|
||||
}
|
||||
enum class MobileUpdateOptions(val res: Int) {
|
||||
feed_refresh(R.string.pref_mobileUpdate_refresh),
|
||||
episode_download(R.string.pref_mobileUpdate_episode_download),
|
||||
auto_download(R.string.pref_mobileUpdate_auto_download),
|
||||
streaming(R.string.pref_mobileUpdate_streaming),
|
||||
images(R.string.pref_mobileUpdate_images),
|
||||
sync(R.string.synchronization_pref);
|
||||
}
|
||||
|
||||
class ProxyDialog(private val context: Context) {
|
||||
|
@ -161,13 +218,7 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere
|
|||
private val port: Int
|
||||
get() {
|
||||
val port = etPort.text.toString()
|
||||
if (port.isNotEmpty()) {
|
||||
try {
|
||||
return port.toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (port.isNotEmpty()) try { return port.toInt() } catch (e: NumberFormatException) { }
|
||||
return 0
|
||||
}
|
||||
|
||||
|
@ -203,7 +254,7 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere
|
|||
val types: MutableList<String> = ArrayList()
|
||||
types.add(Proxy.Type.DIRECT.name)
|
||||
types.add(Proxy.Type.HTTP.name)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) types.add(Proxy.Type.SOCKS.name)
|
||||
types.add(Proxy.Type.SOCKS.name)
|
||||
val adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, types)
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
spType.setAdapter(adapter)
|
||||
|
@ -327,23 +378,17 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere
|
|||
if (port.isNotEmpty()) portValue = port.toInt()
|
||||
val address: SocketAddress = InetSocketAddress.createUnresolved(host, portValue)
|
||||
val proxyType = Proxy.Type.valueOf(type.uppercase())
|
||||
val builder: OkHttpClient.Builder = newBuilder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.proxy(Proxy(proxyType, address))
|
||||
val builder: OkHttpClient.Builder = newBuilder().connectTimeout(10, TimeUnit.SECONDS).proxy(Proxy(proxyType, address))
|
||||
if (username.isNotEmpty()) {
|
||||
builder.proxyAuthenticator { _: Route?, response: Response ->
|
||||
val credentials = basic(username, password)
|
||||
response.request.newBuilder()
|
||||
.header("Proxy-Authorization", credentials)
|
||||
.build()
|
||||
response.request.newBuilder().header("Proxy-Authorization", credentials).build()
|
||||
}
|
||||
}
|
||||
val client: OkHttpClient = builder.build()
|
||||
val request: Request = Builder().url("https://www.example.com").head().build()
|
||||
try {
|
||||
client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) throw IOException(response.message)
|
||||
}
|
||||
client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException(response.message) }
|
||||
} catch (e: IOException) { throw e }
|
||||
withContext(Dispatchers.Main) {
|
||||
txtvMessage.setTextColor(getColorFromAttr(context, R.attr.icon_green))
|
||||
|
@ -362,86 +407,10 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere
|
|||
}
|
||||
}
|
||||
|
||||
private class DataFolderAdapter(context: Context, selectionHandler: Consumer<String>) : RecyclerView.Adapter<DataFolderAdapter.ViewHolder?>() {
|
||||
private val selectionHandler: Consumer<String>
|
||||
private val currentPath: String?
|
||||
private val entries: List<StoragePath>
|
||||
private val freeSpaceString: String
|
||||
|
||||
init {
|
||||
this.entries = getStorageEntries(context)
|
||||
this.currentPath = getCurrentPath()
|
||||
this.selectionHandler = selectionHandler
|
||||
this.freeSpaceString = context.getString(R.string.choose_data_directory_available_space)
|
||||
}
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val entryView = inflater.inflate(R.layout.choose_data_folder_dialog_entry, parent, false)
|
||||
return ViewHolder(entryView)
|
||||
}
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val storagePath = entries[position]
|
||||
val context = holder.root.context
|
||||
val freeSpace = Formatter.formatShortFileSize(context, storagePath.availableSpace)
|
||||
val totalSpace = Formatter.formatShortFileSize(context, storagePath.totalSpace)
|
||||
holder.path.text = storagePath.shortPath
|
||||
holder.size.text = String.format(freeSpaceString, freeSpace, totalSpace)
|
||||
holder.progressBar.progress = storagePath.usagePercentage
|
||||
val selectListener = View.OnClickListener { _: View? ->
|
||||
selectionHandler.accept(storagePath.fullPath)
|
||||
}
|
||||
holder.root.setOnClickListener(selectListener)
|
||||
holder.radioButton.setOnClickListener(selectListener)
|
||||
if (storagePath.fullPath == currentPath) holder.radioButton.toggle()
|
||||
}
|
||||
override fun getItemCount(): Int {
|
||||
return entries.size
|
||||
}
|
||||
private fun getCurrentPath(): String? {
|
||||
val dataFolder = getDataFolder(null)
|
||||
return dataFolder?.absolutePath
|
||||
}
|
||||
private fun getStorageEntries(context: Context): List<StoragePath> {
|
||||
val mediaDirs = context.getExternalFilesDirs(null)
|
||||
val entries: MutableList<StoragePath> = ArrayList(mediaDirs.size)
|
||||
for (dir in mediaDirs) {
|
||||
if (!isWritable(dir)) continue
|
||||
entries.add(StoragePath(dir.absolutePath))
|
||||
}
|
||||
if (entries.isEmpty() && isWritable(context.filesDir)) entries.add(StoragePath(context.filesDir.absolutePath))
|
||||
return entries
|
||||
}
|
||||
private fun isWritable(dir: File?): Boolean {
|
||||
return dir != null && dir.exists() && dir.canRead() && dir.canWrite()
|
||||
}
|
||||
private class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val binding = ChooseDataFolderDialogEntryBinding.bind(itemView)
|
||||
val root: View = binding.root
|
||||
val path: TextView = binding.path
|
||||
val size: TextView = binding.size
|
||||
val radioButton: RadioButton = binding.radioButton
|
||||
val progressBar: ProgressBar = binding.usedSpace
|
||||
}
|
||||
private class StoragePath(val fullPath: String) {
|
||||
val shortPath: String
|
||||
get() {
|
||||
val prefixIndex = fullPath.indexOf("Android")
|
||||
return if ((prefixIndex > 0)) fullPath.substring(0, prefixIndex) else fullPath
|
||||
}
|
||||
val availableSpace: Long
|
||||
get() = getFreeSpaceAvailable(fullPath)
|
||||
val totalSpace: Long
|
||||
get() = getTotalSpaceAvailable(fullPath)
|
||||
val usagePercentage: Int
|
||||
get() = 100 - (100 * availableSpace / totalSpace.toFloat()).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("EnumEntryName")
|
||||
private enum class Prefs {
|
||||
prefAutoDownloadSettings,
|
||||
prefAutoDeleteLocal,
|
||||
prefProxy,
|
||||
// prefChooseDataDir,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
|||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||
import ac.mdiq.podcini.storage.model.*
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
|
||||
import ac.mdiq.podcini.storage.database.Episodes.hasAlmostEnded
|
||||
import ac.mdiq.podcini.storage.utils.FileNameGenerator.generateFileName
|
||||
import ac.mdiq.podcini.storage.utils.FilesUtils.getDataFolder
|
||||
import ac.mdiq.podcini.ui.activity.OpmlImportActivity
|
||||
|
@ -23,7 +23,6 @@ import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
|||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.app.ProgressDialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
|
@ -42,20 +41,20 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.core.app.ShareCompat.IntentBuilder
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
|
@ -80,30 +79,30 @@ import kotlin.Throws
|
|||
class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private val chooseOpmlExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
result: ActivityResult -> this.chooseOpmlExportPathResult(result) }
|
||||
result: ActivityResult -> this.chooseOpmlExportPathResult(result) }
|
||||
|
||||
private val chooseHtmlExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
result: ActivityResult -> this.chooseHtmlExportPathResult(result) }
|
||||
result: ActivityResult -> this.chooseHtmlExportPathResult(result) }
|
||||
|
||||
private val chooseFavoritesExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
result: ActivityResult -> this.chooseFavoritesExportPathResult(result) }
|
||||
result: ActivityResult -> this.chooseFavoritesExportPathResult(result) }
|
||||
|
||||
private val chooseProgressExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
result: ActivityResult -> this.chooseProgressExportPathResult(result) }
|
||||
result: ActivityResult -> this.chooseProgressExportPathResult(result) }
|
||||
|
||||
private val restoreProgressLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
result: ActivityResult -> this.restoreProgressResult(result) }
|
||||
|
||||
private val restoreDatabaseLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
result: ActivityResult -> this.restoreDatabaseResult(result) }
|
||||
result: ActivityResult -> this.restoreDatabaseResult(result) }
|
||||
|
||||
private val backupDatabaseLauncher = registerForActivityResult<String, Uri>(BackupDatabase()) { uri: Uri? -> this.backupDatabaseResult(uri) }
|
||||
|
||||
private val chooseOpmlImportPathLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.GetContent()) {
|
||||
uri: Uri? -> this.chooseOpmlImportPathResult(uri) }
|
||||
uri: Uri? -> this.chooseOpmlImportPathResult(uri) }
|
||||
|
||||
private val restorePreferencesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
result: ActivityResult -> this.restorePreferencesResult(result) }
|
||||
result: ActivityResult -> this.restorePreferencesResult(result) }
|
||||
|
||||
private val backupPreferencesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
|
@ -116,22 +115,28 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
result: ActivityResult -> this.restoreMediaFilesResult(result) }
|
||||
|
||||
private val backupMediaFilesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
result: ActivityResult -> this.exportMediaFilesResult(result)
|
||||
}
|
||||
result: ActivityResult -> this.exportMediaFilesResult(result) }
|
||||
|
||||
private var progressDialog: ProgressDialog? = null
|
||||
private var showProgress by mutableStateOf(false)
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
(activity as PreferenceActivity).supportActionBar?.setTitle(R.string.import_export_pref)
|
||||
progressDialog = ProgressDialog(context)
|
||||
progressDialog!!.isIndeterminate = true
|
||||
progressDialog!!.setMessage(requireContext().getString(R.string.please_wait))
|
||||
return ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
if (showProgress) {
|
||||
Dialog(onDismissRequest = { showProgress = false }) {
|
||||
Surface(modifier = Modifier.fillMaxSize(), shape = androidx.compose.foundation.shape.RoundedCornerShape(8.dp)) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
Text("Loading...", modifier = Modifier.align(Alignment.BottomCenter))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val scrollState = rememberScrollState()
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(16.dp).verticalScroll(scrollState)) {
|
||||
Text(stringResource(R.string.database), color = textColor, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
|
||||
|
@ -238,7 +243,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
|
||||
private fun exportWithWriter(exportWriter: ExportWriter, uri: Uri?, exportType: Export) {
|
||||
val context: Context? = activity
|
||||
progressDialog!!.show()
|
||||
showProgress = true
|
||||
if (uri == null) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
|
@ -248,7 +253,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
showExportSuccessSnackbar(fileUri, exportType.contentType)
|
||||
}
|
||||
} catch (e: Exception) { showTransportErrorDialog(e)
|
||||
} finally { progressDialog!!.dismiss() }
|
||||
} finally { showProgress = false }
|
||||
}
|
||||
} else {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
|
@ -259,7 +264,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
showExportSuccessSnackbar(output.uri, exportType.contentType)
|
||||
}
|
||||
} catch (e: Exception) { showTransportErrorDialog(e)
|
||||
} finally { progressDialog!!.dismiss() }
|
||||
} finally { showProgress = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -355,7 +360,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
}
|
||||
|
||||
private fun showTransportErrorDialog(error: Throwable) {
|
||||
progressDialog!!.dismiss()
|
||||
showProgress = false
|
||||
val alert = MaterialAlertDialogBuilder(requireContext())
|
||||
alert.setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int -> dialog.dismiss() }
|
||||
alert.setTitle(R.string.import_export_error_label)
|
||||
|
@ -421,7 +426,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0
|
||||
// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||
if (isJsonFile(uri)) {
|
||||
progressDialog!!.show()
|
||||
showProgress = true
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
@ -432,7 +437,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
showImportSuccessDialog()
|
||||
progressDialog!!.dismiss()
|
||||
showProgress = false
|
||||
}
|
||||
} catch (e: Throwable) { showTransportErrorDialog(e) }
|
||||
}
|
||||
|
@ -456,7 +461,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0
|
||||
// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||
if (isRealmFile(uri)) {
|
||||
progressDialog!!.show()
|
||||
showProgress = true
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
@ -464,7 +469,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
showImportSuccessDialog()
|
||||
progressDialog!!.dismiss()
|
||||
showProgress = false
|
||||
}
|
||||
} catch (e: Throwable) { showTransportErrorDialog(e) }
|
||||
}
|
||||
|
@ -497,7 +502,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0
|
||||
// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||
if (isPrefDir(uri)) {
|
||||
progressDialog!!.show()
|
||||
showProgress = true
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
@ -505,7 +510,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
showImportSuccessDialog()
|
||||
progressDialog!!.dismiss()
|
||||
showProgress = false
|
||||
}
|
||||
} catch (e: Throwable) { showTransportErrorDialog(e) }
|
||||
}
|
||||
|
@ -522,7 +527,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0
|
||||
// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||
if (isMediaFilesDir(uri)) {
|
||||
progressDialog!!.show()
|
||||
showProgress = true
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
@ -530,7 +535,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
showImportSuccessDialog()
|
||||
progressDialog!!.dismiss()
|
||||
showProgress = false
|
||||
}
|
||||
} catch (e: Throwable) { showTransportErrorDialog(e) }
|
||||
}
|
||||
|
@ -546,7 +551,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
val uri = result.data!!.data!!
|
||||
// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ?: 0
|
||||
// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||
progressDialog!!.show()
|
||||
showProgress = true
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
@ -554,7 +559,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
showExportSuccessSnackbar(uri, null)
|
||||
progressDialog!!.dismiss()
|
||||
showProgress = false
|
||||
}
|
||||
} catch (e: Throwable) { showTransportErrorDialog(e) }
|
||||
}
|
||||
|
@ -562,7 +567,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
|
||||
private fun backupDatabaseResult(uri: Uri?) {
|
||||
if (uri == null) return
|
||||
progressDialog!!.show()
|
||||
showProgress = true
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
@ -570,7 +575,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
showExportSuccessSnackbar(uri, "application/x-sqlite3")
|
||||
progressDialog!!.dismiss()
|
||||
showProgress = false
|
||||
}
|
||||
} catch (e: Throwable) { showTransportErrorDialog(e) }
|
||||
}
|
||||
|
@ -578,6 +583,9 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
|
||||
private fun chooseOpmlImportPathResult(uri: Uri?) {
|
||||
if (uri == null) return
|
||||
Logd(TAG, "chooseOpmlImportPathResult: uri: $uri")
|
||||
// OpmlTransporter.startImport(requireContext(), uri)
|
||||
// showImportSuccessDialog()
|
||||
val intent = Intent(context, OpmlImportActivity::class.java)
|
||||
intent.setData(uri)
|
||||
startActivity(intent)
|
||||
|
@ -1014,7 +1022,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
/** Writes saved favorites to file. */
|
||||
class EpisodesProgressWriter : ExportWriter {
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
override fun writeDocument(feeds: List<Feed?>?, writer: Writer?, context: Context) {
|
||||
override fun writeDocument(feeds: List<Feed>, writer: Writer?, context: Context) {
|
||||
Logd(TAG, "Starting to write document")
|
||||
val queuedEpisodeActions: MutableList<EpisodeAction> = mutableListOf()
|
||||
val pausedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.paused.name), EpisodeSortOrder.DATE_NEW_OLD)
|
||||
|
@ -1068,7 +1076,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
/** Writes saved favorites to file. */
|
||||
class FavoritesWriter : ExportWriter {
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
override fun writeDocument(feeds: List<Feed?>?, writer: Writer?, context: Context) {
|
||||
override fun writeDocument(feeds: List<Feed>, writer: Writer?, context: Context) {
|
||||
Logd(TAG, "Starting to write document")
|
||||
val templateStream = context.assets.open("html-export-template.html")
|
||||
var template = IOUtils.toString(templateStream, UTF_8)
|
||||
|
@ -1145,16 +1153,16 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
* Takes a list of feeds and a writer and writes those into an HTML document.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
override fun writeDocument(feeds: List<Feed?>?, writer: Writer?, context: Context) {
|
||||
override fun writeDocument(feeds: List<Feed>, writer: Writer?, context: Context) {
|
||||
Logd(TAG, "Starting to write document")
|
||||
val templateStream = context.assets.open("html-export-template.html")
|
||||
var template = IOUtils.toString(templateStream, "UTF-8")
|
||||
template = template.replace("\\{TITLE\\}".toRegex(), "Subscriptions")
|
||||
val templateParts = template.split("\\{FEEDS\\}".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
writer!!.append(templateParts[0])
|
||||
for (feed in feeds!!) {
|
||||
for (feed in feeds) {
|
||||
writer.append("<li><div><img src=\"")
|
||||
writer.append(feed!!.imageUrl)
|
||||
writer.append(feed.imageUrl)
|
||||
writer.append("\" /><p>")
|
||||
writer.append(feed.title)
|
||||
writer.append(" <span><a href=\"")
|
||||
|
|
|
@ -2,55 +2,37 @@ package ac.mdiq.podcini.preferences.fragments
|
|||
|
||||
import ac.mdiq.podcini.BuildConfig
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.ui.activity.BugReportActivity
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity.Companion.getTitleOfPage
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity.Screens
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
|
@ -63,8 +45,6 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import com.bytehamster.lib.preferencesearch.SearchPreference
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -75,129 +55,150 @@ import java.io.InputStreamReader
|
|||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
|
||||
class MainPreferencesFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
Logd("MainPreferencesFragment", "onCreatePreferences")
|
||||
|
||||
// TODO: this can be expensive
|
||||
addPreferencesFromResource(R.xml.preferences)
|
||||
setupMainScreen()
|
||||
setupSearch()
|
||||
var copyrightNoticeText by mutableStateOf("")
|
||||
|
||||
// If you are writing a spin-off, please update the details on screens like "About" and "Report bug"
|
||||
// and afterwards remove the following lines. Please keep in mind that Podcini is licensed under the GPL.
|
||||
// This means that your application needs to be open-source under the GPL, too.
|
||||
// It must also include a prominent copyright notice.
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
(activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.settings_label)
|
||||
val packageHash = requireContext().packageName.hashCode()
|
||||
// Logd("MainPreferencesFragment", "$packageHash ${"ac.mdiq.podcini.R".hashCode()}")
|
||||
when {
|
||||
packageHash != 1329568231 && packageHash != 1297601420 -> {
|
||||
findPreference<Preference>(Prefs.project.name)!!.isVisible = false
|
||||
val copyrightNotice = Preference(requireContext())
|
||||
copyrightNotice.setIcon(R.drawable.ic_info_white)
|
||||
copyrightNotice.icon!!.mutate().colorFilter = PorterDuffColorFilter(-0x340000, PorterDuff.Mode.MULTIPLY)
|
||||
copyrightNotice.summary = ("This application is based on Podcini."
|
||||
copyrightNoticeText = ("This application is based on Podcini."
|
||||
+ " The Podcini team does NOT provide support for this unofficial version."
|
||||
+ " If you can read this message, the developers of this modification"
|
||||
+ " violate the GNU General Public License (GPL).")
|
||||
findPreference<Preference>(Prefs.project.name)!!.parent!!.addPreference(copyrightNotice)
|
||||
}
|
||||
packageHash == 1297601420 -> {
|
||||
val debugNotice = Preference(requireContext())
|
||||
debugNotice.setIcon(R.drawable.ic_info_white)
|
||||
debugNotice.icon!!.mutate().colorFilter = PorterDuffColorFilter(-0x340000, PorterDuff.Mode.MULTIPLY)
|
||||
debugNotice.order = -1
|
||||
debugNotice.summary = "This is a development version of Podcini and not meant for daily use"
|
||||
findPreference<Preference>(Prefs.project.name)!!.parent!!.addPreference(debugNotice)
|
||||
packageHash == 1297601420 -> copyrightNoticeText = "This is a development version of Podcini and not meant for daily use"
|
||||
}
|
||||
|
||||
return ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
val scrollState = rememberScrollState()
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(start = 10.dp, end = 10.dp).verticalScroll(scrollState)) {
|
||||
if (copyrightNoticeText.isNotBlank()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = Icons.Filled.Info, contentDescription = "", tint = Color.Red, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Text(copyrightNoticeText, color = textColor)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_appearance), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Column(modifier = Modifier.weight(1f).clickable(onClick = {
|
||||
(activity as PreferenceActivity).openScreen(R.xml.preferences_user_interface)
|
||||
})) {
|
||||
Text(stringResource(R.string.user_interface_label), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.user_interface_sum), color = textColor)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_play_24dp), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Column(modifier = Modifier.weight(1f).clickable(onClick = {
|
||||
(activity as PreferenceActivity).openScreen(R.xml.preferences_playback)
|
||||
})) {
|
||||
Text(stringResource(R.string.playback_pref), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.playback_pref_sum), color = textColor)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_download), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Column(modifier = Modifier.weight(1f).clickable(onClick = {
|
||||
(activity as PreferenceActivity).openScreen(Screens.preferences_downloads)
|
||||
})) {
|
||||
Text(stringResource(R.string.downloads_pref), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.downloads_pref_sum), color = textColor)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_cloud), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Column(modifier = Modifier.weight(1f).clickable(onClick = {
|
||||
(activity as PreferenceActivity).openScreen(R.xml.preferences_synchronization)
|
||||
})) {
|
||||
Text(stringResource(R.string.synchronization_pref), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.synchronization_sum), color = textColor)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_storage), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Column(modifier = Modifier.weight(1f).clickable(onClick = {
|
||||
(activity as PreferenceActivity).openScreen(Screens.preferences_import_export)
|
||||
})) {
|
||||
Text(stringResource(R.string.import_export_pref), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.import_export_summary), color = textColor)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_notifications), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Column(modifier = Modifier.weight(1f).clickable(onClick = {
|
||||
(activity as PreferenceActivity).openScreen(R.xml.preferences_notifications)
|
||||
})) {
|
||||
Text(stringResource(R.string.notification_pref_fragment), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(stringResource(R.string.pref_backup_on_google_title), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(stringResource(R.string.pref_backup_on_google_sum), color = textColor)
|
||||
}
|
||||
Switch(checked = true, onCheckedChange = {
|
||||
appPrefs.edit().putBoolean(UserPreferences.Prefs.prefOPMLBackup.name, it).apply()
|
||||
// Restart the app
|
||||
val intent = context?.packageManager?.getLaunchIntentForPackage(requireContext().packageName)
|
||||
intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
context?.startActivity(intent)
|
||||
})
|
||||
}
|
||||
HorizontalDivider(modifier = Modifier.fillMaxWidth().height(1.dp).padding(top = 10.dp))
|
||||
Text(stringResource(R.string.project_pref), color = textColor, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, modifier = Modifier.padding(top = 15.dp))
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_questionmark), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Column(modifier = Modifier.weight(1f).clickable(onClick = {
|
||||
openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini")
|
||||
})) {
|
||||
Text(stringResource(R.string.documentation_support), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_chat), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Column(modifier = Modifier.weight(1f).clickable(onClick = {
|
||||
openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini/discussions")
|
||||
})) {
|
||||
Text(stringResource(R.string.visit_user_forum), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_contribute), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Column(modifier = Modifier.weight(1f).clickable(onClick = {
|
||||
openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini")
|
||||
})) {
|
||||
Text(stringResource(R.string.pref_contribute), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_bug), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Column(modifier = Modifier.weight(1f).clickable(onClick = {
|
||||
startActivity(Intent(activity, BugReportActivity::class.java))
|
||||
})) {
|
||||
Text(stringResource(R.string.bug_report_title), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_info), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
|
||||
Column(modifier = Modifier.weight(1f).clickable(onClick = {
|
||||
parentFragmentManager.beginTransaction().replace(R.id.settingsContainer, AboutFragment()).addToBackStack(getString(R.string.about_pref)).commit()
|
||||
})) {
|
||||
Text(stringResource(R.string.about_pref), color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
(activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.settings_label)
|
||||
}
|
||||
|
||||
@SuppressLint("CommitTransaction")
|
||||
private fun setupMainScreen() {
|
||||
findPreference<Preference>(Prefs.prefScreenInterface.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
(activity as PreferenceActivity).openScreen(R.xml.preferences_user_interface)
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(Prefs.prefScreenPlayback.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
(activity as PreferenceActivity).openScreen(R.xml.preferences_playback)
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(Prefs.prefScreenDownloads.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
(activity as PreferenceActivity).openScreen(R.xml.preferences_downloads)
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(Prefs.prefScreenSynchronization.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
(activity as PreferenceActivity).openScreen(R.xml.preferences_synchronization)
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(Prefs.prefScreenImportExport.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
(activity as PreferenceActivity).openScreen(Screens.preferences_import_export)
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(Prefs.notifications.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
(activity as PreferenceActivity).openScreen(R.xml.preferences_notifications)
|
||||
true
|
||||
}
|
||||
val switchPreference = findPreference<SwitchPreferenceCompat>("prefOPMLBackup")
|
||||
switchPreference?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
if (newValue is Boolean) {
|
||||
// Restart the app
|
||||
val intent = context?.packageManager?.getLaunchIntentForPackage(requireContext().packageName)
|
||||
intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
context?.startActivity(intent)
|
||||
}
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(Prefs.prefAbout.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
parentFragmentManager.beginTransaction().replace(R.id.settingsContainer, AboutFragment()).addToBackStack(getString(R.string.about_pref)).commit()
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(Prefs.prefDocumentation.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini")
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(Prefs.prefViewForum.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini/discussions")
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(Prefs.prefContribute.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini")
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(Prefs.prefSendBugReport.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
startActivity(Intent(activity, BugReportActivity::class.java))
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSearch() {
|
||||
val searchPreference = findPreference<SearchPreference>("searchPreference")
|
||||
val config = searchPreference!!.searchConfiguration
|
||||
config.setActivity((activity as AppCompatActivity))
|
||||
config.setFragmentContainerViewId(R.id.settingsContainer)
|
||||
config.setBreadcrumbsEnabled(true)
|
||||
|
||||
config.index(R.xml.preferences_user_interface).addBreadcrumb(getTitleOfPage(R.xml.preferences_user_interface))
|
||||
config.index(R.xml.preferences_playback).addBreadcrumb(getTitleOfPage(R.xml.preferences_playback))
|
||||
config.index(R.xml.preferences_downloads).addBreadcrumb(getTitleOfPage(R.xml.preferences_downloads))
|
||||
// config.index(R.xml.preferences_import_export).addBreadcrumb(getTitleOfPage(R.xml.preferences_import_export))
|
||||
config.index(R.xml.preferences_autodownload)
|
||||
.addBreadcrumb(getTitleOfPage(R.xml.preferences_downloads))
|
||||
.addBreadcrumb(R.string.automation)
|
||||
.addBreadcrumb(getTitleOfPage(R.xml.preferences_autodownload))
|
||||
config.index(R.xml.preferences_synchronization).addBreadcrumb(getTitleOfPage(R.xml.preferences_synchronization))
|
||||
config.index(R.xml.preferences_notifications).addBreadcrumb(getTitleOfPage(R.xml.preferences_notifications))
|
||||
// config.index(R.xml.feed_settings).addBreadcrumb(getTitleOfPage(R.xml.feed_settings))
|
||||
// config.index(R.xml.preferences_swipe)
|
||||
// .addBreadcrumb(getTitleOfPage(R.xml.preferences_user_interface))
|
||||
// .addBreadcrumb(getTitleOfPage(R.xml.preferences_swipe))
|
||||
}
|
||||
|
||||
class AboutFragment : PreferenceFragmentCompat() {
|
||||
@SuppressLint("CommitTransaction")
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {}
|
||||
|
@ -208,7 +209,7 @@ class MainPreferencesFragment : PreferenceFragmentCompat() {
|
|||
setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||
Column(modifier = Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp)) {
|
||||
Image(painter = painterResource(R.drawable.teaser), contentDescription = "")
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 10.dp, top = 5.dp, bottom = 5.dp)) {
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_star), contentDescription = "", tint = textColor)
|
||||
|
@ -324,20 +325,4 @@ class MainPreferencesFragment : PreferenceFragmentCompat() {
|
|||
private class LicenseItem(val title: String, val subtitle: String, val licenseUrl: String, val licenseTextFile: String)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("EnumEntryName")
|
||||
private enum class Prefs {
|
||||
prefScreenInterface,
|
||||
prefScreenPlayback,
|
||||
prefScreenDownloads,
|
||||
prefScreenImportExport,
|
||||
prefScreenSynchronization,
|
||||
prefDocumentation,
|
||||
prefViewForum,
|
||||
prefSendBugReport,
|
||||
project,
|
||||
prefAbout,
|
||||
notifications,
|
||||
prefContribute,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -18,7 +18,7 @@ import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
|||
import ac.mdiq.podcini.storage.database.RealmDB.upsert
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||
import ac.mdiq.podcini.storage.model.*
|
||||
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
|
||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.getPermutor
|
||||
import ac.mdiq.podcini.storage.utils.FilesUtils.getMediafilename
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
|
@ -41,6 +41,8 @@ import kotlin.math.min
|
|||
object Episodes {
|
||||
private val TAG: String = Episodes::class.simpleName ?: "Anonymous"
|
||||
|
||||
private val smartMarkAsPlayedPercent: Int = 95
|
||||
|
||||
val prefRemoveFromQueueMarkedPlayed by lazy { appPrefs.getBoolean(Prefs.prefRemoveFromQueueMarkedPlayed.name, true) }
|
||||
val prefDeleteRemovesFromQueue by lazy { appPrefs.getBoolean(Prefs.prefDeleteRemovesFromQueue.name, false) }
|
||||
|
||||
|
@ -265,4 +267,32 @@ object Episodes {
|
|||
e.media = m
|
||||
return e
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun indexOfItemWithId(episodes: List<Episode?>, id: Long): Int {
|
||||
for (i in episodes.indices) {
|
||||
val episode = episodes[i]
|
||||
if (episode?.id == id) return i
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun episodeListContains(episodes: List<Episode?>, itemId: Long): Boolean {
|
||||
return indexOfItemWithId(episodes, itemId) >= 0
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun indexOfItemWithDownloadUrl(items: List<Episode?>, downloadUrl: String): Int {
|
||||
for (i in items.indices) {
|
||||
val item = items[i]
|
||||
if (item?.media?.downloadUrl == downloadUrl) return i
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun hasAlmostEnded(media: Playable): Boolean {
|
||||
return media.getDuration() > 0 && media.getPosition() >= media.getDuration() * smartMarkAsPlayedPercent * 0.01
|
||||
}
|
||||
}
|
|
@ -5,14 +5,14 @@ import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
|||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.storage.database.Episodes.indexOfItemWithId
|
||||
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsert
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||
import ac.mdiq.podcini.storage.model.*
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil.indexOfItemWithId
|
||||
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
|
||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.getPermutor
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
|
@ -73,9 +73,7 @@ object Queues {
|
|||
Logd(TAG, "getQueueIDList() called")
|
||||
val queues = realm.query(PlayQueue::class).find()
|
||||
val ids = mutableSetOf<Long>()
|
||||
for (queue in queues) {
|
||||
ids.addAll(queue.episodeIds)
|
||||
}
|
||||
for (queue in queues) ids.addAll(queue.episodeIds)
|
||||
return ids
|
||||
}
|
||||
|
||||
|
@ -180,9 +178,7 @@ object Queues {
|
|||
it.episodeIds.clear()
|
||||
it.update()
|
||||
}
|
||||
for (e in curQueue.episodes) {
|
||||
if (e.playState < PlayState.SKIPPED.code) setPlayState(PlayState.SKIPPED.code, false, e)
|
||||
}
|
||||
for (e in curQueue.episodes) if (e.playState < PlayState.SKIPPED.code) setPlayState(PlayState.SKIPPED.code, false, e)
|
||||
curQueue.episodes.clear()
|
||||
EventFlow.postEvent(FlowEvent.QueueEvent.cleared())
|
||||
}
|
||||
|
@ -191,9 +187,7 @@ object Queues {
|
|||
fun removeFromAllQueuesSync(vararg episodes: Episode) {
|
||||
Logd(TAG, "removeFromAllQueuesSync called ")
|
||||
val queues = realm.query(PlayQueue::class).find()
|
||||
for (q in queues) {
|
||||
if (q.id != curQueue.id) removeFromQueueSync(q, *episodes)
|
||||
}
|
||||
for (q in queues) if (q.id != curQueue.id) removeFromQueueSync(q, *episodes)
|
||||
// ensure curQueue is last updated
|
||||
if (curQueue.size() > 0) removeFromQueueSync(curQueue, *episodes)
|
||||
else upsertBlk(curQueue) { it.update() }
|
||||
|
@ -250,9 +244,7 @@ object Queues {
|
|||
idsInQueuesToRemove = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet()
|
||||
if (idsInQueuesToRemove.isNotEmpty()) {
|
||||
val eList = realm.query(Episode::class).query("id IN $0", idsInQueuesToRemove).find()
|
||||
for (e in eList) {
|
||||
if (e.playState < PlayState.SKIPPED.code) setPlayState(PlayState.SKIPPED.code, false, e)
|
||||
}
|
||||
for (e in eList) if (e.playState < PlayState.SKIPPED.code) setPlayState(PlayState.SKIPPED.code, false, e)
|
||||
upsert(q) {
|
||||
it.idsBinList.removeAll(idsInQueuesToRemove)
|
||||
it.idsBinList.addAll(idsInQueuesToRemove)
|
||||
|
@ -272,9 +264,7 @@ object Queues {
|
|||
idsInQueuesToRemove = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet()
|
||||
if (idsInQueuesToRemove.isNotEmpty()) {
|
||||
val eList = realm.query(Episode::class).query("id IN $0", idsInQueuesToRemove).find()
|
||||
for (e in eList) {
|
||||
if (e.playState < PlayState.SKIPPED.code) setPlayState(PlayState.SKIPPED.code, false, e)
|
||||
}
|
||||
for (e in eList) if (e.playState < PlayState.SKIPPED.code) setPlayState(PlayState.SKIPPED.code, false, e)
|
||||
curQueue = upsert(q) {
|
||||
it.idsBinList.removeAll(idsInQueuesToRemove)
|
||||
it.idsBinList.addAll(idsInQueuesToRemove)
|
||||
|
@ -313,13 +303,11 @@ object Queues {
|
|||
}
|
||||
}
|
||||
|
||||
fun inAnyQueue(episode: Episode): Boolean {
|
||||
val queues = realm.query(PlayQueue::class).find()
|
||||
for (q in queues) {
|
||||
if (q.contains(episode)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
// fun inAnyQueue(episode: Episode): Boolean {
|
||||
// val queues = realm.query(PlayQueue::class).find()
|
||||
// for (q in queues) if (q.contains(episode)) return true
|
||||
// return false
|
||||
// }
|
||||
|
||||
class EnqueuePositionPolicy(private val enqueueLocation: EnqueueLocation) {
|
||||
/**
|
||||
|
@ -349,9 +337,7 @@ object Queues {
|
|||
}
|
||||
private fun getPositionOfFirstNonDownloadingItem(startPosition: Int, queueItems: List<Episode>): Int {
|
||||
val curQueueSize = queueItems.size
|
||||
for (i in startPosition until curQueueSize) {
|
||||
if (!isItemAtPositionDownloading(i, queueItems)) return i
|
||||
}
|
||||
for (i in startPosition until curQueueSize) if (!isItemAtPositionDownloading(i, queueItems)) return i
|
||||
return curQueueSize
|
||||
}
|
||||
private fun isItemAtPositionDownloading(position: Int, queueItems: List<Episode>): Boolean {
|
||||
|
@ -362,9 +348,7 @@ object Queues {
|
|||
private fun getCurrentlyPlayingPosition(queueItems: List<Episode>, currentPlaying: Playable?): Int {
|
||||
if (currentPlaying !is EpisodeMedia) return -1
|
||||
val curPlayingItemId = currentPlaying.episodeOrFetch()?.id
|
||||
for (i in queueItems.indices) {
|
||||
if (curPlayingItemId == queueItems[i].id) return i
|
||||
}
|
||||
for (i in queueItems.indices) if (curPlayingItemId == queueItems[i].id) return i
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -92,6 +92,8 @@ class Episode : RealmObject {
|
|||
@FullText
|
||||
var comment: String = ""
|
||||
|
||||
var commentTime: Long = 0L
|
||||
|
||||
@Ignore
|
||||
val isNew: Boolean
|
||||
get() = playState == PlayState.NEW.code
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package ac.mdiq.podcini.storage.model
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
enum class EpisodeSortOrder(val code: Int, val res: Int) {
|
||||
DATE_OLD_NEW(1, R.string.publish_date),
|
||||
|
@ -21,6 +23,8 @@ enum class EpisodeSortOrder(val code: Int, val res: Int) {
|
|||
DOWNLOAD_DATE_NEW_OLD(16, R.string.download_date),
|
||||
VIEWS_LOW_HIGH(17, R.string.view_count),
|
||||
VIEWS_HIGH_LOW(18, R.string.view_count),
|
||||
COMMENT_DATE_OLD_NEW(19, R.string.last_comment_date),
|
||||
COMMENT_DATE_NEW_OLD(20, R.string.last_comment_date),
|
||||
|
||||
FEED_TITLE_A_Z(101, R.string.feed_title),
|
||||
FEED_TITLE_Z_A(102, R.string.feed_title),
|
||||
|
@ -60,5 +64,187 @@ enum class EpisodeSortOrder(val code: Int, val res: Int) {
|
|||
for (i in stringValues.indices) values[i] = valueOf(stringValues[i]!!)
|
||||
return values
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Permutor that sorts a list appropriate to the given sort order.
|
||||
* @return Permutor that sorts a list appropriate to the given sort order.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getPermutor(sortOrder: EpisodeSortOrder): Permutor<Episode> {
|
||||
var comparator: java.util.Comparator<Episode>? = null
|
||||
var permutor: Permutor<Episode>? = null
|
||||
|
||||
when (sortOrder) {
|
||||
EPISODE_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemTitle(f1).compareTo(itemTitle(f2)) }
|
||||
EPISODE_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemTitle(f2).compareTo(itemTitle(f1)) }
|
||||
DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> pubDate(f1).compareTo(pubDate(f2)) }
|
||||
DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> pubDate(f2).compareTo(pubDate(f1)) }
|
||||
DURATION_SHORT_LONG -> comparator = Comparator { f1: Episode?, f2: Episode? -> duration(f1).compareTo(duration(f2)) }
|
||||
DURATION_LONG_SHORT -> comparator = Comparator { f1: Episode?, f2: Episode? -> duration(f2).compareTo(duration(f1)) }
|
||||
EPISODE_FILENAME_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemLink(f1).compareTo(itemLink(f2)) }
|
||||
EPISODE_FILENAME_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemLink(f2).compareTo(itemLink(f1)) }
|
||||
PLAYED_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> playDate(f1).compareTo(playDate(f2)) }
|
||||
PLAYED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> playDate(f2).compareTo(playDate(f1)) }
|
||||
COMPLETED_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> completeDate(f1).compareTo(completeDate(f2)) }
|
||||
COMPLETED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> completeDate(f2).compareTo(completeDate(f1)) }
|
||||
DOWNLOAD_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> downloadDate(f1).compareTo(downloadDate(f2)) }
|
||||
DOWNLOAD_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> downloadDate(f2).compareTo(downloadDate(f1)) }
|
||||
VIEWS_LOW_HIGH -> comparator = Comparator { f1: Episode?, f2: Episode? -> viewCount(f1).compareTo(viewCount(f2)) }
|
||||
VIEWS_HIGH_LOW -> comparator = Comparator { f1: Episode?, f2: Episode? -> viewCount(f2).compareTo(viewCount(f1)) }
|
||||
COMMENT_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> commentDate(f1).compareTo(commentDate(f2)) }
|
||||
COMMENT_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> commentDate(f2).compareTo(playDate(f1)) }
|
||||
|
||||
FEED_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f1).compareTo(feedTitle(f2)) }
|
||||
FEED_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f2).compareTo(feedTitle(f1)) }
|
||||
RANDOM, RANDOM1 -> permutor = object : Permutor<Episode> {
|
||||
override fun reorder(queue: MutableList<Episode>?) {
|
||||
if (!queue.isNullOrEmpty()) queue.shuffle()
|
||||
}
|
||||
}
|
||||
SMART_SHUFFLE_OLD_NEW -> permutor = object : Permutor<Episode> {
|
||||
override fun reorder(queue: MutableList<Episode>?) {
|
||||
if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList<Episode?>, true)
|
||||
}
|
||||
}
|
||||
SMART_SHUFFLE_NEW_OLD -> permutor = object : Permutor<Episode> {
|
||||
override fun reorder(queue: MutableList<Episode>?) {
|
||||
if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList<Episode?>, false)
|
||||
}
|
||||
}
|
||||
SIZE_SMALL_LARGE -> comparator = Comparator { f1: Episode?, f2: Episode? -> size(f1).compareTo(size(f2)) }
|
||||
SIZE_LARGE_SMALL -> comparator = Comparator { f1: Episode?, f2: Episode? -> size(f2).compareTo(size(f1)) }
|
||||
}
|
||||
if (comparator != null) {
|
||||
val comparator2: java.util.Comparator<Episode> = comparator
|
||||
permutor = object : Permutor<Episode> {
|
||||
override fun reorder(queue: MutableList<Episode>?) {if (!queue.isNullOrEmpty()) queue.sortWith(comparator2)}
|
||||
}
|
||||
}
|
||||
return permutor!!
|
||||
}
|
||||
|
||||
private fun pubDate(item: Episode?): Date {
|
||||
return if (item == null) Date() else Date(item.pubDate)
|
||||
}
|
||||
|
||||
private fun playDate(item: Episode?): Long {
|
||||
return item?.media?.getLastPlayedTime() ?: 0
|
||||
}
|
||||
|
||||
private fun commentDate(item: Episode?): Long {
|
||||
return item?.commentTime ?: 0
|
||||
}
|
||||
|
||||
private fun downloadDate(item: Episode?): Long {
|
||||
return item?.media?.downloadTime ?: 0
|
||||
}
|
||||
|
||||
private fun completeDate(item: Episode?): Date {
|
||||
return item?.media?.playbackCompletionDate ?: Date(0)
|
||||
}
|
||||
|
||||
private fun itemTitle(item: Episode?): String {
|
||||
return (item?.title ?: "").lowercase(Locale.getDefault())
|
||||
}
|
||||
|
||||
private fun duration(item: Episode?): Int {
|
||||
return item?.media?.getDuration() ?: 0
|
||||
}
|
||||
|
||||
private fun size(item: Episode?): Long {
|
||||
return item?.media?.size ?: 0
|
||||
}
|
||||
|
||||
private fun itemLink(item: Episode?): String {
|
||||
return (item?.link ?: "").lowercase(Locale.getDefault())
|
||||
}
|
||||
|
||||
private fun feedTitle(item: Episode?): String {
|
||||
return (item?.feed?.title ?: "").lowercase(Locale.getDefault())
|
||||
}
|
||||
|
||||
private fun viewCount(item: Episode?): Int {
|
||||
return item?.viewCount ?: 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a reordering by pubdate that avoids consecutive episodes from the same feed in the queue.
|
||||
* A listener might want to hear episodes from any given feed in pubdate order, but would
|
||||
* prefer a more balanced ordering that avoids having to listen to clusters of consecutive
|
||||
* episodes from the same feed. This is what "Smart Shuffle" tries to accomplish.
|
||||
* Assume the queue looks like this: `ABCDDEEEEEEEEEE`.
|
||||
* This method first starts with a queue of the final size, where each slot is empty (null).
|
||||
* It takes the podcast with most episodes (`E`) and places the episodes spread out in the queue: `EE_E_EE_E_EE_EE`.
|
||||
* The podcast with the second-most number of episodes (`D`) is then
|
||||
* placed spread-out in the *available* slots: `EE_EDEE_EDEE_EE`.
|
||||
* This continues, until we end up with: `EEBEDEECEDEEAEE`.
|
||||
* Note that episodes aren't strictly ordered in terms of pubdate, but episodes of each feed are.
|
||||
*
|
||||
* @param queue A (modifiable) list of FeedItem elements to be reordered.
|
||||
* @param ascending `true` to use ascending pubdate in the reordering;
|
||||
* `false` for descending.
|
||||
*/
|
||||
private fun smartShuffle(queue: MutableList<Episode?>, ascending: Boolean) {
|
||||
// Divide FeedItems into lists by feed
|
||||
val map: MutableMap<Long, MutableList<Episode>> = HashMap()
|
||||
for (item in queue) {
|
||||
if (item == null) continue
|
||||
val id = item.feedId
|
||||
if (id != null) {
|
||||
if (!map.containsKey(id)) map[id] = ArrayList()
|
||||
map[id]!!.add(item)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort each individual list by PubDate (ascending/descending)
|
||||
val itemComparator: java.util.Comparator<Episode> =
|
||||
if (ascending) Comparator { f1: Episode, f2: Episode -> f1.pubDate.compareTo(f2.pubDate) }
|
||||
else Comparator { f1: Episode, f2: Episode -> f2.pubDate.compareTo(f1.pubDate) }
|
||||
|
||||
val feeds: MutableList<List<Episode>> = ArrayList()
|
||||
for ((_, value) in map) {
|
||||
value.sortWith(itemComparator)
|
||||
feeds.add(value)
|
||||
}
|
||||
|
||||
val emptySlots = ArrayList<Int>()
|
||||
for (i in queue.indices) {
|
||||
queue[i] = null
|
||||
emptySlots.add(i)
|
||||
}
|
||||
|
||||
// Starting with the largest feed, place items spread out through the empty slots in the queue
|
||||
feeds.sortWith { f1: List<Episode>, f2: List<Episode> -> f2.size.compareTo(f1.size) }
|
||||
for (feedItems in feeds) {
|
||||
val spread = emptySlots.size.toDouble() / (feedItems.size + 1)
|
||||
val emptySlotIterator = emptySlots.iterator()
|
||||
var skipped = 0
|
||||
var placed = 0
|
||||
while (emptySlotIterator.hasNext()) {
|
||||
val nextEmptySlot = emptySlotIterator.next()
|
||||
skipped++
|
||||
if (skipped >= spread * (placed + 1)) {
|
||||
if (queue[nextEmptySlot] != null) throw RuntimeException("Slot to be placed in not empty")
|
||||
queue[nextEmptySlot] = feedItems[placed]
|
||||
emptySlotIterator.remove()
|
||||
placed++
|
||||
if (placed == feedItems.size) break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for passing around list permutor method. This is used for cases where a simple comparator
|
||||
* won't work (e.g. Random, Smart Shuffle, etc)
|
||||
* @param <E> the type of elements in the list
|
||||
</E> */
|
||||
interface Permutor<E> {
|
||||
/**
|
||||
* Reorders the specified list.
|
||||
* @param queue A (modifiable) list of elements to be reordered
|
||||
*/
|
||||
fun reorder(queue: MutableList<E>?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package ac.mdiq.podcini.storage.model
|
|||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.model.FeedFunding.Companion.extractPaymentLinks
|
||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.fromCode
|
||||
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
|
||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.getPermutor
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
|
@ -102,6 +102,8 @@ class Feed : RealmObject {
|
|||
@FullText
|
||||
var comment: String = ""
|
||||
|
||||
var commentTime: Long = 0L
|
||||
|
||||
/**
|
||||
* Returns the value that uniquely identifies this Feed. If the
|
||||
* feedIdentifier attribute is not null, it will be returned. Else it will
|
||||
|
@ -288,6 +290,10 @@ class Feed : RealmObject {
|
|||
paymentLinks.add(funding)
|
||||
}
|
||||
|
||||
fun isSynthetic(): Boolean {
|
||||
return id <= MAX_SYNTHETIC_ID
|
||||
}
|
||||
|
||||
fun getVirtualQueueItems(): List<Episode> {
|
||||
var qString = "feedId == $id AND (playState < ${PlayState.SKIPPED.code} OR playState == ${PlayState.AGAIN.code} OR playState == ${PlayState.FOREVER.code})"
|
||||
// TODO: perhaps need to set prefStreamOverDownload for youtube feeds
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
package ac.mdiq.podcini.storage.utils
|
||||
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.Playable
|
||||
|
||||
object EpisodeUtil {
|
||||
private val TAG: String = EpisodeUtil::class.simpleName ?: "Anonymous"
|
||||
// val smartMarkAsPlayedSecs: Int
|
||||
// get() = appPrefs.getString(UserPreferences.Prefs.prefSmartMarkAsPlayedSecs.name, "30")!!.toInt()
|
||||
|
||||
private val smartMarkAsPlayedPercent: Int = 95
|
||||
|
||||
@JvmStatic
|
||||
fun indexOfItemWithId(episodes: List<Episode?>, id: Long): Int {
|
||||
for (i in episodes.indices) {
|
||||
val episode = episodes[i]
|
||||
if (episode?.id == id) return i
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun episodeListContains(episodes: List<Episode?>, itemId: Long): Boolean {
|
||||
return indexOfItemWithId(episodes, itemId) >= 0
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun indexOfItemWithDownloadUrl(items: List<Episode?>, downloadUrl: String): Int {
|
||||
for (i in items.indices) {
|
||||
val item = items[i]
|
||||
if (item?.media?.downloadUrl == downloadUrl) return i
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun hasAlmostEnded(media: Playable): Boolean {
|
||||
return media.getDuration() > 0 && media.getPosition() >= media.getDuration() * smartMarkAsPlayedPercent * 0.01
|
||||
}
|
||||
}
|
|
@ -1,186 +0,0 @@
|
|||
package ac.mdiq.podcini.storage.utils
|
||||
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Provides method for sorting the a list of [Episode] according to rules.
|
||||
*/
|
||||
object EpisodesPermutors {
|
||||
/**
|
||||
* Returns a Permutor that sorts a list appropriate to the given sort order.
|
||||
* @return Permutor that sorts a list appropriate to the given sort order.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getPermutor(sortOrder: EpisodeSortOrder): Permutor<Episode> {
|
||||
var comparator: Comparator<Episode>? = null
|
||||
var permutor: Permutor<Episode>? = null
|
||||
|
||||
when (sortOrder) {
|
||||
EpisodeSortOrder.EPISODE_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemTitle(f1).compareTo(itemTitle(f2)) }
|
||||
EpisodeSortOrder.EPISODE_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemTitle(f2).compareTo(itemTitle(f1)) }
|
||||
EpisodeSortOrder.DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> pubDate(f1).compareTo(pubDate(f2)) }
|
||||
EpisodeSortOrder.DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> pubDate(f2).compareTo(pubDate(f1)) }
|
||||
EpisodeSortOrder.DURATION_SHORT_LONG -> comparator = Comparator { f1: Episode?, f2: Episode? -> duration(f1).compareTo(duration(f2)) }
|
||||
EpisodeSortOrder.DURATION_LONG_SHORT -> comparator = Comparator { f1: Episode?, f2: Episode? -> duration(f2).compareTo(duration(f1)) }
|
||||
EpisodeSortOrder.EPISODE_FILENAME_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemLink(f1).compareTo(itemLink(f2)) }
|
||||
EpisodeSortOrder.EPISODE_FILENAME_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemLink(f2).compareTo(itemLink(f1)) }
|
||||
EpisodeSortOrder.PLAYED_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> playDate(f1).compareTo(playDate(f2)) }
|
||||
EpisodeSortOrder.PLAYED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> playDate(f2).compareTo(playDate(f1)) }
|
||||
EpisodeSortOrder.COMPLETED_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> completeDate(f1).compareTo(completeDate(f2)) }
|
||||
EpisodeSortOrder.COMPLETED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> completeDate(f2).compareTo(completeDate(f1)) }
|
||||
EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> downloadDate(f1).compareTo(downloadDate(f2)) }
|
||||
EpisodeSortOrder.DOWNLOAD_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> downloadDate(f2).compareTo(downloadDate(f1)) }
|
||||
EpisodeSortOrder.VIEWS_LOW_HIGH -> comparator = Comparator { f1: Episode?, f2: Episode? -> viewCount(f1).compareTo(viewCount(f2)) }
|
||||
EpisodeSortOrder.VIEWS_HIGH_LOW -> comparator = Comparator { f1: Episode?, f2: Episode? -> viewCount(f2).compareTo(viewCount(f1)) }
|
||||
|
||||
EpisodeSortOrder.FEED_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f1).compareTo(feedTitle(f2)) }
|
||||
EpisodeSortOrder.FEED_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f2).compareTo(feedTitle(f1)) }
|
||||
EpisodeSortOrder.RANDOM, EpisodeSortOrder.RANDOM1 -> permutor = object : Permutor<Episode> {
|
||||
override fun reorder(queue: MutableList<Episode>?) {
|
||||
if (!queue.isNullOrEmpty()) queue.shuffle()
|
||||
}
|
||||
}
|
||||
EpisodeSortOrder.SMART_SHUFFLE_OLD_NEW -> permutor = object : Permutor<Episode> {
|
||||
override fun reorder(queue: MutableList<Episode>?) {
|
||||
if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList<Episode?>, true)
|
||||
}
|
||||
}
|
||||
EpisodeSortOrder.SMART_SHUFFLE_NEW_OLD -> permutor = object : Permutor<Episode> {
|
||||
override fun reorder(queue: MutableList<Episode>?) {
|
||||
if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList<Episode?>, false)
|
||||
}
|
||||
}
|
||||
EpisodeSortOrder.SIZE_SMALL_LARGE -> comparator = Comparator { f1: Episode?, f2: Episode? -> size(f1).compareTo(size(f2)) }
|
||||
EpisodeSortOrder.SIZE_LARGE_SMALL -> comparator = Comparator { f1: Episode?, f2: Episode? -> size(f2).compareTo(size(f1)) }
|
||||
}
|
||||
if (comparator != null) {
|
||||
val comparator2: Comparator<Episode> = comparator
|
||||
permutor = object : Permutor<Episode> {
|
||||
override fun reorder(queue: MutableList<Episode>?) {if (!queue.isNullOrEmpty()) queue.sortWith(comparator2)}
|
||||
}
|
||||
}
|
||||
return permutor!!
|
||||
}
|
||||
|
||||
private fun pubDate(item: Episode?): Date {
|
||||
return if (item == null) Date() else Date(item.pubDate)
|
||||
}
|
||||
|
||||
private fun playDate(item: Episode?): Long {
|
||||
return item?.media?.getLastPlayedTime() ?: 0
|
||||
}
|
||||
|
||||
private fun downloadDate(item: Episode?): Long {
|
||||
return item?.media?.downloadTime ?: 0
|
||||
}
|
||||
|
||||
private fun completeDate(item: Episode?): Date {
|
||||
return item?.media?.playbackCompletionDate ?: Date(0)
|
||||
}
|
||||
|
||||
private fun itemTitle(item: Episode?): String {
|
||||
return (item?.title ?: "").lowercase(Locale.getDefault())
|
||||
}
|
||||
|
||||
private fun duration(item: Episode?): Int {
|
||||
return item?.media?.getDuration() ?: 0
|
||||
}
|
||||
|
||||
private fun size(item: Episode?): Long {
|
||||
return item?.media?.size ?: 0
|
||||
}
|
||||
|
||||
private fun itemLink(item: Episode?): String {
|
||||
return (item?.link ?: "").lowercase(Locale.getDefault())
|
||||
}
|
||||
|
||||
private fun feedTitle(item: Episode?): String {
|
||||
return (item?.feed?.title ?: "").lowercase(Locale.getDefault())
|
||||
}
|
||||
|
||||
private fun viewCount(item: Episode?): Int {
|
||||
return item?.viewCount ?: 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a reordering by pubdate that avoids consecutive episodes from the same feed in the queue.
|
||||
* A listener might want to hear episodes from any given feed in pubdate order, but would
|
||||
* prefer a more balanced ordering that avoids having to listen to clusters of consecutive
|
||||
* episodes from the same feed. This is what "Smart Shuffle" tries to accomplish.
|
||||
* Assume the queue looks like this: `ABCDDEEEEEEEEEE`.
|
||||
* This method first starts with a queue of the final size, where each slot is empty (null).
|
||||
* It takes the podcast with most episodes (`E`) and places the episodes spread out in the queue: `EE_E_EE_E_EE_EE`.
|
||||
* The podcast with the second-most number of episodes (`D`) is then
|
||||
* placed spread-out in the *available* slots: `EE_EDEE_EDEE_EE`.
|
||||
* This continues, until we end up with: `EEBEDEECEDEEAEE`.
|
||||
* Note that episodes aren't strictly ordered in terms of pubdate, but episodes of each feed are.
|
||||
*
|
||||
* @param queue A (modifiable) list of FeedItem elements to be reordered.
|
||||
* @param ascending `true` to use ascending pubdate in the reordering;
|
||||
* `false` for descending.
|
||||
*/
|
||||
private fun smartShuffle(queue: MutableList<Episode?>, ascending: Boolean) {
|
||||
// Divide FeedItems into lists by feed
|
||||
val map: MutableMap<Long, MutableList<Episode>> = HashMap()
|
||||
for (item in queue) {
|
||||
if (item == null) continue
|
||||
val id = item.feedId
|
||||
if (id != null) {
|
||||
if (!map.containsKey(id)) map[id] = ArrayList()
|
||||
map[id]!!.add(item)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort each individual list by PubDate (ascending/descending)
|
||||
val itemComparator: Comparator<Episode> =
|
||||
if (ascending) Comparator { f1: Episode, f2: Episode -> f1.pubDate.compareTo(f2.pubDate) }
|
||||
else Comparator { f1: Episode, f2: Episode -> f2.pubDate.compareTo(f1.pubDate) }
|
||||
|
||||
val feeds: MutableList<List<Episode>> = ArrayList()
|
||||
for ((_, value) in map) {
|
||||
value.sortWith(itemComparator)
|
||||
feeds.add(value)
|
||||
}
|
||||
|
||||
val emptySlots = ArrayList<Int>()
|
||||
for (i in queue.indices) {
|
||||
queue[i] = null
|
||||
emptySlots.add(i)
|
||||
}
|
||||
|
||||
// Starting with the largest feed, place items spread out through the empty slots in the queue
|
||||
feeds.sortWith { f1: List<Episode>, f2: List<Episode> -> f2.size.compareTo(f1.size) }
|
||||
for (feedItems in feeds) {
|
||||
val spread = emptySlots.size.toDouble() / (feedItems.size + 1)
|
||||
val emptySlotIterator = emptySlots.iterator()
|
||||
var skipped = 0
|
||||
var placed = 0
|
||||
while (emptySlotIterator.hasNext()) {
|
||||
val nextEmptySlot = emptySlotIterator.next()
|
||||
skipped++
|
||||
if (skipped >= spread * (placed + 1)) {
|
||||
if (queue[nextEmptySlot] != null) throw RuntimeException("Slot to be placed in not empty")
|
||||
queue[nextEmptySlot] = feedItems[placed]
|
||||
emptySlotIterator.remove()
|
||||
placed++
|
||||
if (placed == feedItems.size) break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for passing around list permutor method. This is used for cases where a simple comparator
|
||||
* won't work (e.g. Random, Smart Shuffle, etc)
|
||||
* @param <E> the type of elements in the list
|
||||
</E> */
|
||||
interface Permutor<E> {
|
||||
/**
|
||||
* Reorders the specified list.
|
||||
* @param queue A (modifiable) list of elements to be reordered
|
||||
*/
|
||||
fun reorder(queue: MutableList<E>?)
|
||||
}
|
||||
}
|
|
@ -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_) }
|
||||
|
|
|
@ -54,11 +54,7 @@ class OpmlImportActivity : AppCompatActivity() {
|
|||
private val titleList: List<String>
|
||||
get() {
|
||||
val result: MutableList<String> = ArrayList()
|
||||
if (!readElements.isNullOrEmpty()) {
|
||||
for (element in readElements!!) {
|
||||
if (element.text != null) result.add(element.text!!)
|
||||
}
|
||||
}
|
||||
if (!readElements.isNullOrEmpty()) for (element in readElements!!) if (element.text != null) result.add(element.text!!)
|
||||
return result
|
||||
}
|
||||
|
||||
|
@ -85,9 +81,7 @@ class OpmlImportActivity : AppCompatActivity() {
|
|||
binding.feedlist.onItemClickListener = OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, _: Long ->
|
||||
val checked = binding.feedlist.checkedItemPositions
|
||||
var checkedCount = 0
|
||||
for (i in 0 until checked.size()) {
|
||||
if (checked.valueAt(i)) checkedCount++
|
||||
}
|
||||
for (i in 0 until checked.size()) if (checked.valueAt(i)) checkedCount++
|
||||
if (listAdapter != null) {
|
||||
if (checkedCount == listAdapter!!.count) {
|
||||
selectAll.isVisible = false
|
||||
|
@ -104,10 +98,10 @@ class OpmlImportActivity : AppCompatActivity() {
|
|||
}
|
||||
binding.butConfirm.setOnClickListener {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
val checked = binding.feedlist.checkedItemPositions
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
val checked = binding.feedlist.checkedItemPositions
|
||||
for (i in 0 until checked.size()) {
|
||||
if (!checked.valueAt(i)) continue
|
||||
|
||||
|
@ -144,10 +138,7 @@ class OpmlImportActivity : AppCompatActivity() {
|
|||
|
||||
private fun importUri(uri: Uri?) {
|
||||
if (uri == null) {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.opml_import_error_no_file)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
MaterialAlertDialogBuilder(this).setMessage(R.string.opml_import_error_no_file).setPositiveButton(android.R.string.ok, null).show()
|
||||
return
|
||||
}
|
||||
this.uri = uri
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ import ac.mdiq.podcini.storage.model.*
|
|||
import ac.mdiq.podcini.storage.model.Feed.Companion.MAX_SYNTHETIC_ID
|
||||
import ac.mdiq.podcini.storage.model.Feed.Companion.newId
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
|
||||
import ac.mdiq.podcini.storage.database.Episodes.hasAlmostEnded
|
||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||
import ac.mdiq.podcini.ui.actions.EpisodeActionButton
|
||||
import ac.mdiq.podcini.ui.actions.NullActionButton
|
||||
|
@ -47,6 +47,7 @@ import ac.mdiq.podcini.util.FlowEvent
|
|||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.MiscFormatter.formatDateTimeFlex
|
||||
import ac.mdiq.podcini.util.MiscFormatter.formatNumber
|
||||
import ac.mdiq.podcini.util.MiscFormatter.localDateTimeString
|
||||
import ac.mdiq.vista.extractor.Vista
|
||||
import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeServiceURL
|
||||
import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeURL
|
||||
|
@ -395,7 +396,7 @@ fun EraseEpisodesDialog(selected: List<Episode>, feed: Feed?, onDismissRequest:
|
|||
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
if (feed == null || feed.id > MAX_SYNTHETIC_ID) Text(stringResource(R.string.not_erase_message), modifier = Modifier.padding(10.dp))
|
||||
if (feed == null || !feed.isSynthetic()) Text(stringResource(R.string.not_erase_message), modifier = Modifier.padding(10.dp))
|
||||
else Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text(message + ": ${selected.size}")
|
||||
Text(stringResource(R.string.feed_delete_reason_msg))
|
||||
|
@ -410,8 +411,8 @@ fun EraseEpisodesDialog(selected: List<Episode>, feed: Feed?, onDismissRequest:
|
|||
val sLog = SubscriptionLog(e.id, e.title?:"", e.media?.downloadUrl?:"", e.link?:"", SubscriptionLog.Type.Media.name)
|
||||
upsert(sLog) {
|
||||
it.rating = e.rating
|
||||
it.comment = e.comment
|
||||
it.comment += "\nReason to remove:\n" + textState.text
|
||||
it.comment = if (e.comment.isBlank()) "" else (e.comment + "\n")
|
||||
it.comment += localDateTimeString() + "\nReason to remove:\n" + textState.text
|
||||
it.cancelDate = Date().time
|
||||
}
|
||||
}
|
||||
|
@ -594,7 +595,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
|
|||
Text(stringResource(id = R.string.reserve_episodes_label))
|
||||
}
|
||||
}
|
||||
if (feed != null && feed.id <= MAX_SYNTHETIC_ID) {
|
||||
if (feed != null && feed.isSynthetic()) {
|
||||
options.add {
|
||||
Row(modifier = Modifier.padding(horizontal = 16.dp).clickable {
|
||||
isExpanded = false
|
||||
|
@ -719,8 +720,11 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
|
|||
modifier = Modifier.width(imageWidth).height(imageHeight)
|
||||
.clickable(onClick = {
|
||||
Logd(TAG, "icon clicked!")
|
||||
if (selectMode) toggleSelected(vm)
|
||||
else if (vm.episode.feed != null) activity.loadChildFragment(FeedInfoFragment.newInstance(vm.episode.feed!!))
|
||||
when {
|
||||
selectMode -> toggleSelected(vm)
|
||||
vm.episode.feed != null && vm.episode.feed?.isSynthetic() != true -> activity.loadChildFragment(FeedInfoFragment.newInstance(vm.episode.feed!!))
|
||||
else -> activity.loadChildFragment(EpisodeInfoFragment.newInstance(vm.episode))
|
||||
}
|
||||
}))
|
||||
Box(Modifier.weight(1f).height(imageHeight)) {
|
||||
TitleColumn(vm, index, modifier = Modifier.fillMaxWidth())
|
||||
|
@ -735,9 +739,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
|
|||
Logd(TAG, "LaunchedEffect $index isPlayingState: ${vms[index].isPlayingState} ${vm.episode.playState} ${vms[index].episode.title}")
|
||||
Logd(TAG, "LaunchedEffect $index downloadState: ${vms[index].downloadState} ${vm.episode.media?.downloaded} ${vm.dlPercent}")
|
||||
vm.actionButton = vm.actionButton.forItem(vm.episode)
|
||||
if (vm.actionButton.getLabel() != actionButton.getLabel()) {
|
||||
actionButton = vm.actionButton
|
||||
}
|
||||
if (vm.actionButton.getLabel() != actionButton.getLabel()) actionButton = vm.actionButton
|
||||
}
|
||||
} else {
|
||||
LaunchedEffect(Unit) {
|
||||
|
@ -1013,18 +1015,16 @@ fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: Mutable
|
|||
val selectedList = remember { MutableList(item.values.size) { mutableStateOf(false)} }
|
||||
var expandRow by remember { mutableStateOf(false) }
|
||||
Row {
|
||||
Text(stringResource(item.nameRes) + ".. :", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineSmall, color = textColor, modifier = Modifier.clickable {
|
||||
expandRow = !expandRow
|
||||
})
|
||||
Text(stringResource(item.nameRes) + ".. :", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineSmall, color = textColor,
|
||||
modifier = Modifier.clickable { expandRow = !expandRow })
|
||||
var lowerSelected by remember { mutableStateOf(false) }
|
||||
var higherSelected by remember { mutableStateOf(false) }
|
||||
Spacer(Modifier.weight(1f))
|
||||
if (expandRow) Text("<<<", color = if (lowerSelected) Color.Green else buttonColor, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.clickable {
|
||||
val hIndex = selectedList.indexOfLast { it.value }
|
||||
if (hIndex < 0) return@clickable
|
||||
if (!lowerSelected) {
|
||||
for (i in 0..hIndex) selectedList[i].value = true
|
||||
} else {
|
||||
if (!lowerSelected) for (i in 0..hIndex) selectedList[i].value = true
|
||||
else {
|
||||
for (i in 0..hIndex) selectedList[i].value = false
|
||||
selectedList[hIndex].value = true
|
||||
}
|
||||
|
@ -1067,9 +1067,7 @@ fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: Mutable
|
|||
if (expandRow) NonlazyGrid(columns = 3, itemCount = item.values.size) { index ->
|
||||
if (selectNone) selectedList[index].value = false
|
||||
LaunchedEffect(Unit) {
|
||||
if (filter != null) {
|
||||
if (item.values[index].filterId in filter.properties) selectedList[index].value = true
|
||||
}
|
||||
if (filter != null && item.values[index].filterId in filter.properties) selectedList[index].value = true
|
||||
}
|
||||
OutlinedButton(
|
||||
modifier = Modifier.padding(0.dp).heightIn(min = 20.dp).widthIn(min = 20.dp).wrapContentWidth(),
|
||||
|
|
|
@ -28,6 +28,7 @@ import ac.mdiq.podcini.util.EventFlow
|
|||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.MiscFormatter
|
||||
import ac.mdiq.podcini.util.MiscFormatter.localDateTimeString
|
||||
import android.util.Log
|
||||
import android.view.Gravity
|
||||
import androidx.compose.foundation.*
|
||||
|
@ -115,12 +116,12 @@ fun RemoveFeedDialog(feeds: List<Feed>, onDismissRequest: () -> Unit, callback:
|
|||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
for (f in feeds) {
|
||||
if (f.id > MAX_SYNTHETIC_ID) {
|
||||
if (!f.isSynthetic()) {
|
||||
val sLog = SubscriptionLog(f.id, f.title ?: "", f.downloadUrl ?: "", f.link ?: "", SubscriptionLog.Type.Feed.name)
|
||||
upsert(sLog) {
|
||||
it.rating = f.rating
|
||||
it.comment = f.comment
|
||||
it.comment += "\nReason to remove:\n" + textState.text
|
||||
it.comment = if (f.comment.isBlank()) "" else (f.comment + "\n")
|
||||
it.comment += localDateTimeString() + "\nReason to remove:\n" + textState.text
|
||||
it.cancelDate = Date().time
|
||||
}
|
||||
} else {
|
||||
|
@ -128,8 +129,8 @@ fun RemoveFeedDialog(feeds: List<Feed>, onDismissRequest: () -> Unit, callback:
|
|||
val sLog = SubscriptionLog(e.id, e.title ?: "", e.media?.downloadUrl ?: "", e.link ?: "", SubscriptionLog.Type.Media.name)
|
||||
upsert(sLog) {
|
||||
it.rating = e.rating
|
||||
it.comment = e.comment
|
||||
it.comment += "\nReason to remove:\n" + textState.text
|
||||
it.comment = if (e.comment.isBlank()) "" else (e.comment + "\n")
|
||||
it.comment += localDateTimeString() + "\nReason to remove:\n" + textState.text
|
||||
it.cancelDate = Date().time
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,6 +5,7 @@ import ac.mdiq.podcini.databinding.ComposeFragmentBinding
|
|||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.storage.database.Episodes
|
||||
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||
|
@ -14,7 +15,6 @@ import ac.mdiq.podcini.storage.model.Episode
|
|||
import ac.mdiq.podcini.storage.model.EpisodeFilter
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||
import ac.mdiq.podcini.ui.actions.DeleteActionButton
|
||||
import ac.mdiq.podcini.ui.actions.SwipeAction
|
||||
import ac.mdiq.podcini.ui.actions.SwipeActions
|
||||
|
@ -240,7 +240,7 @@ import java.util.*
|
|||
return // Refreshed anyway
|
||||
}
|
||||
// for (downloadUrl in event.urls) {
|
||||
// val pos = EpisodeUtil.indexOfItemWithDownloadUrl(episodes.toList(), downloadUrl)
|
||||
// val pos = Episodes.indexOfItemWithDownloadUrl(episodes.toList(), downloadUrl)
|
||||
// if (pos >= 0) adapter.notifyItemChangedCompat(pos)
|
||||
// }
|
||||
}
|
||||
|
@ -295,7 +295,7 @@ import java.util.*
|
|||
val size: Int = event.episodes.size
|
||||
while (i < size) {
|
||||
val item: Episode = event.episodes[i++]
|
||||
val pos = EpisodeUtil.indexOfItemWithId(episodes, item.id)
|
||||
val pos = Episodes.indexOfItemWithId(episodes, item.id)
|
||||
if (pos >= 0) {
|
||||
episodes.removeAt(pos)
|
||||
vms.removeAt(pos)
|
||||
|
@ -317,7 +317,7 @@ import java.util.*
|
|||
val size: Int = event.episodes.size
|
||||
while (i < size) {
|
||||
val item: Episode = event.episodes[i++]
|
||||
val pos = EpisodeUtil.indexOfItemWithId(episodes, item.id)
|
||||
val pos = Episodes.indexOfItemWithId(episodes, item.id)
|
||||
if (pos >= 0) {
|
||||
episodes.removeAt(pos)
|
||||
vms.removeAt(pos)
|
||||
|
@ -356,7 +356,7 @@ import java.util.*
|
|||
} else {
|
||||
val mediaUrls: MutableList<String> = ArrayList()
|
||||
for (url in runningDownloads) {
|
||||
if (EpisodeUtil.indexOfItemWithDownloadUrl(downloadedItems, url) != -1) continue
|
||||
if (Episodes.indexOfItemWithDownloadUrl(downloadedItems, url) != -1) continue
|
||||
mediaUrls.add(url)
|
||||
}
|
||||
val currentDownloads = getEpisdesWithUrl(mediaUrls).toMutableList()
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 ""
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/choose_data_folder_dialog"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/recyclerView" />
|
||||
|
||||
</LinearLayout>
|
|
@ -1,46 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/choose_data_folder_dialog_entry"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radio_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:padding="4dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_toEndOf="@+id/radio_button"
|
||||
android:layout_toRightOf="@+id/radio_button"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/path"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="/storage/sdcard0" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/size"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="2 GB" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/used_space"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
|
@ -352,7 +352,7 @@
|
|||
<string name="downloads_pref">تنزيلات</string>
|
||||
<string name="downloads_pref_sum">الفترة الزمنية للتحديثات، بيانات الجوال، التنزيل التلقائي، الحذف التلقائي</string>
|
||||
<string name="feed_refresh_title">حدث البودكاستات</string>
|
||||
<string name="feed_refresh_sum">حدد الفترة الزمنية التي سيقوم Podcini فيها تلقائيا بالبحث عن حلقات جديدة </string>
|
||||
|
||||
<string name="feed_refresh_never">أبداً</string>
|
||||
<string name="feed_every_hour">كل ساعة</string>
|
||||
<string name="feed_every_2_hours">كل 2 ساعتين</string>
|
||||
|
|
|
@ -328,7 +328,7 @@
|
|||
<string name="downloads_pref">Baixades</string>
|
||||
<string name="downloads_pref_sum">Intèrval d\'actualització, dades mòbils, baixades automàtiques, esborrat automàtic</string>
|
||||
<string name="feed_refresh_title">Actualitza els pòdcasts</string>
|
||||
<string name="feed_refresh_sum">Especifiqueu un interval segons el qual Podcini cercarà nous episodis automàticament</string>
|
||||
|
||||
<string name="feed_refresh_never">Mai</string>
|
||||
<string name="feed_every_hour">Cada hora</string>
|
||||
<string name="feed_every_2_hours">Cada 2 hores</string>
|
||||
|
|
|
@ -357,7 +357,7 @@
|
|||
<string name="downloads_pref">Stahování</string>
|
||||
<string name="downloads_pref_sum">Interval aktualizace, Mobilní data, Automatické stahování, Automatické mazání</string>
|
||||
<string name="feed_refresh_title">Aktualizovat podcasty</string>
|
||||
<string name="feed_refresh_sum">Zadejte interval, ve kterém bude Podcini automaticky vyhledávat nové epizody</string>
|
||||
|
||||
<string name="feed_refresh_never">Nikdy</string>
|
||||
<string name="feed_every_hour">Každou hodinu</string>
|
||||
<string name="feed_every_2_hours">Každé 2 hodiny</string>
|
||||
|
|
|
@ -342,7 +342,7 @@
|
|||
<string name="downloads_pref">Overførsler</string>
|
||||
<string name="downloads_pref_sum">Opdateringsinterval, Mobildata, Automatisk overførsel, Automatisk sletning</string>
|
||||
<string name="feed_refresh_title">Opdater podcasts</string>
|
||||
<string name="feed_refresh_sum">Angiv et interval, hvormed Podcini automatisk søger efter nye afsnit.</string>
|
||||
|
||||
<string name="feed_refresh_never">Aldrig</string>
|
||||
<string name="feed_every_hour">Hver time</string>
|
||||
<string name="feed_every_2_hours">Hver 2. time</string>
|
||||
|
|
|
@ -346,7 +346,7 @@
|
|||
<string name="downloads_pref">Downloads</string>
|
||||
<string name="downloads_pref_sum">Aktualisierungsintervall, Mobile Daten, Automatischer Download, Automatisches Löschen</string>
|
||||
<string name="feed_refresh_title">Podcasts aktualisieren</string>
|
||||
<string name="feed_refresh_sum">Ein Intervall festlegen, in dem Podcini automatisch nach neuen Episoden sucht</string>
|
||||
|
||||
<string name="feed_refresh_never">Niemals</string>
|
||||
<string name="feed_every_hour">Jede Stunde</string>
|
||||
<string name="feed_every_2_hours">Alle 2 Stunden</string>
|
||||
|
|
|
@ -348,7 +348,7 @@
|
|||
<string name="downloads_pref">Descargas</string>
|
||||
<string name="downloads_pref_sum">Intervalo de actualización, datos móviles, descargas automáticas, borrado automático</string>
|
||||
<string name="feed_refresh_title">Actualizar pódcasts</string>
|
||||
<string name="feed_refresh_sum">Especifique un intervalo para buscar nuevos episodios automáticamente</string>
|
||||
|
||||
<string name="feed_refresh_never">Nunca</string>
|
||||
<string name="feed_every_hour">Cada hora</string>
|
||||
<string name="feed_every_2_hours">Cada 2 horas</string>
|
||||
|
|
|
@ -329,7 +329,7 @@
|
|||
<string name="downloads_pref">بارگیریها</string>
|
||||
<string name="downloads_pref_sum">دورهٔ بارگیری، دادهٔ همراه، بارگیری خودکار، حذف خودکار</string>
|
||||
<string name="feed_refresh_title">پادکست ها را بازخوانی کنید</string>
|
||||
<string name="feed_refresh_sum">مشخّص کردن دورهای برای گشتن خودکار آنتناپاد به دنبال قسمتهای جدید</string>
|
||||
|
||||
<string name="feed_refresh_never">هرگز</string>
|
||||
<string name="feed_every_hour">هر ساعت</string>
|
||||
<string name="feed_every_2_hours">هر ۲ ساعت</string>
|
||||
|
|
|
@ -319,7 +319,7 @@
|
|||
<string name="playback_pref_sum">Kuulokkeiden ohjaimet, ohitusaikavälit, jono</string>
|
||||
<string name="downloads_pref">Lataukset</string>
|
||||
<string name="feed_refresh_title">Päivitä podcastit</string>
|
||||
<string name="feed_refresh_sum">Määritä aikaväli tai tietty aika etsiä uusia jaksoja automaattisesti</string>
|
||||
|
||||
<string name="feed_refresh_never">Ei koskaan</string>
|
||||
<string name="feed_every_hour">Tunnin välein</string>
|
||||
<string name="feed_every_2_hours">2 tunnin välein</string>
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
<string name="downloads_pref">Téléchargements</string>
|
||||
<string name="downloads_pref_sum">Fréquence de mise à jour, utilisation de la connexion mobile, téléchargements et suppressions automatiques</string>
|
||||
<string name="feed_refresh_title">Actualiser les podcasts</string>
|
||||
<string name="feed_refresh_sum">Choisir la fréquence de mise à jour automatique des épisodes par Podcini</string>
|
||||
|
||||
<string name="feed_refresh_never">Jamais</string>
|
||||
<string name="feed_every_hour">Toutes les heures</string>
|
||||
<string name="feed_every_2_hours">Toutes les 2 heures</string>
|
||||
|
|
|
@ -341,7 +341,7 @@
|
|||
<string name="downloads_pref">Descargas</string>
|
||||
<string name="downloads_pref_sum">Intervalo de actualización, Datos móbiles, Descarga automática, Eliminación automática</string>
|
||||
<string name="feed_refresh_title">Actualizar podcast</string>
|
||||
<string name="feed_refresh_sum">Indicar cada canto tempo debe Podcini comprobar se hai episodios novos</string>
|
||||
|
||||
<string name="feed_refresh_never">Nunca</string>
|
||||
<string name="feed_every_hour">Cada hora</string>
|
||||
<string name="feed_every_2_hours">Cada 2 horas</string>
|
||||
|
|
|
@ -266,7 +266,7 @@
|
|||
<string name="playback_pref_sum">Kontrol headphone, Jangka waktu lewati, Antrean</string>
|
||||
<string name="downloads_pref_sum">Waktu pembaharuan, Jaringan seluler, Unduh otomatis, Hapus otomatis</string>
|
||||
<string name="feed_refresh_title">Segarkan podcast</string>
|
||||
<string name="feed_refresh_sum">Tentukan jangka waktu di mana Podcini secara otomatis mencari episode baru</string>
|
||||
|
||||
<string name="feed_refresh_never">Jangan pernah</string>
|
||||
<string name="feed_every_hour">Setiap jam</string>
|
||||
<string name="feed_every_2_hours">Tiap 2 jam</string>
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
<string name="downloads_pref">Download</string>
|
||||
<string name="downloads_pref_sum">Intervallo di aggiornamento, rete mobile, download ed eliminazione automatici</string>
|
||||
<string name="feed_refresh_title">Aggiorna i podcast</string>
|
||||
<string name="feed_refresh_sum">Definisce l\'intervallo di ricerca automatica dei nuovi episodi.</string>
|
||||
|
||||
<string name="feed_refresh_never">Mai</string>
|
||||
<string name="feed_every_hour">Ogni ora</string>
|
||||
<string name="feed_every_2_hours">Ogni 2 ore</string>
|
||||
|
|
|
@ -353,7 +353,7 @@
|
|||
<string name="downloads_pref">הורדות</string>
|
||||
<string name="downloads_pref_sum">הפרש בין עדכונים, חיבור סלולרי, הורדה אוטומטית, מחיקה אוטומטית</string>
|
||||
<string name="feed_refresh_title">רענון פודקאסטים</string>
|
||||
<string name="feed_refresh_sum">נא לציין הפרש או מועד מסוים לחיפוש פרקים אוטומטית על ידי אנטנה־פּוֹד</string>
|
||||
|
||||
<string name="feed_refresh_never">אף פעם לא</string>
|
||||
<string name="feed_every_hour">כל שעה</string>
|
||||
<string name="feed_every_2_hours">כל שעתיים</string>
|
||||
|
|
|
@ -317,7 +317,7 @@
|
|||
<string name="downloads_pref">다운로드</string>
|
||||
<string name="downloads_pref_sum">업데이트 주기, 휴대전화 망 데이터, 자동 다운로드, 자동 삭제</string>
|
||||
<string name="feed_refresh_title">팟캐스트 새로고침</string>
|
||||
<string name="feed_refresh_sum">새로운 에피소드가 있는지 자동으로 확인할 주기를 지정합니다</string>
|
||||
|
||||
<string name="feed_refresh_never">안 함</string>
|
||||
<string name="feed_every_hour">매 시간</string>
|
||||
<string name="feed_every_2_hours">매 2시간</string>
|
||||
|
|
|
@ -321,7 +321,7 @@
|
|||
<string name="downloads_pref">Nedlastinger</string>
|
||||
<string name="downloads_pref_sum">Oppdaterings-intervall, Mobil data, Automatisk nedlasting, Automatisk sletting</string>
|
||||
<string name="feed_refresh_title">Oppdater podkaster</string>
|
||||
<string name="feed_refresh_sum">Spesifiser en intervall eller et spesifikt tidspunkt når det skal sjekkes automatisk for nye episoder</string>
|
||||
|
||||
<string name="feed_refresh_never">Aldri</string>
|
||||
<string name="feed_every_hour">Hver time</string>
|
||||
<string name="feed_every_2_hours">Hver andre time</string>
|
||||
|
|
|
@ -330,7 +330,7 @@
|
|||
<string name="downloads_pref">Downloads</string>
|
||||
<string name="downloads_pref_sum">Intervalo de atualização, Dados móveis, Download automático, Exclusão automática</string>
|
||||
<string name="feed_refresh_title">Atualizar podcasts</string>
|
||||
<string name="feed_refresh_sum">Especifique um intervalo em que o Podcini procura novos episódios automaticamente</string>
|
||||
|
||||
<string name="feed_refresh_never">Nunca</string>
|
||||
<string name="feed_every_hour">A cada hora</string>
|
||||
<string name="feed_every_2_hours">A cada 2 horas</string>
|
||||
|
|
|
@ -348,7 +348,7 @@
|
|||
<string name="downloads_pref">Descargas</string>
|
||||
<string name="downloads_pref_sum">Intervalo de atualização, dados móveis, descargas e eliminação automática</string>
|
||||
<string name="feed_refresh_title">Recarregar podcasts</string>
|
||||
<string name="feed_refresh_sum">Defina um intervalo para que Podcini procure episódios automaticamente</string>
|
||||
|
||||
<string name="feed_refresh_never">Nunca</string>
|
||||
<string name="feed_every_hour">A cada 1 hora</string>
|
||||
<string name="feed_every_2_hours">A cada 2 horas</string>
|
||||
|
|
|
@ -347,7 +347,7 @@
|
|||
<string name="downloads_pref">Descărcări</string>
|
||||
<string name="downloads_pref_sum">Interval actualizare, Date mobile, Descărcări automate, Ștergeri automate</string>
|
||||
<string name="feed_refresh_title">Reîmprospătează podcasturile</string>
|
||||
<string name="feed_refresh_sum">Specifică un anumit interval în care Podcini caută episoade noi automat</string>
|
||||
|
||||
<string name="feed_refresh_never">Nicio data</string>
|
||||
<string name="feed_every_hour">La fiecare oră</string>
|
||||
<string name="feed_every_2_hours">La fiecare 2 ore</string>
|
||||
|
|
|
@ -333,7 +333,7 @@
|
|||
<string name="downloads_pref">Загрузки</string>
|
||||
<string name="downloads_pref_sum">Интервал обновления, мобильная сеть, автоматизация</string>
|
||||
<string name="feed_refresh_title">Обновить подкасты</string>
|
||||
<string name="feed_refresh_sum">Укажите интервал с которым Podcini будет автоматически искать обновления выпусков</string>
|
||||
|
||||
<string name="feed_refresh_never">Никогда</string>
|
||||
<string name="feed_every_hour">Каждый час</string>
|
||||
<string name="feed_every_2_hours">Каждые 2 часа</string>
|
||||
|
|
|
@ -352,7 +352,7 @@
|
|||
<string name="downloads_pref">Sťahovanie</string>
|
||||
<string name="downloads_pref_sum">Interval aktualizácie, Mobilné dáta, Automatické sťahovanie, Automatické mazanie</string>
|
||||
<string name="feed_refresh_title">Aktualizovať podcasty</string>
|
||||
<string name="feed_refresh_sum">Zvoliť interval alebo čas v ktorom Podcini automaticky hľadá nové epizódy</string>
|
||||
|
||||
<string name="feed_refresh_never">Nikdy</string>
|
||||
<string name="feed_every_hour">Každú hodinu</string>
|
||||
<string name="feed_every_2_hours">Každé 2 hodiny</string>
|
||||
|
|
|
@ -341,7 +341,7 @@
|
|||
<string name="downloads_pref">Nedladdningar</string>
|
||||
<string name="downloads_pref_sum">Uppdateringsintervall, Mobildata, Automatisk nedladdning, Automatisk radering</string>
|
||||
<string name="feed_refresh_title">Uppdatera podcasts</string>
|
||||
<string name="feed_refresh_sum">Ange ett intervall med vilket Podcini letar efter nya episoder automatiskt</string>
|
||||
|
||||
<string name="feed_refresh_never">Aldrig</string>
|
||||
<string name="feed_every_hour">Varje timme</string>
|
||||
<string name="feed_every_2_hours">Varje 2 timmar</string>
|
||||
|
|
|
@ -328,7 +328,7 @@
|
|||
<string name="downloads_pref">İndirilenler</string>
|
||||
<string name="downloads_pref_sum">Güncelleme aralığı, Mobil veri, Otomatik indirme, Otomatik silme</string>
|
||||
<string name="feed_refresh_title">Podcastleri güncelle</string>
|
||||
<string name="feed_refresh_sum">Podcini\'un yeni bölümleri otomatik olarak arayacağı zaman aralığını belirtin</string>
|
||||
|
||||
<string name="feed_refresh_never">Hiçbir zaman</string>
|
||||
<string name="feed_every_hour">Saatte bir</string>
|
||||
<string name="feed_every_2_hours">2 saatte bir</string>
|
||||
|
|
|
@ -353,7 +353,7 @@
|
|||
<string name="downloads_pref">Завантаження</string>
|
||||
<string name="downloads_pref_sum">Інтервал оновлення, Мобільні дані, Автоматичне завантаження, Автоматичне видалення</string>
|
||||
<string name="feed_refresh_title">Оновити подкасти</string>
|
||||
<string name="feed_refresh_sum">Вкажіть інтервал, з яким Podcini автоматично шукатиме нові епізоди</string>
|
||||
|
||||
<string name="feed_refresh_never">Ніколи</string>
|
||||
<string name="feed_every_hour">Щогодини</string>
|
||||
<string name="feed_every_2_hours">Кожні 2 години</string>
|
||||
|
|
|
@ -341,7 +341,7 @@
|
|||
<string name="downloads_pref">下载</string>
|
||||
<string name="downloads_pref_sum">更新间隔、移动数据、自动下载、自动删除</string>
|
||||
<string name="feed_refresh_title">刷新播客</string>
|
||||
<string name="feed_refresh_sum">指定 Podcini 自动查找新节目的间隔</string>
|
||||
|
||||
<string name="feed_refresh_never">永不</string>
|
||||
<string name="feed_every_hour">每 1 小时</string>
|
||||
<string name="feed_every_2_hours">每 2 小时</string>
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="feed_refresh_interval_entries">
|
||||
<item>@string/feed_refresh_never</item>
|
||||
<item>@string/feed_every_hour</item>
|
||||
<item>@string/feed_every_2_hours</item>
|
||||
<item>@string/feed_every_4_hours</item>
|
||||
<item>@string/feed_every_8_hours</item>
|
||||
<item>@string/feed_every_12_hours</item>
|
||||
<item>@string/feed_every_24_hours</item>
|
||||
<item>@string/feed_every_72_hours</item>
|
||||
</string-array>
|
||||
<string-array name="feed_refresh_interval_values">
|
||||
<item>0</item>
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
<item>4</item>
|
||||
<item>8</item>
|
||||
<item>12</item>
|
||||
<item>24</item>
|
||||
<item>72</item>
|
||||
</string-array>
|
||||
<!-- <string-array name="feed_refresh_interval_entries">-->
|
||||
<!-- <item>@string/feed_refresh_never</item>-->
|
||||
<!-- <item>@string/feed_every_hour</item>-->
|
||||
<!-- <item>@string/feed_every_2_hours</item>-->
|
||||
<!-- <item>@string/feed_every_4_hours</item>-->
|
||||
<!-- <item>@string/feed_every_8_hours</item>-->
|
||||
<!-- <item>@string/feed_every_12_hours</item>-->
|
||||
<!-- <item>@string/feed_every_24_hours</item>-->
|
||||
<!-- <item>@string/feed_every_72_hours</item>-->
|
||||
<!-- </string-array>-->
|
||||
<!-- <string-array name="feed_refresh_interval_values">-->
|
||||
<!-- <item>0</item>-->
|
||||
<!-- <item>1</item>-->
|
||||
<!-- <item>2</item>-->
|
||||
<!-- <item>4</item>-->
|
||||
<!-- <item>8</item>-->
|
||||
<!-- <item>12</item>-->
|
||||
<!-- <item>24</item>-->
|
||||
<!-- <item>72</item>-->
|
||||
<!-- </string-array>-->
|
||||
|
||||
<!-- <string-array name="smart_mark_as_played_values">-->
|
||||
<!-- <item>0</item>-->
|
||||
|
@ -59,26 +59,25 @@
|
|||
<item>-1</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="mobile_update_entries">
|
||||
<item>@string/pref_mobileUpdate_refresh</item>
|
||||
<item>@string/pref_mobileUpdate_episode_download</item>
|
||||
<item>@string/pref_mobileUpdate_auto_download</item>
|
||||
<item>@string/pref_mobileUpdate_streaming</item>
|
||||
<item>@string/pref_mobileUpdate_images</item>
|
||||
<item>@string/synchronization_pref</item>
|
||||
</string-array>
|
||||
<string-array name="mobile_update_values">
|
||||
<item>feed_refresh</item>
|
||||
<item>episode_download</item>
|
||||
<item>auto_download</item>
|
||||
<item>streaming</item>
|
||||
<item>images</item>
|
||||
<item>sync</item>
|
||||
</string-array>
|
||||
<!-- <string-array name="mobile_update_entries">-->
|
||||
<!-- <item>@string/pref_mobileUpdate_refresh</item>-->
|
||||
<!-- <item>@string/pref_mobileUpdate_episode_download</item>-->
|
||||
<!-- <item>@string/pref_mobileUpdate_auto_download</item>-->
|
||||
<!-- <item>@string/pref_mobileUpdate_streaming</item>-->
|
||||
<!-- <item>@string/pref_mobileUpdate_images</item>-->
|
||||
<!-- <item>@string/synchronization_pref</item>-->
|
||||
<!-- </string-array>-->
|
||||
<!-- <string-array name="mobile_update_values">-->
|
||||
<!-- <item>feed_refresh</item>-->
|
||||
<!-- <item>episode_download</item>-->
|
||||
<!-- <item>auto_download</item>-->
|
||||
<!-- <item>streaming</item>-->
|
||||
<!-- <item>images</item>-->
|
||||
<!-- <item>sync</item>-->
|
||||
<!-- </string-array>-->
|
||||
|
||||
<string-array name="mobile_update_default_value">
|
||||
<item>images</item>
|
||||
<item>sync</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="episode_cleanup_entries">
|
||||
|
|
|
@ -388,6 +388,7 @@
|
|||
<string name="view_count">View count</string>
|
||||
<string name="date">Date</string>
|
||||
<string name="count">Count</string>
|
||||
<string name="last_comment_date">Comment date</string>
|
||||
<string name="last_played_date">Played date</string>
|
||||
<string name="completed_date">Completed date</string>
|
||||
<string name="duration">Duration</string>
|
||||
|
@ -478,7 +479,7 @@
|
|||
<string name="downloads_pref">Downloads</string>
|
||||
<string name="downloads_pref_sum">Update interval, Mobile data, Automatic download, Automatic deletion</string>
|
||||
<string name="feed_refresh_title">Refresh podcasts</string>
|
||||
<string name="feed_refresh_sum">Specify an interval at which Podcini looks for new episodes automatically</string>
|
||||
<string name="feed_refresh_sum">Specify an interval (in hours, 0 means never) at which Podcini looks for new episodes automatically.</string>
|
||||
<string name="feed_refresh_never">Never</string>
|
||||
<string name="feed_every_hour">Every hour</string>
|
||||
<string name="feed_every_2_hours">Every 2 hours</string>
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:search="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<com.bytehamster.lib.preferencesearch.SearchPreference
|
||||
android:key="searchPreference"
|
||||
search:textHint="@string/preference_search_hint"
|
||||
search:textNoResults="@string/preference_search_no_results"
|
||||
search:textClearHistory="@string/preference_search_clear_history" />
|
||||
|
||||
<Preference
|
||||
android:key="prefScreenInterface"
|
||||
android:title="@string/user_interface_label"
|
||||
android:summary="@string/user_interface_sum"
|
||||
android:icon="@drawable/ic_appearance" />
|
||||
|
||||
<Preference
|
||||
android:key="prefScreenPlayback"
|
||||
android:title="@string/playback_pref"
|
||||
android:summary="@string/playback_pref_sum"
|
||||
android:icon="@drawable/ic_play_24dp" />
|
||||
|
||||
<Preference
|
||||
android:key="prefScreenDownloads"
|
||||
android:title="@string/downloads_pref"
|
||||
android:summary="@string/downloads_pref_sum"
|
||||
android:icon="@drawable/ic_download" />
|
||||
|
||||
<Preference
|
||||
android:key="prefScreenSynchronization"
|
||||
android:title="@string/synchronization_pref"
|
||||
android:summary="@string/synchronization_sum"
|
||||
android:icon="@drawable/ic_cloud" />
|
||||
|
||||
<Preference
|
||||
android:key="prefScreenImportExport"
|
||||
android:title="@string/import_export_pref"
|
||||
android:summary="@string/import_export_summary"
|
||||
android:icon="@drawable/ic_storage" />
|
||||
|
||||
<Preference
|
||||
android:key="notifications"
|
||||
android:title="@string/notification_pref_fragment"
|
||||
android:icon="@drawable/ic_notifications"/>
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:enabled="true"
|
||||
android:key="prefOPMLBackup"
|
||||
android:summary="@string/pref_backup_on_google_sum"
|
||||
android:title="@string/pref_backup_on_google_title"/>
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="project"
|
||||
android:title="@string/project_pref">
|
||||
<Preference
|
||||
android:key="prefDocumentation"
|
||||
android:title="@string/documentation_support"
|
||||
android:icon="@drawable/ic_questionmark" />
|
||||
<Preference
|
||||
android:key="prefViewForum"
|
||||
android:title="@string/visit_user_forum"
|
||||
android:icon="@drawable/ic_chat" />
|
||||
<Preference
|
||||
android:key="prefContribute"
|
||||
android:title="@string/pref_contribute"
|
||||
android:icon="@drawable/ic_contribute" />
|
||||
<Preference
|
||||
android:key="prefSendBugReport"
|
||||
android:title="@string/bug_report_title"
|
||||
android:icon="@drawable/ic_bug" />
|
||||
<Preference
|
||||
android:key="prefAbout"
|
||||
android:title="@string/about_pref"
|
||||
android:icon="@drawable/ic_info" />
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
|
@ -1,62 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:search="http://schemas.android.com/apk/com.bytehamster.lib.preferencesearch">
|
||||
|
||||
<!-- <Preference-->
|
||||
<!-- android:title="@string/choose_data_directory"-->
|
||||
<!-- android:key="prefChooseDataDir"/>-->
|
||||
|
||||
<PreferenceCategory android:title="@string/automation">
|
||||
<ac.mdiq.podcini.preferences.MaterialListPreference
|
||||
android:entryValues="@array/feed_refresh_interval_values"
|
||||
android:entries="@array/feed_refresh_interval_entries"
|
||||
android:key="prefAutoUpdateIntervall"
|
||||
android:title="@string/feed_refresh_title"
|
||||
android:summary="@string/feed_refresh_sum"
|
||||
android:defaultValue="12"/>
|
||||
<Preference
|
||||
android:summary="@string/pref_automatic_download_sum"
|
||||
android:key="prefAutoDownloadSettings"
|
||||
android:title="@string/pref_automatic_download_title"
|
||||
search:ignore="true" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:enabled="true"
|
||||
android:key="prefAutoDelete"
|
||||
android:summary="@string/pref_auto_delete_sum"
|
||||
android:title="@string/pref_auto_delete_title"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:enabled="true"
|
||||
android:key="prefAutoDeleteLocal"
|
||||
android:summary="@string/pref_auto_local_delete_sum"
|
||||
android:title="@string/pref_auto_local_delete_title"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:enabled="true"
|
||||
android:key="prefFavoriteKeepsEpisode"
|
||||
android:summary="@string/pref_keeps_important_episodes_sum"
|
||||
android:title="@string/pref_keeps_important_episodes_title"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:enabled="true"
|
||||
android:key="prefDeleteRemovesFromQueue"
|
||||
android:summary="@string/pref_delete_removes_from_queue_sum"
|
||||
android:title="@string/pref_delete_removes_from_queue_title"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/download_pref_details">
|
||||
<ac.mdiq.podcini.preferences.MaterialMultiSelectListPreference
|
||||
android:defaultValue="@array/mobile_update_default_value"
|
||||
android:entries="@array/mobile_update_entries"
|
||||
android:entryValues="@array/mobile_update_values"
|
||||
android:key="prefMobileUpdateTypes"
|
||||
android:summary="@string/pref_mobileUpdate_sum"
|
||||
android:title="@string/pref_metered_network_title"/>
|
||||
<Preference
|
||||
android:key="prefProxy"
|
||||
android:summary="@string/pref_proxy_sum"
|
||||
android:title="@string/pref_proxy_title"/>
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
11
changelog.md
11
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
|
||||
|
|
Loading…
Reference in New Issue