diff --git a/README.md b/README.md index 39a54479..44699017 100644 --- a/README.md +++ b/README.md @@ -73,15 +73,15 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c * operations are only on the selected (single or multiple) * List info is shown in Queue and Downloads views * Left and right swipe actions on lists now have telltales and can be configured on the spot -* Played episodes have clearer markings +* Played or new episodes have clearer markings * Sort dialog no longer dims the main view +* download date can be used to sort both feeds and episodes +* Subscriptions sorting is now bi-directional based on various explicit measures * in episode list view, if episode has no media, TTS button is shown for fetching transcript (if not exist) and then generating audio file from the transcript. TTS audio files are playable in the same way as local media (with speed setting, pause and rewind/forward) * Long-press filter button in FeedEpisode view enables/disables filters without changing filter settings -* Subscriptions view has various explicit measures for sorting -* subscriptions sorting is now bi-directional * in Subscriptions view, click on cover image of a feed opens the FeedInfo view (not FeedEpisodes view) * History view shows time of last play, and allows filters and sorts -* 5 queues are provided by default: Default queue, and Queues 1-4 +* Multiple queues can be used: 5 queues are provided by default: Default queue, and Queues 1-4 * all queue operations are on the curQueue, which can be set in all episodes list views * on app startup, the most recently updated queue is set to curQueue * queue is circular: if the final item in queue finished, the first item in queue (if exists) will get played @@ -115,6 +115,18 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c * It syncs the play states (position and played) of episodes that exist in both devices (ensure to refresh first) and that have been played (completed or not) * So far, every sync is a full sync, no sync for subscriptions and media files +### Automation + +* auto download algorithm is changed to individual feed based. + * When auto download is enabled in the Settings, feeds to be auto-downloaded need to be separately enabled in the feed settings. + * Each feed also has its own download policy (only new episodes, newest episodes, and oldest episodes. "newest episodes" meaning most recent episodes, new or old) + * Each feed has its own limit (Episode cache) for number of episodes downloaded, this limit rules in combination of the overall limit for the app. + * After auto download run, episodes with New status is changed to Unplayed. + * auto download feed setting dialog is also changed: + * there are now separate dialogs for inclusive and exclusive filters where filter tokens can be specified independently + * on exclusive dialog, there are optional check boxes "Exclude episodes shorter than" and "Mark excluded episodes played" + + ### Security and reliability * Disabled `usesCleartextTraffic`, so that all content transmission is more private and secure diff --git a/app/build.gradle b/app/build.gradle index dd3ed12b..3cc4da12 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -115,7 +115,7 @@ android { checkOnly += ['NewApi', 'InlinedApi', 'Performance', 'DuplicateIds'] disable += ['TypographyDashes', 'TypographyQuotes', 'ObsoleteLintCustomCheck', 'CheckResult', 'UnusedAttribute', 'BatteryLife', 'InflateParams', - 'RestrictedApi', 'TrustAllX509TrustManager', 'ExportedReceiver', 'AllowBackup', 'VectorDrawableCompat', + 'RestrictedApi', 'TrustAllX509TrustManager', 'ExportedReceiver', 'VectorDrawableCompat', 'StaticFieldLeak', 'UseCompoundDrawables', 'NestedWeights', 'Overdraw', 'UselessParent', 'TextFields', 'AlwaysShowAction', 'Autofill', 'ClickableViewAccessibility', 'ContentDescription', 'KeyboardInaccessibleWidget', 'LabelFor', 'SetTextI18n', 'HardcodedText', 'RelativeOverlap', @@ -126,8 +126,8 @@ android { buildConfig true } defaultConfig { - versionCode 3020213 - versionName "6.0.13" + versionCode 3020214 + versionName "6.1.0" applicationId "ac.mdiq.podcini.R" def commit = "" diff --git a/app/src/androidTest/kotlin/ac/test/podcini/EspressoTestUtils.kt b/app/src/androidTest/kotlin/ac/test/podcini/EspressoTestUtils.kt index 40551090..d2c27f35 100644 --- a/app/src/androidTest/kotlin/ac/test/podcini/EspressoTestUtils.kt +++ b/app/src/androidTest/kotlin/ac/test/podcini/EspressoTestUtils.kt @@ -140,12 +140,12 @@ object EspressoTestUtils { InstrumentationRegistry.getInstrumentation().targetContext .getSharedPreferences(MainActivity.PREF_NAME, Context.MODE_PRIVATE) .edit() - .putBoolean(MainActivity.PREF_IS_FIRST_LAUNCH, false) + .putBoolean(MainActivity.Extras.prefMainActivityIsFirstLaunch.name, false) .commit() PreferenceManager.getDefaultSharedPreferences(InstrumentationRegistry.getInstrumentation().targetContext) .edit() - .putString(UserPreferences.PREF_UPDATE_INTERVAL, "0") + .putString(UserPreferences.Prefs.prefAutoUpdateIntervall.name, "0") .commit() RatingDialog.init(InstrumentationRegistry.getInstrumentation().targetContext) @@ -160,7 +160,7 @@ object EspressoTestUtils { .commit() PreferenceManager.getDefaultSharedPreferences(InstrumentationRegistry.getInstrumentation().targetContext) .edit() - .putString(UserPreferences.PREF_DEFAULT_PAGE, UserPreferences.DEFAULT_PAGE_REMEMBER) + .putString(UserPreferences.Prefs.prefDefaultPage.name, UserPreferences.DEFAULT_PAGE_REMEMBER) .commit() } diff --git a/app/src/androidTest/kotlin/ac/test/podcini/playback/PlaybackTest.kt b/app/src/androidTest/kotlin/ac/test/podcini/playback/PlaybackTest.kt index 78578270..244936a9 100644 --- a/app/src/androidTest/kotlin/ac/test/podcini/playback/PlaybackTest.kt +++ b/app/src/androidTest/kotlin/ac/test/podcini/playback/PlaybackTest.kt @@ -194,17 +194,17 @@ class PlaybackTest { protected fun setContinuousPlaybackPreference(value: Boolean) { val prefs = PreferenceManager.getDefaultSharedPreferences(context) - prefs.edit().putBoolean(UserPreferences.PREF_FOLLOW_QUEUE, value).commit() + prefs.edit().putBoolean(UserPreferences.Prefs.prefFollowQueue.name, value).commit() } protected fun setSkipKeepsEpisodePreference(value: Boolean) { val prefs = PreferenceManager.getDefaultSharedPreferences(context) - prefs.edit().putBoolean(UserPreferences.PREF_SKIP_KEEPS_EPISODE, value).commit() + prefs.edit().putBoolean(UserPreferences.Prefs.prefSkipKeepsEpisode.name, value).commit() } protected fun setSmartMarkAsPlayedPreference(smartMarkAsPlayedSecs: Int) { val prefs = PreferenceManager.getDefaultSharedPreferences(context) - prefs.edit().putString(UserPreferences.PREF_SMART_MARK_AS_PLAYED_SECS, + prefs.edit().putString(UserPreferences.Prefs.prefSmartMarkAsPlayedSecs.name, smartMarkAsPlayedSecs.toString(10)) .commit() } diff --git a/app/src/androidTest/kotlin/ac/test/podcini/storage/AutoDownloadTest.kt b/app/src/androidTest/kotlin/ac/test/podcini/storage/AutoDownloadTest.kt index 9d2a4a0d..80cccdc9 100644 --- a/app/src/androidTest/kotlin/ac/test/podcini/storage/AutoDownloadTest.kt +++ b/app/src/androidTest/kotlin/ac/test/podcini/storage/AutoDownloadTest.kt @@ -39,14 +39,12 @@ class AutoDownloadTest { // Setup: enable automatic download // it is not needed, as the actual automatic download is stubbed. stubDownloadAlgorithm = StubDownloadAlgorithm() -// setDownloadAlgorithm(stubDownloadAlgorithm!!) downloadAlgorithm = stubDownloadAlgorithm!! } @After @Throws(Exception::class) fun tearDown() { -// setDownloadAlgorithm(Episodes.AutomaticDownloadAlgorithm()) downloadAlgorithm = AutoDownloads.AutoDownloadAlgorithm() EspressoTestUtils.tryKillPlaybackService() stubFeedsServer!!.tearDown() @@ -54,7 +52,6 @@ class AutoDownloadTest { /** * A cross-functional test, ensuring playback's behavior works with Auto Download in boundary condition. - * * Scenario: * - For setting enqueue location AFTER_CURRENTLY_PLAYING * - when playback of an episode is complete and the app advances to the next episode (continuous playback on) @@ -107,7 +104,7 @@ class AutoDownloadTest { var currentlyPlayingAtDownload: Long = -1 private set - override fun autoDownloadEpisodeMedia(context: Context): Runnable { + override fun autoDownloadEpisodeMediaNew(context: Context): Runnable { return Runnable { if (currentlyPlayingAtDownload == -1L) { // currentlyPlayingAtDownload = currentlyPlayingFeedMediaId diff --git a/app/src/androidTest/kotlin/ac/test/podcini/ui/FeedSettingsTest.kt b/app/src/androidTest/kotlin/ac/test/podcini/ui/FeedSettingsTest.kt index 3270699e..864255ae 100644 --- a/app/src/androidTest/kotlin/ac/test/podcini/ui/FeedSettingsTest.kt +++ b/app/src/androidTest/kotlin/ac/test/podcini/ui/FeedSettingsTest.kt @@ -39,7 +39,7 @@ class FeedSettingsTest { uiTestUtils!!.addLocalFeedData(false) feed = uiTestUtils!!.hostedFeeds[0] val intent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, MainActivity::class.java) - intent.putExtra(MainActivity.EXTRA_FEED_ID, feed!!.id) + intent.putExtra(MainActivity.Extras.fragment_feed_id.name, feed!!.id) activityRule.launchActivity(intent) } diff --git a/app/src/androidTest/kotlin/ac/test/podcini/ui/PreferencesTest.kt b/app/src/androidTest/kotlin/ac/test/podcini/ui/PreferencesTest.kt index 02661285..e0be914b 100644 --- a/app/src/androidTest/kotlin/ac/test/podcini/ui/PreferencesTest.kt +++ b/app/src/androidTest/kotlin/ac/test/podcini/ui/PreferencesTest.kt @@ -32,11 +32,11 @@ import ac.mdiq.podcini.preferences.UserPreferences.isAutoDeleteLocal import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownloadOnBattery import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs -import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue import ac.mdiq.podcini.preferences.UserPreferences.shouldPauseForFocusLoss import ac.mdiq.podcini.preferences.UserPreferences.showNextChapterOnFullNotification import ac.mdiq.podcini.preferences.UserPreferences.showPlaybackSpeedOnFullNotification import ac.mdiq.podcini.preferences.UserPreferences.showSkipOnFullNotification +import ac.mdiq.podcini.storage.database.Episodes.shouldDeleteRemoveFromQueue import ac.mdiq.podcini.storage.database.Queues import ac.mdiq.podcini.storage.database.Queues.enqueueLocation import de.test.podcini.EspressoTestUtils @@ -65,7 +65,7 @@ class PreferencesTest { EspressoTestUtils.clearPreferences() activityTestRule.launchActivity(Intent()) val prefs = PreferenceManager.getDefaultSharedPreferences(activityTestRule.activity) - prefs.edit().putBoolean(UserPreferences.PREF_ENABLE_AUTODL, true).commit() + prefs.edit().putBoolean(UserPreferences.Prefs.prefEnableAutoDl.name, true).commit() res = activityTestRule.activity.resources init(activityTestRule.activity) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt index 79f3dab6..2b9f8fb8 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt @@ -9,7 +9,6 @@ import ac.mdiq.podcini.net.sync.model.EpisodeAction import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileEpisodeDownload import ac.mdiq.podcini.preferences.UserPreferences -import ac.mdiq.podcini.preferences.UserPreferences.PREF_ENQUEUE_DOWNLOADED import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.storage.database.Episodes import ac.mdiq.podcini.storage.database.LogsAndStats @@ -126,7 +125,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() { return workRequest } private fun enqueueDownloadedEpisodes(): Boolean { - return appPrefs.getBoolean(PREF_ENQUEUE_DOWNLOADED, true) + return appPrefs.getBoolean(UserPreferences.Prefs.prefEnqueueDownloaded.name, true) } } @@ -291,7 +290,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() { // sendMessage(title, false) // return // } - val builder = NotificationCompat.Builder(applicationContext, NotificationUtils.CHANNEL_ID_DOWNLOAD_ERROR) + val builder = NotificationCompat.Builder(applicationContext, NotificationUtils.CHANNEL_ID.error.name) builder.setTicker(applicationContext.getString(R.string.download_report_title)) .setContentTitle(applicationContext.getString(R.string.download_report_title)) .setContentText(applicationContext.getString(R.string.download_error_tap_for_details)) @@ -314,7 +313,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() { val bigText = bigTextB.toString().trim { it <= ' ' } val contentText = if (progressCopy.size == 1) bigText else applicationContext.resources.getQuantityString(R.plurals.downloads_left, progressCopy.size, progressCopy.size) - val builder = NotificationCompat.Builder(applicationContext, NotificationUtils.CHANNEL_ID_DOWNLOADING) + val builder = NotificationCompat.Builder(applicationContext, NotificationUtils.CHANNEL_ID.downloading.name) builder.setTicker(applicationContext.getString(R.string.download_notification_title_episodes)) .setContentTitle(applicationContext.getString(R.string.download_notification_title_episodes)) .setContentText(contentText) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt index e14f0057..fea8357b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt @@ -8,14 +8,13 @@ import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest import ac.mdiq.podcini.net.feed.parser.FeedHandler import ac.mdiq.podcini.net.feed.parser.FeedHandlerResult import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileFeedRefresh -import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.net.utils.NetworkUtils.isFeedRefreshAllowed import ac.mdiq.podcini.net.utils.NetworkUtils.isNetworkRestricted import ac.mdiq.podcini.net.utils.NetworkUtils.isVpnOverWifi import ac.mdiq.podcini.net.utils.NetworkUtils.networkAvailable -import ac.mdiq.podcini.preferences.UserPreferences.PREF_UPDATE_INTERVAL +import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia import ac.mdiq.podcini.storage.database.Feeds @@ -66,7 +65,7 @@ object FeedUpdateManager { const val EXTRA_EVEN_ON_MOBILE: String = "even_on_mobile" val updateInterval: Long - get() = appPrefs.getString(PREF_UPDATE_INTERVAL, "12")!!.toInt().toLong() + get() = appPrefs.getString(UserPreferences.Prefs.prefAutoUpdateIntervall.name, "12")!!.toInt().toLong() val isAutoUpdateDisabled: Boolean get() = updateInterval == 0L @@ -77,9 +76,8 @@ object FeedUpdateManager { */ @JvmStatic fun restartUpdateAlarm(context: Context, replace: Boolean) { - if (isAutoUpdateDisabled) { - WorkManager.getInstance(context).cancelUniqueWork(WORK_ID_FEED_UPDATE) - } else { + if (isAutoUpdateDisabled) WorkManager.getInstance(context).cancelUniqueWork(WORK_ID_FEED_UPDATE) + else { val workRequest: PeriodicWorkRequest = PeriodicWorkRequest.Builder(FeedUpdateWorker::class.java, updateInterval, TimeUnit.HOURS) .setConstraints(Builder() .setRequiredNetworkType(if (isAllowMobileFeedRefresh) NetworkType.CONNECTED else NetworkType.UNMETERED) @@ -172,7 +170,8 @@ object FeedUpdateManager { } refreshFeeds(toUpdate, force) notificationManager.cancel(R.id.notification_updating_feeds) - autodownloadEpisodeMedia(applicationContext) + autodownloadEpisodeMedia(applicationContext, toUpdate.toList()) + toUpdate.clear() return Result.success() } private fun createNotification(toUpdate: List?): Notification { @@ -184,7 +183,7 @@ object FeedUpdateManager { toUpdate.size, toUpdate.size) bigText = Stream.of(toUpdate).map { feed: Feed? -> "• " + feed!!.title }.collect(Collectors.joining("\n")) } - return NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID_DOWNLOADING) + return NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID.downloading.name) .setContentTitle(context.getString(R.string.download_notification_title_feeds)) .setContentText(contentText) .setStyle(NotificationCompat.BigTextStyle().bigText(bigText)) @@ -212,10 +211,11 @@ object FeedUpdateManager { // Toast.makeText(applicationContext, R.string.notification_permission_text, Toast.LENGTH_LONG).show() return } - while (toUpdate.isNotEmpty()) { + var i = 0 + while (i < toUpdate.size) { if (isStopped) return notificationManager.notify(R.id.notification_updating_feeds, createNotification(toUpdate)) - val feed = unmanaged(toUpdate[0]) + val feed = unmanaged(toUpdate[i++]) try { Logd(TAG, "updating local feed? ${feed.isLocalFeed} ${feed.title}") if (feed.isLocalFeed) LocalFeedUpdater.updateFeed(feed, applicationContext, null) @@ -226,7 +226,7 @@ object FeedUpdateManager { val status = DownloadResult(feed.id, feed.title?:"", DownloadError.ERROR_IO_ERROR, false, e.message?:"") LogsAndStats.addDownloadStatus(status) } - toUpdate.removeAt(0) +// toUpdate.removeAt(0) } } @UnstableApi @@ -363,8 +363,8 @@ object FeedUpdateManager { } class FeedSyncTask(private val context: Context, request: DownloadRequest) { - var savedFeed: Feed? = null - private set +// var savedFeed: Feed? = null +// private set private val task = FeedParserTask(request) private var feedHandlerResult: FeedHandlerResult? = null val downloadStatus: DownloadResult @@ -375,7 +375,7 @@ object FeedUpdateManager { fun run(): Boolean { feedHandlerResult = task.call() if (!task.isSuccessful) return false - savedFeed = Feeds.updateFeed(context, feedHandlerResult!!.feed, false) + Feeds.updateFeed(context, feedHandlerResult!!.feed, false) return true } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/CombinedSearcher.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/CombinedSearcher.kt index e8dc7b35..d5c18a76 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/CombinedSearcher.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/CombinedSearcher.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.supervisorScope class CombinedSearcher : PodcastSearcher { - override suspend fun search1(query: String): List { + override suspend fun search(query: String): List { val searchProviders = PodcastSearcherRegistry.searchProviders val searchResults = MutableList?>(searchProviders.size) { null } @@ -19,7 +19,7 @@ class CombinedSearcher : PodcastSearcher { if (searchProviderInfo.weight > 0.00001f && searcher.javaClass != CombinedSearcher::class.java) { async(Dispatchers.IO) { try { - val results = searcher.search1(query) + val results = searcher.search(query) searchResults[index] = results } catch (e: Throwable) { Log.d(TAG, Log.getStackTraceString(e)) @@ -70,7 +70,7 @@ class CombinedSearcher : PodcastSearcher { // return PodcastSearcherRegistry.lookupUrl(url) // } - override suspend fun lookupUrl1(resultUrl: String): String { + override suspend fun lookupUrl(resultUrl: String): String { return PodcastSearcherRegistry.lookupUrl1(resultUrl) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/FyydPodcastSearcher.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/FyydPodcastSearcher.kt deleted file mode 100644 index b86787d1..00000000 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/FyydPodcastSearcher.kt +++ /dev/null @@ -1,40 +0,0 @@ -package ac.mdiq.podcini.net.feed.discovery - -import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient -import de.mfietz.fyydlin.FyydClient -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -class FyydPodcastSearcher : PodcastSearcher { - private val client = FyydClient(getHttpClient()) - - override suspend fun search1(query: String): List? { - val response = withContext(Dispatchers.IO) { - client.searchPodcasts(query, 10).blockingGet() - } - val searchResults = ArrayList() - - if (response.data.isNotEmpty()) { - for (searchHit in response.data) { - val podcast = PodcastSearchResult.fromFyyd(searchHit) - searchResults.add(podcast) - } - } - return searchResults - } - -// override fun lookupUrl(url: String): Single { -// return Single.just(url) -// } - - override suspend fun lookupUrl1(resultUrl: String): String { - return resultUrl - } - - override fun urlNeedsLookup(url: String): Boolean { - return false - } - - override val name: String - get() = "fyyd" -} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/GpodnetPodcastSearcher.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/GpodnetPodcastSearcher.kt deleted file mode 100644 index eba8b74a..00000000 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/GpodnetPodcastSearcher.kt +++ /dev/null @@ -1,44 +0,0 @@ -package ac.mdiq.podcini.net.feed.discovery - -import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient -import ac.mdiq.podcini.net.sync.SynchronizationCredentials -import ac.mdiq.podcini.net.sync.gpoddernet.GpodnetService -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -class GpodnetPodcastSearcher : PodcastSearcher { - - override suspend fun search1(query: String): List? { - return try { - val service = GpodnetService(getHttpClient(), - SynchronizationCredentials.hosturl, SynchronizationCredentials.deviceID ?: "", - SynchronizationCredentials.username ?: "", SynchronizationCredentials.password ?: "") - val gpodnetPodcasts = withContext(Dispatchers.IO) { - service.searchPodcasts(query, 0) - } - val results: MutableList = ArrayList() - for (podcast in gpodnetPodcasts) { - results.add(PodcastSearchResult.fromGpodder(podcast)) - } - results - } catch (e: GpodnetService.GpodnetServiceException) { - e.printStackTrace() - throw e - } - } - -// override fun lookupUrl(url: String): Single { -// return Single.just(url) -// } - - override suspend fun lookupUrl1(resultUrl: String): String { - return resultUrl - } - - override fun urlNeedsLookup(url: String): Boolean { - return false - } - - override val name: String - get() = "Gpodder.net" -} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/ItunesPodcastSearcher.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/ItunesPodcastSearcher.kt deleted file mode 100644 index 56aac68f..00000000 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/ItunesPodcastSearcher.kt +++ /dev/null @@ -1,84 +0,0 @@ -package ac.mdiq.podcini.net.feed.discovery - -import ac.mdiq.podcini.net.feed.FeedUrlNotFoundException -import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient -import okhttp3.Request.Builder -import org.json.JSONException -import org.json.JSONObject -import java.io.IOException -import java.io.UnsupportedEncodingException -import java.net.URLEncoder -import java.util.regex.Pattern - -class ItunesPodcastSearcher : PodcastSearcher { - - override suspend fun search1(query: String): List? { - val encodedQuery = try { - URLEncoder.encode(query, "UTF-8") - } catch (e: UnsupportedEncodingException) { - // this won't ever be thrown - query - } - val formattedUrl = String.format(ITUNES_API_URL, encodedQuery) - - val client = getHttpClient() - val httpReq: Builder = Builder().url(formattedUrl) - val podcasts: MutableList = ArrayList() - try { - val response = client.newCall(httpReq.build()).execute() - - if (response.isSuccessful) { - val resultString = response.body!!.string() - val result = JSONObject(resultString) - val j = result.getJSONArray("results") - - for (i in 0 until j.length()) { - val podcastJson = j.getJSONObject(i) - val podcast = PodcastSearchResult.fromItunes(podcastJson) - if (podcast.feedUrl != null) podcasts.add(podcast) - } - } else { - throw IOException(response.toString()) - } - } catch (e: IOException) { - throw e - } catch (e: JSONException) { - throw e - } - return podcasts - } - - override suspend fun lookupUrl1(url: String): String { - val pattern = Pattern.compile(PATTERN_BY_ID) - val matcher = pattern.matcher(url) - val lookupUrl = if (matcher.find()) "https://itunes.apple.com/lookup?id=" + matcher.group(1) else url - val client = getHttpClient() - val httpReq = Builder().url(lookupUrl).build() - val response = client.newCall(httpReq).execute() - if (!response.isSuccessful) { - throw IOException(response.toString()) - } - val resultString = response.body!!.string() - val result = JSONObject(resultString) - val results = result.getJSONArray("results").getJSONObject(0) - val feedUrlName = "feedUrl" - if (!results.has(feedUrlName)) { - val artistName = results.getString("artistName") - val trackName = results.getString("trackName") - throw FeedUrlNotFoundException(artistName, trackName) - } - return results.getString(feedUrlName) - } - - override fun urlNeedsLookup(url: String): Boolean { - return url.contains("itunes.apple.com") || url.matches(PATTERN_BY_ID.toRegex()) - } - - override val name: String - get() = "Apple" - - companion object { - private const val ITUNES_API_URL = "https://itunes.apple.com/search?media=podcast&term=%s" - private const val PATTERN_BY_ID = ".*/podcasts\\.apple\\.com/.*/podcast/.*/id(\\d+).*" - } -} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/ItunesTopListLoader.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/ItunesTopListLoader.kt index c79dfcb9..5cc49207 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/ItunesTopListLoader.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/ItunesTopListLoader.kt @@ -18,6 +18,7 @@ import java.util.* import java.util.concurrent.TimeUnit class ItunesTopListLoader(private val context: Context) { + @Throws(JSONException::class, IOException::class) fun loadToplist(country: String, limit: Int, subscribed: List): List { val client = getHttpClient() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/PodcastIndexPodcastSearcher.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/PodcastIndexPodcastSearcher.kt deleted file mode 100644 index a19f3c26..00000000 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/PodcastIndexPodcastSearcher.kt +++ /dev/null @@ -1,108 +0,0 @@ -package ac.mdiq.podcini.net.feed.discovery - -import ac.mdiq.podcini.BuildConfig -import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient -import ac.mdiq.podcini.util.config.ClientConfig -import okhttp3.Request -import okhttp3.Request.Builder -import org.json.JSONException -import org.json.JSONObject -import java.io.IOException -import java.io.UnsupportedEncodingException -import java.net.URLEncoder -import java.security.MessageDigest -import java.util.* - -class PodcastIndexPodcastSearcher : PodcastSearcher { - - override suspend fun search1(query: String): List? { - val encodedQuery = try { - URLEncoder.encode(query, "UTF-8") - } catch (e: UnsupportedEncodingException) { - // this won't ever be thrown - query - } - val formattedUrl = String.format(SEARCH_API_URL, encodedQuery) - val podcasts: MutableList = ArrayList() - try { - val client = getHttpClient() - val response = client.newCall(buildAuthenticatedRequest(formattedUrl)).execute() - - if (response.isSuccessful) { - val resultString = response.body!!.string() - val result = JSONObject(resultString) - val j = result.getJSONArray("feeds") - - for (i in 0 until j.length()) { - val podcastJson = j.getJSONObject(i) - val podcast = PodcastSearchResult.fromPodcastIndex(podcastJson) - if (podcast.feedUrl != null) podcasts.add(podcast) - } - } else { - throw IOException(response.toString()) - } - } catch (e: IOException) { - throw e - } catch (e: JSONException) { - throw e - } - return podcasts - } - -// override fun lookupUrl(url: String): Single { -// return Single.just(url) -// } - - override suspend fun lookupUrl1(resultUrl: String): String { - return resultUrl - } - - override fun urlNeedsLookup(url: String): Boolean { - return false - } - - override val name: String - get() = "Podcast Index" - - private fun buildAuthenticatedRequest(url: String): Request { - val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")) - calendar.clear() - val now = Date() - calendar.time = now - val secondsSinceEpoch = calendar.timeInMillis / 1000L - val apiHeaderTime = secondsSinceEpoch.toString() - val data4Hash = BuildConfig.PODCASTINDEX_API_KEY + BuildConfig.PODCASTINDEX_API_SECRET + apiHeaderTime - val hashString = sha1(data4Hash) ?:"" - - val httpReq: Builder = Builder() - .addHeader("X-Auth-Date", apiHeaderTime) - .addHeader("X-Auth-Key", BuildConfig.PODCASTINDEX_API_KEY) - .addHeader("Authorization", hashString) - .addHeader("User-Agent", ClientConfig.USER_AGENT?:"") - .url(url) - return httpReq.build() - } - - companion object { - private const val SEARCH_API_URL = "https://api.podcastindex.org/api/1.0/search/byterm?q=%s" - - private fun sha1(clearString: String): String? { - try { - val messageDigest = MessageDigest.getInstance("SHA-1") - messageDigest.update(clearString.toByteArray(charset("UTF-8"))) - return toHex(messageDigest.digest()) - } catch (ignored: Exception) { - ignored.printStackTrace() - return null - } - } - - private fun toHex(bytes: ByteArray): String { - val buffer = StringBuilder() - for (b in bytes) { - buffer.append(String.format(Locale.getDefault(), "%02x", b)) - } - return buffer.toString() - } - } -} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/PodcastSearcher.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/PodcastSearcher.kt index dde5af43..5802e4be 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/PodcastSearcher.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/PodcastSearcher.kt @@ -2,11 +2,11 @@ package ac.mdiq.podcini.net.feed.discovery interface PodcastSearcher { - fun urlNeedsLookup(resultUrl: String): Boolean + fun urlNeedsLookup(url: String): Boolean - suspend fun search1(query: String): List? + suspend fun search(query: String): List? - suspend fun lookupUrl1(resultUrl: String): String + suspend fun lookupUrl(url: String): String val name: String? } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/PodcastSearcherRegistry.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/PodcastSearcherRegistry.kt index 0f40cd9b..8c2f328b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/PodcastSearcherRegistry.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/PodcastSearcherRegistry.kt @@ -1,5 +1,24 @@ package ac.mdiq.podcini.net.feed.discovery +import ac.mdiq.podcini.BuildConfig +import ac.mdiq.podcini.net.download.service.PodciniHttpClient +import ac.mdiq.podcini.net.feed.FeedUrlNotFoundException +import ac.mdiq.podcini.net.sync.SynchronizationCredentials +import ac.mdiq.podcini.net.sync.gpoddernet.GpodnetService +import ac.mdiq.podcini.util.config.ClientConfig +import de.mfietz.fyydlin.FyydClient +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.Request +import org.json.JSONException +import org.json.JSONObject +import java.io.IOException +import java.io.UnsupportedEncodingException +import java.net.URLEncoder +import java.security.MessageDigest +import java.util.* +import java.util.regex.Pattern + object PodcastSearcherRegistry { @get:Synchronized var searchProviders: MutableList = mutableListOf() @@ -19,7 +38,7 @@ object PodcastSearcherRegistry { suspend fun lookupUrl1(url: String): String { for (searchProviderInfo in searchProviders) { if (searchProviderInfo.searcher.javaClass != CombinedSearcher::class.java && searchProviderInfo.searcher.urlNeedsLookup(url)) - return searchProviderInfo.searcher.lookupUrl1(url) + return searchProviderInfo.searcher.lookupUrl(url) } return url } @@ -33,3 +52,230 @@ object PodcastSearcherRegistry { class SearcherInfo(val searcher: PodcastSearcher, val weight: Float) } + +class PodcastIndexPodcastSearcher : PodcastSearcher { + override suspend fun search(query: String): List { + val encodedQuery = try { + withContext(Dispatchers.IO) { + URLEncoder.encode(query, "UTF-8") + } + } catch (e: UnsupportedEncodingException) { + // this won't ever be thrown + query + } + val formattedUrl = String.format(SEARCH_API_URL, encodedQuery) + val podcasts: MutableList = ArrayList() + try { + val client = PodciniHttpClient.getHttpClient() + val response = client.newCall(buildAuthenticatedRequest(formattedUrl)).execute() + + if (response.isSuccessful) { + val resultString = response.body!!.string() + val result = JSONObject(resultString) + val j = result.getJSONArray("feeds") + + for (i in 0 until j.length()) { + val podcastJson = j.getJSONObject(i) + val podcast = PodcastSearchResult.fromPodcastIndex(podcastJson) + if (podcast.feedUrl != null) podcasts.add(podcast) + } + } else { + throw IOException(response.toString()) + } + } catch (e: IOException) { + throw e + } catch (e: JSONException) { + throw e + } + return podcasts + } + + override suspend fun lookupUrl(url: String): String { + return url + } + + override fun urlNeedsLookup(url: String): Boolean { + return false + } + + override val name: String + get() = "Podcast Index" + + private fun buildAuthenticatedRequest(url: String): Request { + val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")) + calendar.clear() + val now = Date() + calendar.time = now + val secondsSinceEpoch = calendar.timeInMillis / 1000L + val apiHeaderTime = secondsSinceEpoch.toString() + val data4Hash = BuildConfig.PODCASTINDEX_API_KEY + BuildConfig.PODCASTINDEX_API_SECRET + apiHeaderTime + val hashString = sha1(data4Hash) ?:"" + + val httpReq: Request.Builder = Request.Builder() + .addHeader("X-Auth-Date", apiHeaderTime) + .addHeader("X-Auth-Key", BuildConfig.PODCASTINDEX_API_KEY) + .addHeader("Authorization", hashString) + .addHeader("User-Agent", ClientConfig.USER_AGENT ?:"") + .url(url) + return httpReq.build() + } + + companion object { + private const val SEARCH_API_URL = "https://api.podcastindex.org/api/1.0/search/byterm?q=%s" + + private fun sha1(clearString: String): String? { + try { + val messageDigest = MessageDigest.getInstance("SHA-1") + messageDigest.update(clearString.toByteArray(charset("UTF-8"))) + return toHex(messageDigest.digest()) + } catch (ignored: Exception) { + ignored.printStackTrace() + return null + } + } + + private fun toHex(bytes: ByteArray): String { + val buffer = StringBuilder() + for (b in bytes) { + buffer.append(String.format(Locale.getDefault(), "%02x", b)) + } + return buffer.toString() + } + } +} + +class FyydPodcastSearcher : PodcastSearcher { + private val client = FyydClient(PodciniHttpClient.getHttpClient()) + + override suspend fun search(query: String): List { + val response = withContext(Dispatchers.IO) { + client.searchPodcasts(query, 10).blockingGet() + } + val searchResults = ArrayList() + + if (response.data.isNotEmpty()) { + for (searchHit in response.data) { + val podcast = PodcastSearchResult.fromFyyd(searchHit) + searchResults.add(podcast) + } + } + return searchResults + } + + override suspend fun lookupUrl(url: String): String { + return url + } + + override fun urlNeedsLookup(url: String): Boolean { + return false + } + + override val name: String + get() = "fyyd" +} + +class GpodnetPodcastSearcher : PodcastSearcher { + override suspend fun search(query: String): List? { + return try { + val service = GpodnetService(PodciniHttpClient.getHttpClient(), + SynchronizationCredentials.hosturl, SynchronizationCredentials.deviceID ?: "", + SynchronizationCredentials.username ?: "", SynchronizationCredentials.password ?: "") + val gpodnetPodcasts = withContext(Dispatchers.IO) { + service.searchPodcasts(query, 0) + } + val results: MutableList = ArrayList() + for (podcast in gpodnetPodcasts) { + results.add(PodcastSearchResult.fromGpodder(podcast)) + } + results + } catch (e: GpodnetService.GpodnetServiceException) { + e.printStackTrace() + throw e + } + } + + override suspend fun lookupUrl(url: String): String { + return url + } + + override fun urlNeedsLookup(url: String): Boolean { + return false + } + + override val name: String + get() = "Gpodder.net" +} + +class ItunesPodcastSearcher : PodcastSearcher { + override suspend fun search(query: String): List { + val encodedQuery = try { + withContext(Dispatchers.IO) { + URLEncoder.encode(query, "UTF-8") + } + } catch (e: UnsupportedEncodingException) { + // this won't ever be thrown + query + } + val formattedUrl = String.format(ITUNES_API_URL, encodedQuery) + + val client = PodciniHttpClient.getHttpClient() + val httpReq: Request.Builder = Request.Builder().url(formattedUrl) + val podcasts: MutableList = ArrayList() + try { + val response = client.newCall(httpReq.build()).execute() + + if (response.isSuccessful) { + val resultString = response.body!!.string() + val result = JSONObject(resultString) + val j = result.getJSONArray("results") + + for (i in 0 until j.length()) { + val podcastJson = j.getJSONObject(i) + val podcast = PodcastSearchResult.fromItunes(podcastJson) + if (podcast.feedUrl != null) podcasts.add(podcast) + } + } else { + throw IOException(response.toString()) + } + } catch (e: IOException) { + throw e + } catch (e: JSONException) { + throw e + } + return podcasts + } + + override suspend fun lookupUrl(url: String): String { + val pattern = Pattern.compile(PATTERN_BY_ID) + val matcher = pattern.matcher(url) + val lookupUrl = if (matcher.find()) "https://itunes.apple.com/lookup?id=" + matcher.group(1) else url + val client = PodciniHttpClient.getHttpClient() + val httpReq = Request.Builder().url(lookupUrl).build() + val response = client.newCall(httpReq).execute() + if (!response.isSuccessful) { + throw IOException(response.toString()) + } + val resultString = response.body!!.string() + val result = JSONObject(resultString) + val results = result.getJSONArray("results").getJSONObject(0) + val feedUrlName = "feedUrl" + if (!results.has(feedUrlName)) { + val artistName = results.getString("artistName") + val trackName = results.getString("trackName") + throw FeedUrlNotFoundException(artistName, trackName) + } + return results.getString(feedUrlName) + } + + override fun urlNeedsLookup(url: String): Boolean { + return url.contains("itunes.apple.com") || url.matches(PATTERN_BY_ID.toRegex()) + } + + override val name: String + get() = "Apple" + + companion object { + private const val ITUNES_API_URL = "https://itunes.apple.com/search?media=podcast&term=%s" + private const val PATTERN_BY_ID = ".*/podcasts\\.apple\\.com/.*/podcast/.*/id(\\d+).*" + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/FeedHandler.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/FeedHandler.kt index e6bbed84..42663d95 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/FeedHandler.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/FeedHandler.kt @@ -259,30 +259,30 @@ class FeedHandler { } class UnsupportedFeedtypeException : Exception { - val type: FeedHandler.Type + val type: Type var rootElement: String? = null private set override var message: String? = null get() { return when { field != null -> field!! - type == FeedHandler.Type.INVALID -> "Invalid type" + type == Type.INVALID -> "Invalid type" else -> "Type $type not supported" } } - constructor(type: FeedHandler.Type) : super() { + constructor(type: Type) : super() { this.type = type } - constructor(type: FeedHandler.Type, rootElement: String?) { + constructor(type: Type, rootElement: String?) { this.type = type this.rootElement = rootElement } constructor(message: String?) { this.message = message - type = FeedHandler.Type.INVALID + type = Type.INVALID } // fun getMessage(): String? { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/HandlerState.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/HandlerState.kt index 6e5c235e..fdb7808d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/HandlerState.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/HandlerState.kt @@ -52,10 +52,10 @@ class HandlerState(@JvmField var feed: Feed) { @JvmField val tempObjects: MutableMap = HashMap() + /** + * Returns the SyndElement that comes after the top element of the tagstack. + */ val secondTag: SyndElement - /** - * Returns the SyndElement that comes after the top element of the tagstack. - */ get() { val top = tagstack.pop() val second = tagstack.peek() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SyncService.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SyncService.kt index 618dd5e0..9914b784 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SyncService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SyncService.kt @@ -18,7 +18,7 @@ import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueStorage import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileFor import ac.mdiq.podcini.net.utils.NetworkUtils.setAllowMobileFor import ac.mdiq.podcini.net.utils.UrlChecker.containsUrl -import ac.mdiq.podcini.preferences.UserPreferences.PREF_GPODNET_NOTIFICATIONS +import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.storage.database.Episodes.getEpisodeByGuidOrUrl import ac.mdiq.podcini.storage.database.Episodes.getEpisodes @@ -213,7 +213,7 @@ open class SyncService(context: Context, params: WorkerParameters) : Worker(cont val queuedEpisodeActions: MutableList = synchronizationQueueStorage.queuedEpisodeActions if (lastSync == 0L) { EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_upload_played)) - val readItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.PLAYED), EpisodeSortOrder.DATE_NEW_OLD) + val readItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.played.name), EpisodeSortOrder.DATE_NEW_OLD) Logd(TAG, "First sync. Upload state for all " + readItems.size + " played episodes") for (item in readItems) { val media = item.media ?: continue @@ -300,7 +300,7 @@ open class SyncService(context: Context, params: WorkerParameters) : Worker(cont fun gpodnetNotificationsEnabled(): Boolean { if (Build.VERSION.SDK_INT >= 26) return true // System handles notification preferences - return appPrefs.getBoolean(PREF_GPODNET_NOTIFICATIONS, true) + return appPrefs.getBoolean(UserPreferences.Prefs.pref_gpodnet_notifications.name, true) } protected fun updateErrorNotification(exception: Exception) { @@ -317,12 +317,11 @@ open class SyncService(context: Context, params: WorkerParameters) : Worker(cont // return // } - val intent = applicationContext.packageManager.getLaunchIntentForPackage( - applicationContext.packageName) + val intent = applicationContext.packageManager.getLaunchIntentForPackage(applicationContext.packageName) val pendingIntent = PendingIntent.getActivity(applicationContext, R.id.pending_intent_sync_error, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) val notification = NotificationCompat.Builder(applicationContext, - NotificationUtils.CHANNEL_ID_SYNC_ERROR) + NotificationUtils.CHANNEL_ID.sync_error.name) .setContentTitle(applicationContext.getString(R.string.gpodnetsync_error_title)) .setContentText(description) .setStyle(NotificationCompat.BigTextStyle().bigText(description)) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SynchronizationCredentials.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SynchronizationCredentials.kt index 48017f57..69bfc934 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SynchronizationCredentials.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SynchronizationCredentials.kt @@ -1,7 +1,8 @@ package ac.mdiq.podcini.net.sync import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink.clearQueue -import ac.mdiq.podcini.preferences.UserPreferences.PREF_GPODNET_NOTIFICATIONS +import ac.mdiq.podcini.preferences.UserPreferences +import ac.mdiq.podcini.preferences.UserPreferences.Prefs.pref_gpodnet_notifications import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.util.config.ClientConfig import android.content.Context @@ -57,7 +58,7 @@ object SynchronizationCredentials { } fun setGpodnetNotificationsEnabled() { - appPrefs.edit().putBoolean(PREF_GPODNET_NOTIFICATIONS, true).apply() + appPrefs.edit().putBoolean(UserPreferences.Prefs.pref_gpodnet_notifications.name, true).apply() } @Synchronized diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt index 42d09716..70e59d5b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt @@ -244,9 +244,9 @@ import kotlin.math.min if (lastSync == 0L) { EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_upload_played)) // only push downloaded items - val pausedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.PAUSED), EpisodeSortOrder.DATE_NEW_OLD) - val readItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.PLAYED), EpisodeSortOrder.DATE_NEW_OLD) - val favoriteItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.IS_FAVORITE), EpisodeSortOrder.DATE_NEW_OLD) + val pausedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.paused.name), EpisodeSortOrder.DATE_NEW_OLD) + val readItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.played.name), EpisodeSortOrder.DATE_NEW_OLD) + val favoriteItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.is_favorite.name), EpisodeSortOrder.DATE_NEW_OLD) val comItems = mutableSetOf() comItems.addAll(pausedItems) comItems.addAll(readItems) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/utils/NetworkUtils.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/utils/NetworkUtils.kt index 622f5dab..33205df5 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/utils/NetworkUtils.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/utils/NetworkUtils.kt @@ -1,8 +1,6 @@ package ac.mdiq.podcini.net.utils -import ac.mdiq.podcini.preferences.UserPreferences.PREF_AUTODL_SELECTED_NETWORKS -import ac.mdiq.podcini.preferences.UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER -import ac.mdiq.podcini.preferences.UserPreferences.PREF_MOBILE_UPDATE +import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import android.content.Context import android.net.ConnectivityManager @@ -41,23 +39,23 @@ object NetworkUtils { fun isAllowMobileFor(type: String): Boolean { val defaultValue = HashSet() defaultValue.add("images") - val allowed = appPrefs.getStringSet(PREF_MOBILE_UPDATE, defaultValue) + val allowed = appPrefs.getStringSet(UserPreferences.Prefs.prefMobileUpdateTypes.name, defaultValue) return allowed!!.contains(type) } fun setAllowMobileFor(type: String, allow: Boolean) { val defaultValue = HashSet() defaultValue.add("images") - val getValueStringSet = appPrefs.getStringSet(PREF_MOBILE_UPDATE, defaultValue) + val getValueStringSet = appPrefs.getStringSet(UserPreferences.Prefs.prefMobileUpdateTypes.name, defaultValue) val allowed: MutableSet = HashSet(getValueStringSet!!) if (allow) allowed.add(type) else allowed.remove(type) - appPrefs.edit().putStringSet(PREF_MOBILE_UPDATE, allowed).apply() + appPrefs.edit().putStringSet(UserPreferences.Prefs.prefMobileUpdateTypes.name, allowed).apply() } val isEnableAutodownloadWifiFilter: Boolean - get() = Build.VERSION.SDK_INT < 29 && appPrefs.getBoolean(PREF_ENABLE_AUTODL_WIFI_FILTER, false) + get() = Build.VERSION.SDK_INT < 29 && appPrefs.getBoolean(UserPreferences.Prefs.prefEnableAutoDownloadWifiFilter.name, false) @JvmStatic val isAutoDownloadAllowed: Boolean @@ -158,7 +156,7 @@ object NetworkUtils { val autodownloadSelectedNetworks: Array get() { - val selectedNetWorks = appPrefs.getString(PREF_AUTODL_SELECTED_NETWORKS, "") + val selectedNetWorks = appPrefs.getString(UserPreferences.Prefs.prefAutodownloadSelectedNetworks.name, "") return selectedNetWorks?.split(",")?.toTypedArray() ?: arrayOf() } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt index 46a3513f..02ff135b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt @@ -3,7 +3,7 @@ package ac.mdiq.podcini.playback.base import ac.mdiq.podcini.playback.base.InTheatre.curMedia import ac.mdiq.podcini.playback.base.InTheatre.curState import ac.mdiq.podcini.preferences.UserPreferences -import ac.mdiq.podcini.preferences.UserPreferences.PREF_PLAYBACK_SPEED +import ac.mdiq.podcini.preferences.UserPreferences.Prefs.prefPlaybackSpeed import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.preferences.UserPreferences.setPlaybackSpeed import ac.mdiq.podcini.preferences.UserPreferences.videoPlaybackSpeed @@ -308,7 +308,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont val audioPlaybackSpeed: Float get() { try { - return appPrefs.getString(PREF_PLAYBACK_SPEED, "1.00")!!.toFloat() + return appPrefs.getString(UserPreferences.Prefs.prefPlaybackSpeed.name, "1.00")!!.toFloat() } catch (e: NumberFormatException) { Log.e(TAG, Log.getStackTraceString(e)) setPlaybackSpeed(1.0f) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt index c648bb91..ca7b5f8b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt @@ -23,26 +23,18 @@ import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableFrom import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableTo import ac.mdiq.podcini.preferences.SleepTimerPreferences.isInTimeRange import ac.mdiq.podcini.preferences.SleepTimerPreferences.timerMillis -import ac.mdiq.podcini.preferences.UserPreferences.PREF_FAVORITE_KEEPS_EPISODE -import ac.mdiq.podcini.preferences.UserPreferences.PREF_FOLLOW_QUEUE -import ac.mdiq.podcini.preferences.UserPreferences.PREF_HARDWARE_FORWARD_BUTTON -import ac.mdiq.podcini.preferences.UserPreferences.PREF_HARDWARE_PREVIOUS_BUTTON -import ac.mdiq.podcini.preferences.UserPreferences.PREF_PAUSE_ON_HEADSET_DISCONNECT -import ac.mdiq.podcini.preferences.UserPreferences.PREF_PERSISTENT_NOTIFICATION -import ac.mdiq.podcini.preferences.UserPreferences.PREF_SKIP_KEEPS_EPISODE -import ac.mdiq.podcini.preferences.UserPreferences.PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT -import ac.mdiq.podcini.preferences.UserPreferences.PREF_UNPAUSE_ON_HEADSET_RECONNECT +import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs -import ac.mdiq.podcini.preferences.UserPreferences.shouldAutoDeleteItem -import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue import ac.mdiq.podcini.receiver.MediaButtonReceiver import ac.mdiq.podcini.storage.database.Episodes.addToHistory import ac.mdiq.podcini.storage.database.Episodes.deleteMediaSync import ac.mdiq.podcini.storage.database.Episodes.getEpisodeByGuidOrUrl import ac.mdiq.podcini.storage.database.Episodes.persistEpisode import ac.mdiq.podcini.storage.database.Episodes.setPlayStateSync +import ac.mdiq.podcini.storage.database.Episodes.shouldDeleteRemoveFromQueue +import ac.mdiq.podcini.storage.database.Feeds.shouldAutoDeleteItem import ac.mdiq.podcini.storage.database.Queues.addToQueue import ac.mdiq.podcini.storage.database.Queues.removeFromQueueSync import ac.mdiq.podcini.storage.database.RealmDB.realm @@ -329,11 +321,11 @@ class PlaybackService : MediaSessionService() { } fun shouldSkipKeepEpisode(): Boolean { - return appPrefs.getBoolean(PREF_SKIP_KEEPS_EPISODE, true) + return appPrefs.getBoolean(UserPreferences.Prefs.prefSkipKeepsEpisode.name, true) } fun shouldFavoriteKeepEpisode(): Boolean { - return appPrefs.getBoolean(PREF_FAVORITE_KEEPS_EPISODE, true) + return appPrefs.getBoolean(UserPreferences.Prefs.prefFavoriteKeepsEpisode.name, true) } override fun onPlaybackStart(playable: Playable, position: Int) { @@ -773,7 +765,7 @@ class PlaybackService : MediaSessionService() { else PendingIntent.getService(this, R.id.pending_intent_allow_stream_always, intentAlwaysAllow, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) - val builder = NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID_USER_ACTION) + val builder = NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID.user_action.name) .setSmallIcon(R.drawable.ic_notification_stream) .setContentTitle(getString(R.string.confirm_mobile_streaming_notification_title)) .setContentText(getString(R.string.confirm_mobile_streaming_notification_message)) @@ -1078,10 +1070,11 @@ class PlaybackService : MediaSessionService() { val item = (curMedia as? EpisodeMedia)?.episode ?: currentitem if (item?.feed?.id == event.feed.id) { item.feed = null - if (MediaPlayerBase.status == PlayerStatus.PLAYING) { - mPlayer?.pause(abandonFocus = false, reinit = false) - mPlayer?.resume() - } +// seems no need to pause?? +// if (MediaPlayerBase.status == PlayerStatus.PLAYING) { +// mPlayer?.pause(abandonFocus = false, reinit = false) +// mPlayer?.resume() +// } } } @@ -1187,31 +1180,31 @@ class PlaybackService : MediaSessionService() { * @return `true` if notifications are persistent, `false` otherwise */ val isPersistNotify: Boolean - get() = appPrefs.getBoolean(PREF_PERSISTENT_NOTIFICATION, true) + get() = appPrefs.getBoolean(UserPreferences.Prefs.prefPersistNotify.name, true) val isPauseOnHeadsetDisconnect: Boolean - get() = appPrefs.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true) + get() = appPrefs.getBoolean(UserPreferences.Prefs.prefPauseOnHeadsetDisconnect.name, true) val isUnpauseOnHeadsetReconnect: Boolean - get() = appPrefs.getBoolean(PREF_UNPAUSE_ON_HEADSET_RECONNECT, true) + get() = appPrefs.getBoolean(UserPreferences.Prefs.prefUnpauseOnHeadsetReconnect.name, true) val isUnpauseOnBluetoothReconnect: Boolean - get() = appPrefs.getBoolean(PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT, false) + get() = appPrefs.getBoolean(UserPreferences.Prefs.prefUnpauseOnBluetoothReconnect.name, false) val hardwareForwardButton: Int - get() = appPrefs.getString(PREF_HARDWARE_FORWARD_BUTTON, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD.toString())!!.toInt() + get() = appPrefs.getString(UserPreferences.Prefs.prefHardwareForwardButton.name, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD.toString())!!.toInt() val hardwarePreviousButton: Int - get() = appPrefs.getString(PREF_HARDWARE_PREVIOUS_BUTTON, KeyEvent.KEYCODE_MEDIA_REWIND.toString())!!.toInt() + get() = appPrefs.getString(UserPreferences.Prefs.prefHardwarePreviousButton.name, KeyEvent.KEYCODE_MEDIA_REWIND.toString())!!.toInt() /** * Set to true to enable Continuous Playback */ @set:VisibleForTesting var isFollowQueue: Boolean - get() = appPrefs.getBoolean(PREF_FOLLOW_QUEUE, true) + get() = appPrefs.getBoolean(UserPreferences.Prefs.prefFollowQueue.name, true) set(value) { - appPrefs.edit().putBoolean(PREF_FOLLOW_QUEUE, value).apply() + appPrefs.edit().putBoolean(UserPreferences.Prefs.prefFollowQueue.name, value).apply() } fun updateVolumeIfNecessary(mediaPlayer: MediaPlayerBase, feedId: Long, volumeAdaptionSetting: VolumeAdaptionSetting) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/OpmlBackupAgent.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/OpmlBackupAgent.kt index 5256db0b..fc265027 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/OpmlBackupAgent.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/OpmlBackupAgent.kt @@ -3,7 +3,6 @@ package ac.mdiq.podcini.preferences import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlReader import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlWriter -import ac.mdiq.podcini.preferences.UserPreferences.PREF_OPML_BACKUP import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.storage.database.Feeds.getFeedList import ac.mdiq.podcini.storage.database.Feeds.updateFeed @@ -16,8 +15,10 @@ import android.app.backup.BackupHelper import android.content.Context import android.os.ParcelFileDescriptor import android.util.Log +import android.widget.Toast import androidx.annotation.OptIn import androidx.media3.common.util.UnstableApi +import androidx.preference.PreferenceManager import org.apache.commons.io.IOUtils import org.xmlpull.v1.XmlPullParserException import java.io.* @@ -30,14 +31,13 @@ import java.security.NoSuchAlgorithmException class OpmlBackupAgent : BackupAgentHelper() { - val isAutoBackupOPML: Boolean - get() = appPrefs.getBoolean(PREF_OPML_BACKUP, true) override fun onCreate() { + val isAutoBackupOPML = appPrefs.getBoolean(UserPreferences.Prefs.prefOPMLBackup.name, true) if (isAutoBackupOPML) { - Logd(TAG, "Backup enabled in preferences") + Logd(TAG, "Backup of OPML enabled in preferences") addHelper(OPML_BACKUP_KEY, OpmlBackupHelper(this)) - } else Logd(TAG, "Backup disabled in preferences") + } else Logd(TAG, "Backup of OPML disabled in preferences") } /** @@ -65,7 +65,6 @@ class OpmlBackupAgent : BackupAgentHelper() { try { // Write OPML OpmlWriter().writeDocument(getFeedList(), writer, mContext) - // Compare checksum of new and old file to see if we need to perform a backup at all if (digester != null) { val newChecksum = digester.digest() @@ -86,7 +85,6 @@ class OpmlBackupAgent : BackupAgentHelper() { } writeNewStateDescription(newState, newChecksum) } - Logd(TAG, "Backing up OPML") val bytes = byteStream.toByteArray() data.writeEntityHeader(OPML_ENTITY_KEY, bytes.size) @@ -97,7 +95,6 @@ class OpmlBackupAgent : BackupAgentHelper() { IOUtils.closeQuietly(writer) } } - @OptIn(UnstableApi::class) override fun restoreEntity(data: BackupDataInputStream) { Logd(TAG, "Backup restore") if (OPML_ENTITY_KEY != data.key) { @@ -113,14 +110,22 @@ class OpmlBackupAgent : BackupAgentHelper() { reader = InputStreamReader(data, Charset.forName("UTF-8")) } try { - val opmlElements = OpmlReader().readDocument(reader) - mChecksum = digester?.digest()?: byteArrayOf() - for (opmlElem in opmlElements) { - val feed = Feed(opmlElem.xmlUrl, null, opmlElem.text) - feed.episodes.clear() - updateFeed(mContext, feed, false) + mChecksum = digester?.digest() ?: byteArrayOf() + BufferedReader(reader).use { bufferedReader -> + val tempFile = File.createTempFile("opml_restored", ".tmp", mContext.filesDir) + FileWriter(tempFile).use { fileWriter -> + while (true) { + val line = bufferedReader.readLine() ?: break + fileWriter.write(line) + fileWriter.write(System.lineSeparator()) // Write a newline character + } + } + } + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext) + with(sharedPreferences.edit()) { + putBoolean(UserPreferences.Prefs.prefOPMLRestore.name, true) + apply() } - runOnce(mContext) } catch (e: XmlPullParserException) { Log.e(TAG, "Error while parsing the OPML file", e) } catch (e: IOException) { @@ -129,11 +134,9 @@ class OpmlBackupAgent : BackupAgentHelper() { IOUtils.closeQuietly(reader) } } - override fun writeNewStateDescription(newState: ParcelFileDescriptor) { writeNewStateDescription(newState, mChecksum) } - /** * Writes the new state description, which is the checksum of the OPML file. * @param newState @@ -161,6 +164,25 @@ class OpmlBackupAgent : BackupAgentHelper() { companion object { private val TAG: String = OpmlBackupAgent::class.simpleName ?: "Anonymous" private const val OPML_BACKUP_KEY = "opml" - private const val PREF_BACKUP_ENABLED: String = "backup_enabled" + + val isOPMLRestared: Boolean + get() = appPrefs.getBoolean(UserPreferences.Prefs.prefOPMLRestore.name, true) + + fun performRestore(context: Context) { + Logd(TAG, "performRestore") + val tempFile = File.createTempFile("opml_restored", ".tmp", context.filesDir) + if (tempFile.exists()) { + val reader = FileReader(tempFile) + val opmlElements = OpmlReader().readDocument(reader) + for (opmlElem in opmlElements) { + val feed = Feed(opmlElem.xmlUrl, null, opmlElem.text) + feed.episodes.clear() + updateFeed(context, feed, false) + } + runOnce(context) + } else { + Toast.makeText(context, "No backup data found", Toast.LENGTH_SHORT).show() + } + } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/SleepTimerPreferences.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/SleepTimerPreferences.kt index 6cbade62..09bd0a12 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/SleepTimerPreferences.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/SleepTimerPreferences.kt @@ -8,14 +8,15 @@ import java.util.concurrent.TimeUnit object SleepTimerPreferences { private val TAG: String = SleepTimerPreferences::class.simpleName ?: "Anonymous" - const val PREF_NAME: String = "SleepTimerDialog" - private const val PREF_VALUE = "LastValue" - - private const val PREF_VIBRATE = "Vibrate" - private const val PREF_SHAKE_TO_RESET = "ShakeToReset" - private const val PREF_AUTO_ENABLE = "AutoEnable" - private const val PREF_AUTO_ENABLE_FROM = "AutoEnableFrom" - private const val PREF_AUTO_ENABLE_TO = "AutoEnableTo" + private enum class Prefs { + SleepTimerDialog, + LastValue, + Vibrate, + ShakeToReset, + AutoEnable, + AutoEnableFrom, + AutoEnableTo + } private const val DEFAULT_LAST_TIMER = "15" private const val DEFAULT_AUTO_ENABLE_FROM = 22 @@ -31,17 +32,17 @@ object SleepTimerPreferences { @JvmStatic fun init(context: Context) { Logd(TAG, "Creating new instance of SleepTimerPreferences") - prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + prefs = context.getSharedPreferences(Prefs.SleepTimerDialog.name, Context.MODE_PRIVATE) } @JvmStatic fun setLastTimer(value: String?) { - prefs!!.edit().putString(PREF_VALUE, value).apply() + prefs!!.edit().putString(Prefs.LastValue.name, value).apply() } @JvmStatic fun lastTimerValue(): String? { - return prefs!!.getString(PREF_VALUE, DEFAULT_LAST_TIMER) + return prefs!!.getString(Prefs.LastValue.name, DEFAULT_LAST_TIMER) } @JvmStatic @@ -52,52 +53,52 @@ object SleepTimerPreferences { @JvmStatic fun setVibrate(vibrate: Boolean) { - prefs!!.edit().putBoolean(PREF_VIBRATE, vibrate).apply() + prefs!!.edit().putBoolean(Prefs.Vibrate.name, vibrate).apply() } @JvmStatic fun vibrate(): Boolean { - return prefs!!.getBoolean(PREF_VIBRATE, false) + return prefs!!.getBoolean(Prefs.Vibrate.name, false) } @JvmStatic fun setShakeToReset(shakeToReset: Boolean) { - prefs!!.edit().putBoolean(PREF_SHAKE_TO_RESET, shakeToReset).apply() + prefs!!.edit().putBoolean(Prefs.ShakeToReset.name, shakeToReset).apply() } @JvmStatic fun shakeToReset(): Boolean { - return prefs!!.getBoolean(PREF_SHAKE_TO_RESET, true) + return prefs!!.getBoolean(Prefs.ShakeToReset.name, true) } @JvmStatic fun setAutoEnable(autoEnable: Boolean) { - prefs!!.edit().putBoolean(PREF_AUTO_ENABLE, autoEnable).apply() + prefs!!.edit().putBoolean(Prefs.AutoEnable.name, autoEnable).apply() } @JvmStatic fun autoEnable(): Boolean { - return prefs!!.getBoolean(PREF_AUTO_ENABLE, false) + return prefs!!.getBoolean(Prefs.AutoEnable.name, false) } @JvmStatic fun setAutoEnableFrom(hourOfDay: Int) { - prefs!!.edit().putInt(PREF_AUTO_ENABLE_FROM, hourOfDay).apply() + prefs!!.edit().putInt(Prefs.AutoEnableFrom.name, hourOfDay).apply() } @JvmStatic fun autoEnableFrom(): Int { - return prefs!!.getInt(PREF_AUTO_ENABLE_FROM, DEFAULT_AUTO_ENABLE_FROM) + return prefs!!.getInt(Prefs.AutoEnableFrom.name, DEFAULT_AUTO_ENABLE_FROM) } @JvmStatic fun setAutoEnableTo(hourOfDay: Int) { - prefs!!.edit().putInt(PREF_AUTO_ENABLE_TO, hourOfDay).apply() + prefs!!.edit().putInt(Prefs.AutoEnableTo.name, hourOfDay).apply() } @JvmStatic fun autoEnableTo(): Int { - return prefs!!.getInt(PREF_AUTO_ENABLE_TO, DEFAULT_AUTO_ENABLE_TO) + return prefs!!.getInt(Prefs.AutoEnableTo.name, DEFAULT_AUTO_ENABLE_TO) } @JvmStatic diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/UsageStatistics.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/UsageStatistics.kt index dfed66c8..fdf0a861 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/UsageStatistics.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/UsageStatistics.kt @@ -13,7 +13,7 @@ import kotlin.math.abs * when called. */ object UsageStatistics { - private const val PREF_DB_NAME = "UsageStatistics" + private const val PREF_NAME = "UsageStatistics" private const val MOVING_AVERAGE_WEIGHT = 0.8f private const val MOVING_AVERAGE_BIAS_THRESHOLD = 0.1f private const val SUFFIX_HIDDEN = "_hidden" @@ -31,7 +31,7 @@ object UsageStatistics { */ @JvmStatic fun init(context: Context) { - prefs = context.getSharedPreferences(PREF_DB_NAME, Context.MODE_PRIVATE) + prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) } @JvmStatic diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt index c9844b1e..2393b310 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt @@ -1,6 +1,5 @@ package ac.mdiq.podcini.preferences -import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.storage.model.ProxyConfig import ac.mdiq.podcini.storage.utils.FilesUtils import ac.mdiq.podcini.storage.utils.FilesUtils.createNoMediaFile @@ -22,87 +21,89 @@ import java.net.Proxy object UserPreferences { private val TAG: String = UserPreferences::class.simpleName ?: "Anonymous" - const val PREF_OPML_BACKUP = "prefOPMLBackup" - - // User Interface - const val PREF_THEME: String = "prefTheme" - const val PREF_THEME_BLACK: String = "prefThemeBlack" - const val PREF_TINTED_COLORS: String = "prefTintedColors" - const val PREF_HIDDEN_DRAWER_ITEMS: String = "prefHiddenDrawerItems" - const val PREF_DRAWER_FEED_ORDER: String = "prefDrawerFeedOrder" - const val PREF_DRAWER_FEED_ORDER_DIRECTION: String = "prefDrawerFeedOrderDir" - const val PREF_FEED_GRID_LAYOUT: String = "prefFeedGridLayout" -// const val PREF_DRAWER_FEED_COUNTER: String = "prefDrawerFeedIndicator" - const val PREF_EXPANDED_NOTIFICATION: String = "prefExpandNotify" - const val PREF_USE_EPISODE_COVER: String = "prefEpisodeCover" - const val PREF_SHOW_TIME_LEFT: String = "showTimeLeft" - const val PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify" - const val PREF_FULL_NOTIFICATION_BUTTONS: String = "prefFullNotificationButtons" - const val PREF_SHOW_DOWNLOAD_REPORT = "prefShowDownloadReport" - const val PREF_DEFAULT_PAGE: String = "prefDefaultPage" - private const val PREF_BACK_OPENS_DRAWER: String = "prefBackButtonOpensDrawer" - - const val PREF_QUEUE_KEEP_SORTED: String = "prefQueueKeepSorted" - const val PREF_QUEUE_KEEP_SORTED_ORDER: String = "prefQueueKeepSortedOrder" - const val PREF_DOWNLOADS_SORTED_ORDER = "prefDownloadSortedOrder" -// private const val PREF_HISTORY_SORTED_ORDER = "prefHistorySortedOrder" - - // Episodes - const val PREF_SORT_ALL_EPISODES: String = "prefEpisodesSort" - const val PREF_FILTER_ALL_EPISODES: String = "prefEpisodesFilter" - - // Playback - const val PREF_PAUSE_ON_HEADSET_DISCONNECT: String = "prefPauseOnHeadsetDisconnect" - const val PREF_UNPAUSE_ON_HEADSET_RECONNECT: String = "prefUnpauseOnHeadsetReconnect" - const val PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT: String = "prefUnpauseOnBluetoothReconnect" - const val PREF_HARDWARE_FORWARD_BUTTON: String = "prefHardwareForwardButton" - const val PREF_HARDWARE_PREVIOUS_BUTTON: String = "prefHardwarePreviousButton" - const val PREF_FOLLOW_QUEUE: String = "prefFollowQueue" - const val PREF_SKIP_KEEPS_EPISODE: String = "prefSkipKeepsEpisode" - const val PREF_REMOVDE_FROM_QUEUE_MARKED_PLAYED: String = "prefRemoveFromQueueMarkedPlayed" - const val PREF_FAVORITE_KEEPS_EPISODE = "prefFavoriteKeepsEpisode" - private const val PREF_AUTO_DELETE = "prefAutoDelete" - private const val PREF_AUTO_DELETE_LOCAL = "prefAutoDeleteLocal" - const val PREF_SMART_MARK_AS_PLAYED_SECS: String = "prefSmartMarkAsPlayedSecs" - const val PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray" - private const val PREF_FALLBACK_SPEED = "prefFallbackSpeed" - private const val PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS: String = "prefPauseForFocusLoss" - private const val PREF_TIME_RESPECTS_SPEED = "prefPlaybackTimeRespectsSpeed" - private const val PREF_STREAM_OVER_DOWNLOAD: String = "prefStreamOverDownload" - private const val PREF_SPEEDFORWRD_SPEED = "prefSpeedforwardSpeed" - - // Network - const val PREF_ENQUEUE_DOWNLOADED = "prefEnqueueDownloaded" - const val PREF_ENQUEUE_LOCATION: String = "prefEnqueueLocation" - const val PREF_UPDATE_INTERVAL: String = "prefAutoUpdateIntervall" - const val PREF_MOBILE_UPDATE = "prefMobileUpdateTypes" - const val PREF_EPISODE_CLEANUP: String = "prefEpisodeCleanup" - const val PREF_EPISODE_CACHE_SIZE: String = "prefEpisodeCacheSize" - const val PREF_ENABLE_AUTODL: String = "prefEnableAutoDl" - const val PREF_ENABLE_AUTODL_ON_BATTERY: String = "prefEnableAutoDownloadOnBattery" - const val PREF_ENABLE_AUTODL_WIFI_FILTER: String = "prefEnableAutoDownloadWifiFilter" - const val PREF_AUTODL_SELECTED_NETWORKS = "prefAutodownloadSelectedNetworks" - private const val PREF_PROXY_TYPE = "prefProxyType" - private const val PREF_PROXY_HOST = "prefProxyHost" - private const val PREF_PROXY_PORT = "prefProxyPort" - private const val PREF_PROXY_USER = "prefProxyUser" - private const val PREF_PROXY_PASSWORD = "prefProxyPassword" - - // Services - const val PREF_GPODNET_NOTIFICATIONS = "pref_gpodnet_notifications" - - // Other -// const val PREF_DATA_FOLDER = "prefDataFolder" - const val PREF_DELETE_REMOVES_FROM_QUEUE: String = "prefDeleteRemovesFromQueue" - - // Mediaplayer - const val PREF_PLAYBACK_SPEED = "prefPlaybackSpeed" - private const val PREF_VIDEO_PLAYBACK_SPEED = "prefVideoPlaybackSpeed" - private const val PREF_PLAYBACK_SKIP_SILENCE: String = "prefSkipSilence" - private const val PREF_FAST_FORWARD_SECS = "prefFastForwardSecs" - private const val PREF_REWIND_SECS = "prefRewindSecs" - const val PREF_QUEUE_LOCKED = "prefQueueLocked" - private const val PREF_VIDEO_MODE = "prefVideoPlaybackMode" + @Suppress("EnumEntryName") + enum class Prefs { + prefOPMLBackup, + prefOPMLRestore, + + // User Interface + prefTheme, + prefThemeBlack, + prefTintedColors, + prefHiddenDrawerItems, + prefDrawerFeedOrder, + prefDrawerFeedOrderDir, + prefFeedGridLayout, + prefExpandNotify, + prefEpisodeCover, + showTimeLeft, + prefPersistNotify, + prefFullNotificationButtons, + prefShowDownloadReport, + prefDefaultPage, + prefBackButtonOpensDrawer, + + prefQueueKeepSorted, + prefQueueKeepSortedOrder, + prefDownloadSortedOrder, + + // Episodes + prefEpisodesSort, + prefEpisodesFilter, + + // Playback + prefPauseOnHeadsetDisconnect, + prefUnpauseOnHeadsetReconnect, + prefUnpauseOnBluetoothReconnect, + prefHardwareForwardButton, + prefHardwarePreviousButton, + prefFollowQueue, + prefSkipKeepsEpisode, + prefRemoveFromQueueMarkedPlayed, + prefFavoriteKeepsEpisode, + prefAutoDelete, + prefAutoDeleteLocal, + prefSmartMarkAsPlayedSecs, + prefPlaybackSpeedArray, + prefFallbackSpeed, + prefPauseForFocusLoss, + prefPlaybackTimeRespectsSpeed, + prefStreamOverDownload, + prefSpeedforwardSpeed, + + // Network + prefEnqueueDownloaded, + prefEnqueueLocation, + prefAutoUpdateIntervall, + prefMobileUpdateTypes, + prefEpisodeCleanup, + prefEpisodeCacheSize, + prefEnableAutoDl, + prefEnableAutoDownloadOnBattery, + prefEnableAutoDownloadWifiFilter, + prefAutodownloadSelectedNetworks, + prefProxyType, + prefProxyHost, + prefProxyPort, + prefProxyUser, + prefProxyPassword, + + // Services + pref_gpodnet_notifications, + + // Other +// prefDataFolder + prefDeleteRemovesFromQueue, + + // Mediaplayer + prefPlaybackSpeed, + prefVideoPlaybackSpeed, + prefSkipSilence, + prefFastForwardSecs, + prefRewindSecs, + prefQueueLocked, + prefVideoPlaybackMode, + } // Experimental const val EPISODE_CLEANUP_QUEUE: Int = -1 @@ -111,63 +112,56 @@ object UserPreferences { const val EPISODE_CLEANUP_DEFAULT: Int = 0 // Constants - const val NOTIFICATION_BUTTON_REWIND: Int = 0 - const val NOTIFICATION_BUTTON_FAST_FORWARD: Int = 1 - private const val NOTIFICATION_BUTTON_SKIP: Int = 2 + enum class NOTIFICATION_BUTTON { + REWIND, + FAST_FORWARD, + SKIP, + NEXT_CHAPTER, + PLAYBACK_SPEED, + } - private const val NOTIFICATION_BUTTON_NEXT_CHAPTER: Int = 3 - private const val NOTIFICATION_BUTTON_PLAYBACK_SPEED: Int = 4 const val EPISODE_CACHE_SIZE_UNLIMITED: Int = -1 -// should match those defined in arrays -// const val FEED_ORDER_COUNTER: Int = 0 - const val FEED_ORDER_UNPLAYED: Int = 0 - const val FEED_ORDER_ALPHABETICAL: Int = 1 - const val FEED_ORDER_LAST_UPDATED: Int = 2 - const val FEED_ORDER_LAST_UNREAD_UPDATED: Int = 3 - const val FEED_ORDER_MOST_PLAYED: Int = 4 - const val FEED_ORDER_DOWNLOADED: Int = 5 - const val FEED_ORDER_DOWNLOADED_UNPLAYED: Int = 6 - const val FEED_ORDER_NEW: Int = 7 + const val DEFAULT_PAGE_REMEMBER: String = "remember" private lateinit var context: Context lateinit var appPrefs: SharedPreferences var theme: ThemePreference - get() = when (appPrefs.getString(PREF_THEME, "system")) { + get() = when (appPrefs.getString(Prefs.prefTheme.name, "system")) { "0" -> ThemePreference.LIGHT "1" -> ThemePreference.DARK else -> ThemePreference.SYSTEM } set(theme) { when (theme) { - ThemePreference.LIGHT -> appPrefs.edit().putString(PREF_THEME, "0").apply() - ThemePreference.DARK -> appPrefs.edit().putString(PREF_THEME, "1").apply() - else -> appPrefs.edit().putString(PREF_THEME, "system").apply() + ThemePreference.LIGHT -> appPrefs.edit().putString(Prefs.prefTheme.name, "0").apply() + ThemePreference.DARK -> appPrefs.edit().putString(Prefs.prefTheme.name, "1").apply() + else -> appPrefs.edit().putString(Prefs.prefTheme.name, "system").apply() } } val isBlackTheme: Boolean - get() = appPrefs.getBoolean(PREF_THEME_BLACK, false) + get() = appPrefs.getBoolean(Prefs.prefThemeBlack.name, false) val isThemeColorTinted: Boolean - get() = Build.VERSION.SDK_INT >= 31 && appPrefs.getBoolean(PREF_TINTED_COLORS, false) + get() = Build.VERSION.SDK_INT >= 31 && appPrefs.getBoolean(Prefs.prefTintedColors.name, false) var hiddenDrawerItems: List get() { - val hiddenItems = appPrefs.getString(PREF_HIDDEN_DRAWER_ITEMS, "") + val hiddenItems = appPrefs.getString(Prefs.prefHiddenDrawerItems.name, "") return hiddenItems?.split(",") ?: listOf() } set(items) { val str = items.joinToString() appPrefs.edit() - .putString(PREF_HIDDEN_DRAWER_ITEMS, str) + .putString(Prefs.prefHiddenDrawerItems.name, str) .apply() } var fullNotificationButtons: List get() { - val buttons = appPrefs.getString(PREF_FULL_NOTIFICATION_BUTTONS, "$NOTIFICATION_BUTTON_SKIP,$NOTIFICATION_BUTTON_PLAYBACK_SPEED")?.split(",") ?: listOf() + val buttons = appPrefs.getString(Prefs.prefFullNotificationButtons.name, "${NOTIFICATION_BUTTON.SKIP.ordinal},${NOTIFICATION_BUTTON.PLAYBACK_SPEED.ordinal}")?.split(",") ?: listOf() val notificationButtons: MutableList = ArrayList() for (button in buttons) { notificationButtons.add(button.toInt()) @@ -177,20 +171,20 @@ object UserPreferences { set(items) { val str = items.joinToString() appPrefs.edit() - .putString(PREF_FULL_NOTIFICATION_BUTTONS, str) + .putString(Prefs.prefFullNotificationButtons.name, str) .apply() } val isAutoDelete: Boolean - get() = appPrefs.getBoolean(PREF_AUTO_DELETE, false) + get() = appPrefs.getBoolean(Prefs.prefAutoDelete.name, false) val isAutoDeleteLocal: Boolean - get() = appPrefs.getBoolean(PREF_AUTO_DELETE_LOCAL, false) + get() = appPrefs.getBoolean(Prefs.prefAutoDeleteLocal.name, false) val videoPlayMode: Int get() { try { - return appPrefs.getString(PREF_VIDEO_MODE, "1")!!.toInt() + return appPrefs.getString(Prefs.prefVideoPlaybackMode.name, "1")!!.toInt() } catch (e: NumberFormatException) { Log.e(TAG, Log.getStackTraceString(e)) setVideoMode(1) @@ -201,7 +195,7 @@ object UserPreferences { var videoPlaybackSpeed: Float get() { try { - return appPrefs.getString(PREF_VIDEO_PLAYBACK_SPEED, "1.00")!!.toFloat() + return appPrefs.getString(Prefs.prefVideoPlaybackSpeed.name, "1.00")!!.toFloat() } catch (e: NumberFormatException) { Log.e(TAG, Log.getStackTraceString(e)) videoPlaybackSpeed = 1.0f @@ -210,14 +204,14 @@ object UserPreferences { } set(speed) { appPrefs.edit() - .putString(PREF_VIDEO_PLAYBACK_SPEED, speed.toString()) + .putString(Prefs.prefVideoPlaybackSpeed.name, speed.toString()) .apply() } var isSkipSilence: Boolean - get() = appPrefs.getBoolean(PREF_PLAYBACK_SKIP_SILENCE, false) + get() = appPrefs.getBoolean(Prefs.prefSkipSilence.name, false) set(skipSilence) { - appPrefs.edit().putBoolean(PREF_PLAYBACK_SKIP_SILENCE, skipSilence).apply() + appPrefs.edit().putBoolean(Prefs.prefSkipSilence.name, skipSilence).apply() } /** @@ -226,22 +220,22 @@ object UserPreferences { * 'unlimited'. */ val episodeCacheSize: Int - get() = appPrefs.getString(PREF_EPISODE_CACHE_SIZE, "20")!!.toInt() + get() = appPrefs.getString(Prefs.prefEpisodeCacheSize.name, "20")!!.toInt() @set:VisibleForTesting var isEnableAutodownload: Boolean - get() = appPrefs.getBoolean(PREF_ENABLE_AUTODL, false) + get() = appPrefs.getBoolean(Prefs.prefEnableAutoDl.name, false) set(enabled) { - appPrefs.edit().putBoolean(PREF_ENABLE_AUTODL, enabled).apply() + appPrefs.edit().putBoolean(Prefs.prefEnableAutoDl.name, enabled).apply() } val isEnableAutodownloadOnBattery: Boolean - get() = appPrefs.getBoolean(PREF_ENABLE_AUTODL_ON_BATTERY, true) + get() = appPrefs.getBoolean(Prefs.prefEnableAutoDownloadOnBattery.name, true) var speedforwardSpeed: Float get() { try { - return appPrefs.getString(PREF_SPEEDFORWRD_SPEED, "0.00")!!.toFloat() + return appPrefs.getString(Prefs.prefSpeedforwardSpeed.name, "0.00")!!.toFloat() } catch (e: NumberFormatException) { Log.e(TAG, Log.getStackTraceString(e)) speedforwardSpeed = 0.0f @@ -249,13 +243,13 @@ object UserPreferences { } } set(speed) { - appPrefs.edit().putString(PREF_SPEEDFORWRD_SPEED, speed.toString()).apply() + appPrefs.edit().putString(Prefs.prefSpeedforwardSpeed.name, speed.toString()).apply() } var fallbackSpeed: Float get() { try { - return appPrefs.getString(PREF_FALLBACK_SPEED, "0.00")!!.toFloat() + return appPrefs.getString(Prefs.prefFallbackSpeed.name, "0.00")!!.toFloat() } catch (e: NumberFormatException) { Log.e(TAG, Log.getStackTraceString(e)) fallbackSpeed = 0.0f @@ -263,58 +257,58 @@ object UserPreferences { } } set(speed) { - appPrefs.edit().putString(PREF_FALLBACK_SPEED, speed.toString()).apply() + appPrefs.edit().putString(Prefs.prefFallbackSpeed.name, speed.toString()).apply() } var fastForwardSecs: Int - get() = appPrefs.getInt(PREF_FAST_FORWARD_SECS, 30) + get() = appPrefs.getInt(Prefs.prefFastForwardSecs.name, 30) set(secs) { - appPrefs.edit().putInt(PREF_FAST_FORWARD_SECS, secs).apply() + appPrefs.edit().putInt(Prefs.prefFastForwardSecs.name, secs).apply() } var rewindSecs: Int - get() = appPrefs.getInt(PREF_REWIND_SECS, 10) + get() = appPrefs.getInt(Prefs.prefRewindSecs.name, 10) set(secs) { - appPrefs.edit().putInt(PREF_REWIND_SECS, secs).apply() + appPrefs.edit().putInt(Prefs.prefRewindSecs.name, secs).apply() } var proxyConfig: ProxyConfig get() { - val type = Proxy.Type.valueOf(appPrefs.getString(PREF_PROXY_TYPE, Proxy.Type.DIRECT.name)!!) - val host = appPrefs.getString(PREF_PROXY_HOST, null) - val port = appPrefs.getInt(PREF_PROXY_PORT, 0) - val username = appPrefs.getString(PREF_PROXY_USER, null) - val password = appPrefs.getString(PREF_PROXY_PASSWORD, null) + val type = Proxy.Type.valueOf(appPrefs.getString(Prefs.prefProxyType.name, Proxy.Type.DIRECT.name)!!) + val host = appPrefs.getString(Prefs.prefProxyHost.name, null) + val port = appPrefs.getInt(Prefs.prefProxyPort.name, 0) + val username = appPrefs.getString(Prefs.prefProxyUser.name, null) + val password = appPrefs.getString(Prefs.prefProxyPassword.name, null) return ProxyConfig(type, host, port, username, password) } set(config) { val editor = appPrefs.edit() - editor.putString(PREF_PROXY_TYPE, config.type.name) - if (config.host.isNullOrEmpty()) editor.remove(PREF_PROXY_HOST) - else editor.putString(PREF_PROXY_HOST, config.host) + editor.putString(Prefs.prefProxyType.name, config.type.name) + if (config.host.isNullOrEmpty()) editor.remove(Prefs.prefProxyHost.name) + else editor.putString(Prefs.prefProxyHost.name, config.host) - if (config.port <= 0 || config.port > 65535) editor.remove(PREF_PROXY_PORT) - else editor.putInt(PREF_PROXY_PORT, config.port) + if (config.port <= 0 || config.port > 65535) editor.remove(Prefs.prefProxyPort.name) + else editor.putInt(Prefs.prefProxyPort.name, config.port) - if (config.username.isNullOrEmpty()) editor.remove(PREF_PROXY_USER) - else editor.putString(PREF_PROXY_USER, config.username) + if (config.username.isNullOrEmpty()) editor.remove(Prefs.prefProxyUser.name) + else editor.putString(Prefs.prefProxyUser.name, config.username) - if (config.password.isNullOrEmpty()) editor.remove(PREF_PROXY_PASSWORD) - else editor.putString(PREF_PROXY_PASSWORD, config.password) + if (config.password.isNullOrEmpty()) editor.remove(Prefs.prefProxyPassword.name) + else editor.putString(Prefs.prefProxyPassword.name, config.password) editor.apply() } var defaultPage: String? - get() = appPrefs.getString(PREF_DEFAULT_PAGE, "SubscriptionsFragment") + get() = appPrefs.getString(Prefs.prefDefaultPage.name, "SubscriptionsFragment") set(defaultPage) { - appPrefs.edit().putString(PREF_DEFAULT_PAGE, defaultPage).apply() + appPrefs.edit().putString(Prefs.prefDefaultPage.name, defaultPage).apply() } var isStreamOverDownload: Boolean - get() = appPrefs.getBoolean(PREF_STREAM_OVER_DOWNLOAD, false) + get() = appPrefs.getBoolean(Prefs.prefStreamOverDownload.name, false) set(stream) { - appPrefs.edit().putBoolean(PREF_STREAM_OVER_DOWNLOAD, stream).apply() + appPrefs.edit().putBoolean(Prefs.prefStreamOverDownload.name, stream).apply() } /** @@ -334,40 +328,34 @@ object UserPreferences { * Helper function to return whether the specified button should be shown on full * notifications. * @param buttonId Either NOTIFICATION_BUTTON_REWIND, NOTIFICATION_BUTTON_FAST_FORWARD, - * NOTIFICATION_BUTTON_SKIP, NOTIFICATION_BUTTON_PLAYBACK_SPEED - * or NOTIFICATION_BUTTON_NEXT_CHAPTER. + * NOTIFICATION_BUTTON.SKIP.ordinal, NOTIFICATION_BUTTON.PLAYBACK_SPEED.ordinal + * or NOTIFICATION_BUTTON.NEXT_CHAPTER.ordinal. * @return `true` if button should be shown, `false` otherwise */ private fun showButtonOnFullNotification(buttonId: Int): Boolean { return fullNotificationButtons.contains(buttonId) } - @JvmStatic - fun shouldAutoDeleteItem(feed: Feed): Boolean { - if (!isAutoDelete) return false - return !feed.isLocalFeed || isAutoDeleteLocal - } - // only used in test fun showSkipOnFullNotification(): Boolean { - return showButtonOnFullNotification(NOTIFICATION_BUTTON_SKIP) + return showButtonOnFullNotification(NOTIFICATION_BUTTON.SKIP.ordinal) } // only used in test fun showNextChapterOnFullNotification(): Boolean { - return showButtonOnFullNotification(NOTIFICATION_BUTTON_NEXT_CHAPTER) + return showButtonOnFullNotification(NOTIFICATION_BUTTON.NEXT_CHAPTER.ordinal) } // only used in test fun showPlaybackSpeedOnFullNotification(): Boolean { - return showButtonOnFullNotification(NOTIFICATION_BUTTON_PLAYBACK_SPEED) + return showButtonOnFullNotification(NOTIFICATION_BUTTON.PLAYBACK_SPEED.ordinal) } /** * @return `true` if we should show remaining time or the duration */ fun shouldShowRemainingTime(): Boolean { - return appPrefs.getBoolean(PREF_SHOW_TIME_LEFT, false) + return appPrefs.getBoolean(Prefs.showTimeLeft.name, false) } /** @@ -376,32 +364,28 @@ object UserPreferences { * @return `true` if we should show remaining time or the duration */ fun setShowRemainTimeSetting(showRemain: Boolean?) { - appPrefs.edit().putBoolean(PREF_SHOW_TIME_LEFT, showRemain!!).apply() - } - - fun shouldDeleteRemoveFromQueue(): Boolean { - return appPrefs.getBoolean(PREF_DELETE_REMOVES_FROM_QUEUE, false) + appPrefs.edit().putBoolean(Prefs.showTimeLeft.name, showRemain!!).apply() } // only used in test fun shouldPauseForFocusLoss(): Boolean { - return appPrefs.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, true) + return appPrefs.getBoolean(Prefs.prefPauseForFocusLoss.name, true) } fun backButtonOpensDrawer(): Boolean { - return appPrefs.getBoolean(PREF_BACK_OPENS_DRAWER, false) + return appPrefs.getBoolean(Prefs.prefBackButtonOpensDrawer.name, false) } fun timeRespectsSpeed(): Boolean { - return appPrefs.getBoolean(PREF_TIME_RESPECTS_SPEED, false) + return appPrefs.getBoolean(Prefs.prefPlaybackTimeRespectsSpeed.name, false) } fun setPlaybackSpeed(speed: Float) { - appPrefs.edit().putString(PREF_PLAYBACK_SPEED, speed.toString()).apply() + appPrefs.edit().putString(Prefs.prefPlaybackSpeed.name, speed.toString()).apply() } fun setVideoMode(mode: Int) { - appPrefs.edit().putString(PREF_VIDEO_MODE, mode.toString()).apply() + appPrefs.edit().putString(Prefs.prefVideoPlaybackMode.name, mode.toString()).apply() } enum class ThemePreference { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/AutoDownloadPreferencesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/AutoDownloadPreferencesFragment.kt index 69ac1fe0..8e1f932c 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/AutoDownloadPreferencesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/AutoDownloadPreferencesFragment.kt @@ -1,5 +1,13 @@ package ac.mdiq.podcini.preferences.fragments +import ac.mdiq.podcini.R +import ac.mdiq.podcini.net.utils.NetworkUtils.autodownloadSelectedNetworks +import ac.mdiq.podcini.net.utils.NetworkUtils.isEnableAutodownloadWifiFilter +import ac.mdiq.podcini.preferences.UserPreferences +import ac.mdiq.podcini.preferences.UserPreferences.appPrefs +import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload +import ac.mdiq.podcini.ui.activity.PreferenceActivity +import ac.mdiq.podcini.util.Logd import android.annotation.SuppressLint import android.app.Activity import android.content.Context @@ -12,16 +20,6 @@ import androidx.preference.CheckBoxPreference import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat -import ac.mdiq.podcini.R -import ac.mdiq.podcini.net.utils.NetworkUtils.autodownloadSelectedNetworks -import ac.mdiq.podcini.net.utils.NetworkUtils.isEnableAutodownloadWifiFilter -import ac.mdiq.podcini.ui.activity.PreferenceActivity -import ac.mdiq.podcini.preferences.UserPreferences -import ac.mdiq.podcini.preferences.UserPreferences.PREF_AUTODL_SELECTED_NETWORKS -import ac.mdiq.podcini.preferences.UserPreferences.appPrefs -import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload -import ac.mdiq.podcini.util.Logd -import java.util.* class AutoDownloadPreferencesFragment : PreferenceFragmentCompat() { private var selectedNetworks: Array? = null @@ -46,14 +44,14 @@ class AutoDownloadPreferencesFragment : PreferenceFragmentCompat() { } private fun setupAutoDownloadScreen() { - findPreference(UserPreferences.PREF_ENABLE_AUTODL)!!.onPreferenceChangeListener = + findPreference(UserPreferences.Prefs.prefEnableAutoDl.name)!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? -> if (newValue is Boolean) checkAutodownloadItemVisibility(newValue) true } - if (Build.VERSION.SDK_INT >= 29) findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER)!!.isVisible = false + if (Build.VERSION.SDK_INT >= 29) findPreference(UserPreferences.Prefs.prefEnableAutoDownloadWifiFilter.name)!!.isVisible = false - findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER)?.onPreferenceChangeListener = + findPreference(UserPreferences.Prefs.prefEnableAutoDownloadWifiFilter.name)?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? -> if (newValue is Boolean) { setSelectedNetworksEnabled(newValue) @@ -63,10 +61,10 @@ class AutoDownloadPreferencesFragment : PreferenceFragmentCompat() { } private fun checkAutodownloadItemVisibility(autoDownload: Boolean) { - findPreference(UserPreferences.PREF_EPISODE_CACHE_SIZE)!!.isEnabled = autoDownload - findPreference(UserPreferences.PREF_ENABLE_AUTODL_ON_BATTERY)!!.isEnabled = autoDownload - findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER)!!.isEnabled = autoDownload - findPreference(UserPreferences.PREF_EPISODE_CLEANUP)!!.isEnabled = autoDownload + findPreference(UserPreferences.Prefs.prefEpisodeCacheSize.name)!!.isEnabled = autoDownload + findPreference(UserPreferences.Prefs.prefEnableAutoDownloadOnBattery.name)!!.isEnabled = autoDownload + findPreference(UserPreferences.Prefs.prefEnableAutoDownloadWifiFilter.name)!!.isEnabled = autoDownload + findPreference(UserPreferences.Prefs.prefEpisodeCleanup.name)!!.isEnabled = autoDownload setSelectedNetworksEnabled(autoDownload && isEnableAutodownloadWifiFilter) } @@ -127,8 +125,8 @@ class AutoDownloadPreferencesFragment : PreferenceFragmentCompat() { } } - fun setAutodownloadSelectedNetworks(value: Array?) { - appPrefs.edit().putString(PREF_AUTODL_SELECTED_NETWORKS, value!!.joinToString()).apply() + private fun setAutodownloadSelectedNetworks(value: Array?) { + appPrefs.edit().putString(UserPreferences.Prefs.prefAutodownloadSelectedNetworks.name, value!!.joinToString()).apply() } private fun clearAutodownloadSelectedNetworsPreference() { @@ -144,7 +142,7 @@ class AutoDownloadPreferencesFragment : PreferenceFragmentCompat() { private fun buildEpisodeCleanupPreference() { val res = requireActivity().resources - val pref = findPreference(UserPreferences.PREF_EPISODE_CLEANUP) + val pref = findPreference(UserPreferences.Prefs.prefEpisodeCleanup.name) val values = res.getStringArray(R.array.episode_cleanup_values) val entries = arrayOfNulls(values.size) for (x in values.indices) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/DownloadsPreferencesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/DownloadsPreferencesFragment.kt index c44c05d7..4c94e90c 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/DownloadsPreferencesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/DownloadsPreferencesFragment.kt @@ -76,18 +76,18 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere appPrefs.unregisterOnSharedPreferenceChangeListener(this) } - override fun onResume() { - super.onResume() -// setDataFolderText() - } +// override fun onResume() { +// super.onResume() +//// setDataFolderText() +// } private fun setupNetworkScreen() { - findPreference(PREF_SCREEN_AUTODL)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.prefAutoDownloadSettings.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { (activity as PreferenceActivity).openScreen(R.xml.preferences_autodownload) true } // validate and set correct value: number of downloads between 1 and 50 (inclusive) - findPreference(PREF_PROXY)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.prefProxy.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { val dialog = ProxyDialog(requireContext()) dialog.show() true @@ -99,7 +99,7 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere // } // true // } - findPreference(PREF_AUTO_DELETE_LOCAL)!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> + findPreference(Prefs.prefAutoDeleteLocal.name)!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> if (blockAutoDeleteLocal && newValue as Boolean) { showAutoDeleteEnableDialog() return@OnPreferenceChangeListener false @@ -113,7 +113,7 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere // } override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { - if (UserPreferences.PREF_UPDATE_INTERVAL == key) restartUpdateAlarm(requireContext(), true) + if (UserPreferences.Prefs.prefAutoUpdateIntervall.name == key) restartUpdateAlarm(requireContext(), true) } private fun showAutoDeleteEnableDialog() { @@ -121,7 +121,7 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere .setMessage(R.string.pref_auto_local_delete_dialog_body) .setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> blockAutoDeleteLocal = false - (findPreference(PREF_AUTO_DELETE_LOCAL) as TwoStatePreference?)!!.isChecked = true + (findPreference(Prefs.prefAutoDeleteLocal.name) as TwoStatePreference?)!!.isChecked = true blockAutoDeleteLocal = true } .setNegativeButton(R.string.cancel_label, null) @@ -439,10 +439,11 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere } } - companion object { - private const val PREF_SCREEN_AUTODL = "prefAutoDownloadSettings" - private const val PREF_AUTO_DELETE_LOCAL = "prefAutoDeleteLocal" - private const val PREF_PROXY = "prefProxy" -// private const val PREF_CHOOSE_DATA_DIR = "prefChooseDataDir" + @Suppress("EnumEntryName") + private enum class Prefs { + prefAutoDownloadSettings, + prefAutoDeleteLocal, + prefProxy, +// prefChooseDataDir, } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt index 636036b4..8c376b99 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt @@ -125,23 +125,23 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { } private fun setupStorageScreen() { - findPreference(PREF_OPML_EXPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(IExport.prefOpmlExport.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { openExportPathPicker(Export.OPML, chooseOpmlExportPathLauncher, OpmlWriter()) true } - findPreference(PREF_HTML_EXPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(IExport.prefHtmlExport.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { openExportPathPicker(Export.HTML, chooseHtmlExportPathLauncher, HtmlWriter()) true } - findPreference(PREF_PROGRESS_EXPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(IExport.prefProgressExport.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { openExportPathPicker(Export.PROGRESS, chooseProgressExportPathLauncher, EpisodesProgressWriter()) true } - findPreference(PREF_PROGRESS_IMPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(IExport.prefProgressImport.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { importEpisodeProgress() true } - findPreference(PREF_OPML_IMPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(IExport.prefOpmlImport.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { try { chooseOpmlImportPathLauncher.launch("*/*") } catch (e: ActivityNotFoundException) { @@ -149,31 +149,31 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { } true } - findPreference(PREF_DATABASE_IMPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(IExport.prefDatabaseImport.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { importDatabase() true } - findPreference(PREF_DATABASE_EXPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(IExport.prefDatabaseExport.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { exportDatabase() true } - findPreference(PREF_PREFERENCES_IMPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(IExport.prefPrefImport.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { importPreferences() true } - findPreference(PREF_PREFERENCES_EXPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(IExport.prefPrefExport.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { exportPreferences() true } - findPreference(PREF_MEDIAFILES_IMPORT)?.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(IExport.prefMediaFilesImport.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { importMediaFiles() true } - findPreference(PREF_MEDIAFILES_EXPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(IExport.prefMediaFilesExport.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { exportMediaFiles() true } - findPreference(PREF_FAVORITE_EXPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(IExport.prefFavoritesExport.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { openExportPathPicker(Export.FAVORITES, chooseFavoritesExportPathLauncher, FavoritesWriter()) true } @@ -960,9 +960,9 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { override fun writeDocument(feeds: List?, writer: Writer?, context: Context) { Logd(TAG, "Starting to write document") val queuedEpisodeActions: MutableList = mutableListOf() - val pausedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.PAUSED), EpisodeSortOrder.DATE_NEW_OLD) - val readItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.PLAYED), EpisodeSortOrder.DATE_NEW_OLD) - val favoriteItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.IS_FAVORITE), EpisodeSortOrder.DATE_NEW_OLD) + val pausedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.paused.name), EpisodeSortOrder.DATE_NEW_OLD) + val readItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.played.name), EpisodeSortOrder.DATE_NEW_OLD) + val favoriteItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.is_favorite.name), EpisodeSortOrder.DATE_NEW_OLD) val comItems = mutableSetOf() comItems.addAll(pausedItems) comItems.addAll(readItems) @@ -1021,7 +1021,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { val favTemplate = IOUtils.toString(favTemplateStream, UTF_8) val feedTemplateStream = context.assets.open(FEED_TEMPLATE) val feedTemplate = IOUtils.toString(feedTemplateStream, UTF_8) - val allFavorites = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.IS_FAVORITE), EpisodeSortOrder.DATE_NEW_OLD) + val allFavorites = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.is_favorite.name), EpisodeSortOrder.DATE_NEW_OLD) val favoritesByFeed = buildFeedMap(allFavorites) writer!!.append(templateParts[0]) for (feedId in favoritesByFeed.keys) { @@ -1117,20 +1117,24 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { } } + private enum class IExport { + prefOpmlExport, + prefOpmlImport, + prefProgressExport, + prefProgressImport, + prefHtmlExport, + prefPrefImport, + prefPrefExport, + prefMediaFilesImport, + prefMediaFilesExport, + prefDatabaseImport, + prefDatabaseExport, + prefFavoritesExport, + } + companion object { private val TAG: String = ImportExportPreferencesFragment::class.simpleName ?: "Anonymous" - private const val PREF_OPML_EXPORT = "prefOpmlExport" - private const val PREF_OPML_IMPORT = "prefOpmlImport" - private const val PREF_PROGRESS_EXPORT = "prefProgressExport" - private const val PREF_PROGRESS_IMPORT = "prefProgressImport" - private const val PREF_HTML_EXPORT = "prefHtmlExport" - private const val PREF_PREFERENCES_IMPORT = "prefPrefImport" - private const val PREF_PREFERENCES_EXPORT = "prefPrefExport" - private const val PREF_MEDIAFILES_IMPORT = "prefMediaFilesImport" - private const val PREF_MEDIAFILES_EXPORT = "prefMediaFilesExport" - private const val PREF_DATABASE_IMPORT = "prefDatabaseImport" - private const val PREF_DATABASE_EXPORT = "prefDatabaseExport" - private const val PREF_FAVORITE_EXPORT = "prefFavoritesExport" + private const val DEFAULT_OPML_OUTPUT_NAME = "podcini-feeds-%s.opml" private const val CONTENT_TYPE_OPML = "text/x-opml" private const val DEFAULT_HTML_OUTPUT_NAME = "podcini-feeds-%s.html" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/MainPreferencesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/MainPreferencesFragment.kt index b0500f27..6765c45c 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/MainPreferencesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/MainPreferencesFragment.kt @@ -14,6 +14,7 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreferenceCompat import com.bytehamster.lib.preferencesearch.SearchPreference class MainPreferencesFragment : PreferenceFragmentCompat() { @@ -33,7 +34,7 @@ class MainPreferencesFragment : PreferenceFragmentCompat() { // Logd("MainPreferencesFragment", "$packageHash ${"ac.mdiq.podcini.R".hashCode()}") when { packageHash != 1329568231 && packageHash != 1297601420 -> { - findPreference(PREF_CATEGORY_PROJECT)!!.isVisible = false + findPreference(Prefs.project.name)!!.isVisible = false val copyrightNotice = Preference(requireContext()) copyrightNotice.setIcon(R.drawable.ic_info_white) copyrightNotice.icon!!.mutate().colorFilter = PorterDuffColorFilter(-0x340000, PorterDuff.Mode.MULTIPLY) @@ -41,7 +42,7 @@ class MainPreferencesFragment : PreferenceFragmentCompat() { + " 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(PREF_CATEGORY_PROJECT)!!.parent!!.addPreference(copyrightNotice) + findPreference(Prefs.project.name)!!.parent!!.addPreference(copyrightNotice) } packageHash == 1297601420 -> { val debugNotice = Preference(requireContext()) @@ -49,7 +50,7 @@ class MainPreferencesFragment : PreferenceFragmentCompat() { 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(PREF_CATEGORY_PROJECT)!!.parent!!.addPreference(debugNotice) + findPreference(Prefs.project.name)!!.parent!!.addPreference(debugNotice) } } } @@ -61,74 +62,65 @@ class MainPreferencesFragment : PreferenceFragmentCompat() { @SuppressLint("CommitTransaction") private fun setupMainScreen() { - findPreference(PREF_SCREEN_USER_INTERFACE)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.prefScreenInterface.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { (activity as PreferenceActivity).openScreen(R.xml.preferences_user_interface) true } - findPreference(PREF_SCREEN_PLAYBACK)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.prefScreenPlayback.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { (activity as PreferenceActivity).openScreen(R.xml.preferences_playback) true } - findPreference(PREF_SCREEN_DOWNLOADS)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.prefScreenDownloads.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { (activity as PreferenceActivity).openScreen(R.xml.preferences_downloads) true } - findPreference(PREF_SCREEN_SYNCHRONIZATION)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.prefScreenSynchronization.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { (activity as PreferenceActivity).openScreen(R.xml.preferences_synchronization) true } - findPreference(PREF_SCREEN_IMPORT_EXPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.prefScreenImportExport.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { (activity as PreferenceActivity).openScreen(R.xml.preferences_import_export) true } - findPreference(PREF_NOTIFICATION)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.notifications.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { (activity as PreferenceActivity).openScreen(R.xml.preferences_notifications) true } - findPreference(PREF_ABOUT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + val switchPreference = findPreference("prefOPMLBackup") + switchPreference?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + if (newValue is Boolean) { + // Restart the app + val intent = context?.packageManager?.getLaunchIntentForPackage(requireContext().packageName) + intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + context?.startActivity(intent) + } + true + } + findPreference(Prefs.prefAbout.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { parentFragmentManager.beginTransaction() .replace(R.id.settingsContainer, AboutFragment()) .addToBackStack(getString(R.string.about_pref)) .commit() true } - findPreference(PREF_DOCUMENTATION)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.prefDocumentation.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini") true } - findPreference(PREF_VIEW_FORUM)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.prefViewForum.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini/discussions") true } - findPreference(PREF_CONTRIBUTE)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.prefContribute.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini") true } - findPreference(PREF_SEND_BUG_REPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.prefSendBugReport.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { startActivity(Intent(activity, BugReportActivity::class.java)) true } } -// private val localizedWebsiteLink: String -// get() { -// try { -// requireContext().assets.open("website-languages.txt").use { `is` -> -// val languages = IOUtils.toString(`is`, StandardCharsets.UTF_8.name()).split("\n".toRegex()) -// .dropLastWhile { it.isEmpty() } -// .toTypedArray() -// val deviceLanguage = Locale.getDefault().language -// return if (ArrayUtils.contains(languages, deviceLanguage) && "en" != deviceLanguage) { -// "https://podcini.org/$deviceLanguage" -// } else { -// "https://podcini.org" -// } -// } -// } catch (e: IOException) { -// throw RuntimeException(e) -// } -// } - private fun setupSearch() { val searchPreference = findPreference("searchPreference") val config = searchPreference!!.searchConfiguration @@ -152,18 +144,19 @@ class MainPreferencesFragment : PreferenceFragmentCompat() { .addBreadcrumb(getTitleOfPage(R.xml.preferences_swipe)) } - companion object { - private const val PREF_SCREEN_USER_INTERFACE = "prefScreenInterface" - private const val PREF_SCREEN_PLAYBACK = "prefScreenPlayback" - private const val PREF_SCREEN_DOWNLOADS = "prefScreenDownloads" - private const val PREF_SCREEN_IMPORT_EXPORT = "prefScreenImportExport" - private const val PREF_SCREEN_SYNCHRONIZATION = "prefScreenSynchronization" - private const val PREF_DOCUMENTATION = "prefDocumentation" - private const val PREF_VIEW_FORUM = "prefViewForum" - private const val PREF_SEND_BUG_REPORT = "prefSendBugReport" - private const val PREF_CATEGORY_PROJECT = "project" - private const val PREF_ABOUT = "prefAbout" - private const val PREF_NOTIFICATION = "notifications" - private const val PREF_CONTRIBUTE = "prefContribute" + @Suppress("EnumEntryName") + private enum class Prefs { + prefScreenInterface, + prefScreenPlayback, + prefScreenDownloads, + prefScreenImportExport, + prefScreenSynchronization, + prefDocumentation, + prefViewForum, + prefSendBugReport, + project, + prefAbout, + notifications, + prefContribute, } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/PlaybackPreferencesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/PlaybackPreferencesFragment.kt index a4923ec2..4b6087f8 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/PlaybackPreferencesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/PlaybackPreferencesFragment.kt @@ -46,32 +46,32 @@ class PlaybackPreferencesFragment : PreferenceFragmentCompat() { @OptIn(UnstableApi::class) private fun setupPlaybackScreen() { - findPreference(PREF_PLAYBACK_SPEED_LAUNCHER)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.prefPlaybackSpeedLauncher.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { VariableSpeedDialog.newInstance(booleanArrayOf(false, false, true),2)?.show(childFragmentManager, null) true } - findPreference(PREF_PLAYBACK_REWIND_DELTA_LAUNCHER)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.prefPlaybackRewindDeltaLauncher.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null) true } - findPreference(PREF_PLAYBACK_VIDEO_MODE_LAUNCHER)?.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.prefPlaybackVideoModeLauncher.name)?.onPreferenceClickListener = Preference.OnPreferenceClickListener { VideoModeDialog.showDialog(requireContext()) true } - findPreference(PREF_PLAYBACK_SPEED_FORWARD_LAUNCHER)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.prefPlaybackSpeedForwardLauncher.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { EditForwardSpeedDialog(requireActivity()).show() true } - findPreference(PREF_PLAYBACK_FALLBACK_SPEED_LAUNCHER)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.prefPlaybackFallbackSpeedLauncher.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { EditFallbackSpeedDialog(requireActivity()).show() true } - findPreference(PREF_PLAYBACK_FAST_FORWARD_DELTA_LAUNCHER)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.prefPlaybackFastForwardDeltaLauncher.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null) true } - findPreference(PREF_PLAYBACK_PREFER_STREAMING)!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, _: Any? -> + findPreference(Prefs.prefStreamOverDownload.name)!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, _: Any? -> // Update all visible lists to reflect new streaming action button // TODO: need another event type? EventFlow.postEvent(FlowEvent.EpisodePlayedEvent()) @@ -80,8 +80,8 @@ class PlaybackPreferencesFragment : PreferenceFragmentCompat() { true } if (Build.VERSION.SDK_INT >= 31) { - findPreference(UserPreferences.PREF_UNPAUSE_ON_HEADSET_RECONNECT)!!.isVisible = false - findPreference(UserPreferences.PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT)!!.isVisible = false + findPreference(UserPreferences.Prefs.prefUnpauseOnHeadsetReconnect.name)!!.isVisible = false + findPreference(UserPreferences.Prefs.prefUnpauseOnBluetoothReconnect.name)!!.isVisible = false } buildEnqueueLocationPreference() } @@ -97,7 +97,7 @@ class PlaybackPreferencesFragment : PreferenceFragmentCompat() { } } - val pref = requirePreference(UserPreferences.PREF_ENQUEUE_LOCATION) + val pref = requirePreference(UserPreferences.Prefs.prefEnqueueLocation.name) pref.summary = res.getString(R.string.pref_enqueue_location_sum, options[pref.value]) pref.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? -> if (newValue !is String) return@OnPreferenceChangeListener false @@ -114,7 +114,7 @@ class PlaybackPreferencesFragment : PreferenceFragmentCompat() { private fun buildSmartMarkAsPlayedPreference() { val res = requireActivity().resources - val pref = findPreference(UserPreferences.PREF_SMART_MARK_AS_PLAYED_SECS) + val pref = findPreference(UserPreferences.Prefs.prefSmartMarkAsPlayedSecs.name) val values = res.getStringArray(R.array.smart_mark_as_played_values) val entries = arrayOfNulls(values.size) for (x in values.indices) { @@ -202,13 +202,13 @@ class PlaybackPreferencesFragment : PreferenceFragmentCompat() { } } - companion object { - private const val PREF_PLAYBACK_SPEED_LAUNCHER = "prefPlaybackSpeedLauncher" - private const val PREF_PLAYBACK_REWIND_DELTA_LAUNCHER = "prefPlaybackRewindDeltaLauncher" - private const val PREF_PLAYBACK_FALLBACK_SPEED_LAUNCHER = "prefPlaybackFallbackSpeedLauncher" - private const val PREF_PLAYBACK_SPEED_FORWARD_LAUNCHER = "prefPlaybackSpeedForwardLauncher" - private const val PREF_PLAYBACK_FAST_FORWARD_DELTA_LAUNCHER = "prefPlaybackFastForwardDeltaLauncher" - private const val PREF_PLAYBACK_PREFER_STREAMING = "prefStreamOverDownload" - private const val PREF_PLAYBACK_VIDEO_MODE_LAUNCHER = "prefPlaybackVideoModeLauncher" + private enum class Prefs { + prefPlaybackSpeedLauncher, + prefPlaybackRewindDeltaLauncher, + prefPlaybackFallbackSpeedLauncher, + prefPlaybackSpeedForwardLauncher, + prefPlaybackFastForwardDeltaLauncher, + prefStreamOverDownload, + prefPlaybackVideoModeLauncher, } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/SwipePreferencesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/SwipePreferencesFragment.kt index 8ece8f3b..f419bce9 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/SwipePreferencesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/SwipePreferencesFragment.kt @@ -14,31 +14,31 @@ class SwipePreferencesFragment : PreferenceFragmentCompat() { @OptIn(UnstableApi::class) override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.preferences_swipe) - findPreference(PREF_SWIPE_QUEUE)?.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.prefSwipeQueue.name)?.onPreferenceClickListener = Preference.OnPreferenceClickListener { SwipeActionsDialog(requireContext(), QueueFragment.TAG).show(object : SwipeActionsDialog.Callback { override fun onCall() {} }) true } - findPreference(PREF_SWIPE_EPISODES)?.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.prefSwipeEpisodes.name)?.onPreferenceClickListener = Preference.OnPreferenceClickListener { SwipeActionsDialog(requireContext(), AllEpisodesFragment.TAG).show (object : SwipeActionsDialog.Callback { override fun onCall() {} }) true } - findPreference(PREF_SWIPE_DOWNLOADS)?.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.prefSwipeDownloads.name)?.onPreferenceClickListener = Preference.OnPreferenceClickListener { SwipeActionsDialog(requireContext(), DownloadsFragment.TAG).show (object : SwipeActionsDialog.Callback { override fun onCall() {} }) true } - findPreference(PREF_SWIPE_FEED)?.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.prefSwipeFeed.name)?.onPreferenceClickListener = Preference.OnPreferenceClickListener { SwipeActionsDialog(requireContext(), FeedEpisodesFragment.TAG).show (object : SwipeActionsDialog.Callback { override fun onCall() {} }) true } - findPreference(PREF_SWIPE_HISTORY)?.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.prefSwipeHistory.name)?.onPreferenceClickListener = Preference.OnPreferenceClickListener { SwipeActionsDialog(requireContext(), HistoryFragment.TAG).show (object : SwipeActionsDialog.Callback { override fun onCall() {} }) @@ -51,12 +51,12 @@ class SwipePreferencesFragment : PreferenceFragmentCompat() { (activity as PreferenceActivity).supportActionBar?.setTitle(R.string.swipeactions_label) } - companion object { - private const val PREF_SWIPE_QUEUE = "prefSwipeQueue" -// private const val PREF_SWIPE_STATISTICS = "prefSwipeStatistics" - private const val PREF_SWIPE_EPISODES = "prefSwipeEpisodes" - private const val PREF_SWIPE_DOWNLOADS = "prefSwipeDownloads" - private const val PREF_SWIPE_FEED = "prefSwipeFeed" - private const val PREF_SWIPE_HISTORY = "prefSwipeHistory" + private enum class Prefs { + prefSwipeQueue, +// prefSwipeStatistics, + prefSwipeEpisodes, + prefSwipeDownloads, + prefSwipeFeed, + prefSwipeHistory } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/SynchronizationPreferencesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/SynchronizationPreferencesFragment.kt index b372e2e4..e6edf654 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/SynchronizationPreferencesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/SynchronizationPreferencesFragment.kt @@ -106,7 +106,7 @@ class SynchronizationPreferencesFragment : PreferenceFragmentCompat() { private fun setupScreen() { val activity: Activity? = activity - findPreference(PREFERENCE_GPODNET_SETLOGIN_INFORMATION)?.setOnPreferenceClickListener { + findPreference(Prefs.pref_gpodnet_setlogin_information.name)?.setOnPreferenceClickListener { val dialog: AuthenticationDialog = object : AuthenticationDialog(requireContext(), R.string.pref_gpodnet_setlogin_information_title, false, SynchronizationCredentials.username, null) { override fun onConfirmed(username: String, password: String) { @@ -116,15 +116,15 @@ class SynchronizationPreferencesFragment : PreferenceFragmentCompat() { dialog.show() true } - findPreference(PREFERENCE_SYNC)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.pref_synchronization_sync.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { SyncService.syncImmediately(requireActivity().applicationContext) true } - findPreference(PREFERENCE_FORCE_FULL_SYNC)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.pref_synchronization_force_full_sync.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { SyncService.fullSync(requireContext()) true } - findPreference(PREFERENCE_LOGOUT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.pref_synchronization_logout.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { SynchronizationCredentials.clear(requireContext()) Snackbar.make(requireView(), R.string.pref_synchronization_logout_toast, Snackbar.LENGTH_LONG).show() SynchronizationSettings.setSelectedSyncProvider(null) @@ -134,14 +134,14 @@ class SynchronizationPreferencesFragment : PreferenceFragmentCompat() { } private fun updateScreen() { - val preferenceInstantSync = findPreference(PREFERENCE_INSTANT_SYNC) + val preferenceInstantSync = findPreference(Prefs.preference_instant_sync.name) preferenceInstantSync!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { WifiAuthenticationFragment().show(childFragmentManager, WifiAuthenticationFragment.TAG) true } val loggedIn = isProviderConnected - val preferenceHeader = findPreference(PREFERENCE_SYNCHRONIZATION_DESCRIPTION) + val preferenceHeader = findPreference(Prefs.preference_synchronization_description.name) if (loggedIn) { val selectedProvider = SynchronizationProviderViewData.fromIdentifier(selectedSyncProviderKey) preferenceHeader!!.title = "" @@ -160,20 +160,20 @@ class SynchronizationPreferencesFragment : PreferenceFragmentCompat() { } } - val gpodnetSetLoginPreference = findPreference(PREFERENCE_GPODNET_SETLOGIN_INFORMATION) + val gpodnetSetLoginPreference = findPreference(Prefs.pref_gpodnet_setlogin_information.name) gpodnetSetLoginPreference!!.isVisible = isProviderSelected(SynchronizationProviderViewData.GPODDER_NET) gpodnetSetLoginPreference.isEnabled = loggedIn - findPreference(PREFERENCE_SYNC)!!.isVisible = loggedIn - findPreference(PREFERENCE_FORCE_FULL_SYNC)!!.isVisible = loggedIn - findPreference(PREFERENCE_LOGOUT)!!.isVisible = loggedIn + findPreference(Prefs.pref_synchronization_sync.name)!!.isVisible = loggedIn + findPreference(Prefs.pref_synchronization_force_full_sync.name)!!.isVisible = loggedIn + findPreference(Prefs.pref_synchronization_logout.name)!!.isVisible = loggedIn if (loggedIn) { val summary = getString(R.string.synchronization_login_status, SynchronizationCredentials.username, SynchronizationCredentials.hosturl) val formattedSummary = HtmlCompat.fromHtml(summary, HtmlCompat.FROM_HTML_MODE_LEGACY) - findPreference(PREFERENCE_LOGOUT)!!.summary = formattedSummary + findPreference(Prefs.pref_synchronization_logout.name)!!.summary = formattedSummary updateLastSyncReport(SynchronizationSettings.isLastSyncSuccessful, SynchronizationSettings.lastSyncAttempt) } else { - findPreference(PREFERENCE_LOGOUT)?.summary = "" + findPreference(Prefs.pref_synchronization_logout.name)?.summary = "" (activity as PreferenceActivity).supportActionBar?.setSubtitle("") } } @@ -674,12 +674,12 @@ class SynchronizationPreferencesFragment : PreferenceFragmentCompat() { } } - companion object { - private const val PREFERENCE_INSTANT_SYNC = "preference_instant_sync" - private const val PREFERENCE_SYNCHRONIZATION_DESCRIPTION = "preference_synchronization_description" - private const val PREFERENCE_GPODNET_SETLOGIN_INFORMATION = "pref_gpodnet_setlogin_information" - private const val PREFERENCE_SYNC = "pref_synchronization_sync" - private const val PREFERENCE_FORCE_FULL_SYNC = "pref_synchronization_force_full_sync" - private const val PREFERENCE_LOGOUT = "pref_synchronization_logout" + private enum class Prefs { + preference_instant_sync, + preference_synchronization_description, + pref_gpodnet_setlogin_information, + pref_synchronization_sync, + pref_synchronization_force_full_sync, + pref_synchronization_logout, } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt index a66c2e37..3819479e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt @@ -6,7 +6,7 @@ import ac.mdiq.podcini.preferences.UserPreferences.fullNotificationButtons import ac.mdiq.podcini.preferences.UserPreferences.setShowRemainTimeSetting import ac.mdiq.podcini.ui.activity.PreferenceActivity import ac.mdiq.podcini.ui.dialog.DrawerPreferencesDialog -import ac.mdiq.podcini.ui.dialog.FeedSortDialogNew +import ac.mdiq.podcini.ui.dialog.FeedSortDialog import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.FlowEvent import android.content.Context @@ -37,12 +37,12 @@ class UserInterfacePreferencesFragment : PreferenceFragmentCompat() { ActivityCompat.recreate(requireActivity()) true } - findPreference(UserPreferences.PREF_THEME)!!.onPreferenceChangeListener = restartApp - findPreference(UserPreferences.PREF_THEME_BLACK)!!.onPreferenceChangeListener = restartApp - findPreference(UserPreferences.PREF_TINTED_COLORS)!!.onPreferenceChangeListener = restartApp - if (Build.VERSION.SDK_INT < 31) findPreference(UserPreferences.PREF_TINTED_COLORS)!!.isVisible = false + findPreference(UserPreferences.Prefs.prefTheme.name)!!.onPreferenceChangeListener = restartApp + findPreference(UserPreferences.Prefs.prefThemeBlack.name)!!.onPreferenceChangeListener = restartApp + findPreference(UserPreferences.Prefs.prefTintedColors.name)!!.onPreferenceChangeListener = restartApp + if (Build.VERSION.SDK_INT < 31) findPreference(UserPreferences.Prefs.prefTintedColors.name)!!.isVisible = false - findPreference(UserPreferences.PREF_SHOW_TIME_LEFT)?.setOnPreferenceChangeListener { _: Preference?, newValue: Any? -> + findPreference(UserPreferences.Prefs.showTimeLeft.name)?.setOnPreferenceChangeListener { _: Preference?, newValue: Any? -> setShowRemainTimeSetting(newValue as Boolean?) // TODO: need another event type? // EventFlow.postEvent(FlowEvent.EpisodePlayedEvent()) @@ -50,12 +50,12 @@ class UserInterfacePreferencesFragment : PreferenceFragmentCompat() { true } - findPreference(UserPreferences.PREF_HIDDEN_DRAWER_ITEMS)?.setOnPreferenceClickListener { + findPreference(UserPreferences.Prefs.prefHiddenDrawerItems.name)?.setOnPreferenceClickListener { DrawerPreferencesDialog.show(requireContext(), null) true } - findPreference(UserPreferences.PREF_FULL_NOTIFICATION_BUTTONS)?.setOnPreferenceClickListener { + findPreference(UserPreferences.Prefs.prefFullNotificationButtons.name)?.setOnPreferenceClickListener { showFullNotificationButtonsDialog() true } @@ -65,16 +65,16 @@ class UserInterfacePreferencesFragment : PreferenceFragmentCompat() { // true // }) - findPreference(UserPreferences.PREF_DRAWER_FEED_ORDER)?.onPreferenceClickListener = (Preference.OnPreferenceClickListener { + findPreference(UserPreferences.Prefs.prefDrawerFeedOrder.name)?.onPreferenceClickListener = (Preference.OnPreferenceClickListener { // FeedSortDialog.showDialog(requireContext()) - FeedSortDialogNew().show(childFragmentManager, "FeedSortDialog") + FeedSortDialog().show(childFragmentManager, "FeedSortDialog") true }) findPreference(PREF_SWIPE)?.setOnPreferenceClickListener { (activity as PreferenceActivity).openScreen(R.xml.preferences_swipe) true } - if (Build.VERSION.SDK_INT >= 26) findPreference(UserPreferences.PREF_EXPANDED_NOTIFICATION)!!.isVisible = false + if (Build.VERSION.SDK_INT >= 26) findPreference(UserPreferences.Prefs.prefExpandNotify.name)!!.isVisible = false } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/receiver/PlayerWidget.kt b/app/src/main/kotlin/ac/mdiq/podcini/receiver/PlayerWidget.kt index f2b3c9f4..bd51a13a 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/receiver/PlayerWidget.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/receiver/PlayerWidget.kt @@ -27,9 +27,9 @@ class PlayerWidget : AppWidgetProvider() { getSharedPrefs(context) WidgetUpdaterWorker.enqueueWork(context) - if (!prefs!!.getBoolean(KEY_WORKAROUND_ENABLED, false)) { + if (!prefs!!.getBoolean(Prefs.WorkaroundEnabled.name, false)) { scheduleWorkaround(context) - prefs!!.edit().putBoolean(KEY_WORKAROUND_ENABLED, true).apply() + prefs!!.edit().putBoolean(Prefs.WorkaroundEnabled.name, true).apply() } } @@ -42,37 +42,41 @@ class PlayerWidget : AppWidgetProvider() { override fun onDeleted(context: Context, appWidgetIds: IntArray) { Logd(TAG, "OnDeleted") for (appWidgetId in appWidgetIds) { - prefs!!.edit().remove(KEY_WIDGET_COLOR + appWidgetId).apply() - prefs!!.edit().remove(KEY_WIDGET_PLAYBACK_SPEED + appWidgetId).apply() - prefs!!.edit().remove(KEY_WIDGET_REWIND + appWidgetId).apply() - prefs!!.edit().remove(KEY_WIDGET_FAST_FORWARD + appWidgetId).apply() - prefs!!.edit().remove(KEY_WIDGET_SKIP + appWidgetId).apply() + prefs!!.edit().remove(Prefs.widget_color.name + appWidgetId).apply() + prefs!!.edit().remove(Prefs.widget_playback_speed.name + appWidgetId).apply() + prefs!!.edit().remove(Prefs.widget_rewind.name + appWidgetId).apply() + prefs!!.edit().remove(Prefs.widget_fast_forward.name + appWidgetId).apply() + prefs!!.edit().remove(Prefs.widget_skip.name + appWidgetId).apply() } val manager = AppWidgetManager.getInstance(context) val widgetIds = manager.getAppWidgetIds(ComponentName(context, PlayerWidget::class.java)) if (widgetIds.isEmpty()) { - prefs!!.edit().putBoolean(KEY_WORKAROUND_ENABLED, false).apply() - WorkManager.getInstance(context).cancelUniqueWork(WORKAROUND_WORK_NAME) + prefs!!.edit().putBoolean(Prefs.WorkaroundEnabled.name, false).apply() + WorkManager.getInstance(context).cancelUniqueWork(Prefs.WidgetUpdaterWorkaround.name) } super.onDeleted(context, appWidgetIds) } private fun setEnabled(enabled: Boolean) { - prefs!!.edit().putBoolean(KEY_ENABLED, enabled).apply() + prefs!!.edit().putBoolean(Prefs.WidgetEnabled.name, enabled).apply() + } + + enum class Prefs { + widget_color, + widget_playback_speed, + widget_skip, + widget_fast_forward, + widget_rewind, + WidgetUpdaterWorkaround, + WorkaroundEnabled, + WidgetEnabled } companion object { private val TAG: String = PlayerWidget::class.simpleName ?: "Anonymous" - const val PREFS_NAME: String = "PlayerWidgetPrefs" - private const val KEY_WORKAROUND_ENABLED = "WorkaroundEnabled" - private const val KEY_ENABLED = "WidgetEnabled" - const val KEY_WIDGET_COLOR: String = "widget_color" - const val KEY_WIDGET_PLAYBACK_SPEED: String = "widget_playback_speed" - const val KEY_WIDGET_SKIP: String = "widget_skip" - const val KEY_WIDGET_FAST_FORWARD: String = "widget_fast_forward" - const val KEY_WIDGET_REWIND: String = "widget_rewind" + private const val PREFS_NAME: String = "PlayerWidgetPrefs" + const val DEFAULT_COLOR: Int = -0xd9d3cf - private const val WORKAROUND_WORK_NAME = "WidgetUpdaterWorkaround" var prefs: SharedPreferences? = null @@ -87,12 +91,12 @@ class PlayerWidget : AppWidgetProvider() { val workRequest: OneTimeWorkRequest = OneTimeWorkRequest.Builder(WidgetUpdaterWorker::class.java) .setInitialDelay((100 * 356).toLong(), TimeUnit.DAYS) .build() - WorkManager.getInstance(context).enqueueUniqueWork(WORKAROUND_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest) + WorkManager.getInstance(context).enqueueUniqueWork(Prefs.WidgetUpdaterWorkaround.name, ExistingWorkPolicy.REPLACE, workRequest) } @JvmStatic fun isEnabled(): Boolean { - return prefs!!.getBoolean(KEY_ENABLED, false) + return prefs!!.getBoolean(Prefs.WidgetEnabled.name, false) } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoCleanups.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoCleanups.kt index e56fea54..0e84e6ab 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoCleanups.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoCleanups.kt @@ -2,7 +2,6 @@ package ac.mdiq.podcini.storage.algorithms import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.EPISODE_CLEANUP_NULL -import ac.mdiq.podcini.preferences.UserPreferences.PREF_EPISODE_CLEANUP import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.preferences.UserPreferences.episodeCacheSize import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload @@ -25,9 +24,9 @@ import java.util.concurrent.ExecutionException object AutoCleanups { var episodeCleanupValue: Int - get() = appPrefs.getString(PREF_EPISODE_CLEANUP, "" + EPISODE_CLEANUP_NULL)!!.toInt() + get() = appPrefs.getString(UserPreferences.Prefs.prefEpisodeCleanup.name, "" + EPISODE_CLEANUP_NULL)!!.toInt() set(episodeCleanupValue) { - appPrefs.edit().putString(PREF_EPISODE_CLEANUP, episodeCleanupValue.toString()).apply() + appPrefs.edit().putString(UserPreferences.Prefs.prefEpisodeCleanup.name, episodeCleanupValue.toString()).apply() } /** @@ -48,7 +47,7 @@ object AutoCleanups { return when (val cleanupValue = episodeCleanupValue) { UserPreferences.EPISODE_CLEANUP_EXCEPT_FAVORITE -> ExceptFavoriteCleanupAlgorithm() UserPreferences.EPISODE_CLEANUP_QUEUE -> APQueueCleanupAlgorithm() - UserPreferences.EPISODE_CLEANUP_NULL -> APNullCleanupAlgorithm() + EPISODE_CLEANUP_NULL -> APNullCleanupAlgorithm() else -> APCleanupAlgorithm(cleanupValue) } } @@ -60,16 +59,12 @@ object AutoCleanups { private val candidates: List get() { val candidates: MutableList = ArrayList() - val downloadedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.DOWNLOADED), EpisodeSortOrder.DATE_NEW_OLD) + val downloadedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.downloaded.name), EpisodeSortOrder.DATE_NEW_OLD) for (item in downloadedItems) { if (item.media != null && item.media!!.downloaded && !item.isFavorite) candidates.add(item) } return candidates } - /** - * The maximum number of episodes that could be cleaned up. - * @return the number of episodes that *could* be cleaned up, if needed - */ override fun getReclaimableItems(): Int { return candidates.size } @@ -100,7 +95,7 @@ object AutoCleanups { public override fun getDefaultCleanupParameter(): Int { val cacheSize = episodeCacheSize if (cacheSize != UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED) { - val downloadedEpisodes = getEpisodesCount(EpisodeFilter(EpisodeFilter.DOWNLOADED)) + val downloadedEpisodes = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name)) if (downloadedEpisodes > cacheSize) return downloadedEpisodes - cacheSize } return 0 @@ -118,7 +113,7 @@ object AutoCleanups { private val candidates: List get() { val candidates: MutableList = ArrayList() - val downloadedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.DOWNLOADED), EpisodeSortOrder.DATE_NEW_OLD) + val downloadedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.downloaded.name), EpisodeSortOrder.DATE_NEW_OLD) val idsInQueues = getInQueueEpisodeIds() for (item in downloadedItems) { if (item.media != null && item.media!!.downloaded && !idsInQueues.contains(item.id) && !item.isFavorite) @@ -126,9 +121,6 @@ object AutoCleanups { } return candidates } - /** - * @return the number of episodes that *could* be cleaned up, if needed - */ override fun getReclaimableItems(): Int { return candidates.size } @@ -185,9 +177,6 @@ object AutoCleanups { } } - /** - * Implementation of the EpisodeCleanupAlgorithm interface used by Podcini. - */ /** the number of days after playback to wait before an item is eligible to be cleaned up. * Fractional for number of hours, e.g., 0.5 = 12 hours, 0.0416 = 1 hour. */ @@ -195,7 +184,7 @@ object AutoCleanups { private val candidates: List get() { val candidates: MutableList = ArrayList() - val downloadedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.DOWNLOADED), EpisodeSortOrder.DATE_NEW_OLD) + val downloadedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.downloaded.name), EpisodeSortOrder.DATE_NEW_OLD) val idsInQueues = getInQueueEpisodeIds() val mostRecentDateForDeletion = calcMostRecentDateForDeletion(Date()) for (item in downloadedItems) { @@ -207,9 +196,6 @@ object AutoCleanups { } return candidates } - /** - * @return the number of episodes that *could* be cleaned up, if needed - */ override fun getReclaimableItems(): Int { return candidates.size } @@ -258,7 +244,6 @@ object AutoCleanups { /** * Deletes downloaded episodes that are no longer needed. What episodes are deleted and how many * of them depends on the implementation. - * * @param context Can be used for accessing the database * @param numToRemove An additional parameter. This parameter is either returned by getDefaultCleanupParameter * or getPerformCleanupParameter. @@ -293,7 +278,7 @@ object AutoCleanups { */ fun getNumEpisodesToCleanup(amountOfRoomNeeded: Int): Int { if (amountOfRoomNeeded >= 0 && episodeCacheSize != UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED) { - val downloadedEpisodes = getEpisodesCount(EpisodeFilter(EpisodeFilter.DOWNLOADED)) + val downloadedEpisodes = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name)) if (downloadedEpisodes + amountOfRoomNeeded >= episodeCacheSize) return (downloadedEpisodes + amountOfRoomNeeded - episodeCacheSize) } return 0 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt index 52488f69..6343d975 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt @@ -10,15 +10,21 @@ import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownloadOnBattery import ac.mdiq.podcini.storage.database.Episodes.getEpisodes import ac.mdiq.podcini.storage.database.Episodes.getEpisodesCount -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.database.Episodes.setPlayState +import ac.mdiq.podcini.storage.database.Feeds.getFeedList +import ac.mdiq.podcini.storage.database.RealmDB.realm +import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope +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.util.Logd import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.BatteryManager import androidx.media3.common.util.UnstableApi +import io.realm.kotlin.Realm +import io.realm.kotlin.UpdatePolicy import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.Future @@ -35,7 +41,7 @@ object AutoDownloads { t } - var downloadAlgorithm = AutoDownloadAlgorithm() + var downloadAlgorithm = FeedBasedAutoDLAlgorithm() /** * Looks for non-downloaded episodes in the queue or list of unread episodes and request a download if @@ -47,9 +53,9 @@ object AutoDownloads { * @return A Future that can be used for waiting for the methods completion. */ @UnstableApi - fun autodownloadEpisodeMedia(context: Context): Future<*> { + fun autodownloadEpisodeMedia(context: Context, feeds: List? = null): Future<*> { Logd(TAG, "autodownloadEpisodeMedia") - return autodownloadExec.submit(downloadAlgorithm.autoDownloadEpisodeMedia(context)) + return autodownloadExec.submit(downloadAlgorithm.autoDownloadEpisodeMedia(context, feeds)) } /** @@ -66,8 +72,9 @@ object AutoDownloads { * @param context Used for accessing the DB. * @return A Runnable that will be submitted to an ExecutorService. */ +// likely not needed @UnstableApi - open fun autoDownloadEpisodeMedia(context: Context): Runnable? { + open fun autoDownloadEpisodeMedia(context: Context, feeds: List? = null): Runnable? { return Runnable { // true if we should auto download based on network status // val networkShouldAutoDl = (isAutoDownloadAllowed) @@ -78,15 +85,14 @@ object AutoDownloads { // we should only auto download if both network AND power are happy if (networkShouldAutoDl && powerShouldAutoDl) { Logd(TAG, "Performing auto-dl of undownloaded episodes") - val candidates: MutableList - val queue = curQueue.episodes - val newItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.NEW), EpisodeSortOrder.DATE_NEW_OLD) + val queueItems = curQueue.episodes + val newItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.new.name), EpisodeSortOrder.DATE_NEW_OLD) Logd(TAG, "newItems: ${newItems.size}") - candidates = ArrayList(queue.size + newItems.size) - candidates.addAll(queue) + val candidates: MutableList = ArrayList(queueItems.size + newItems.size) + candidates.addAll(queueItems) for (newItem in newItems) { val feedPrefs = newItem.feed!!.preferences - if (feedPrefs!!.autoDownload && !candidates.contains(newItem) && feedPrefs.autoDownloadFilter.shouldAutoDownload(newItem)) candidates.add(newItem) + if (feedPrefs!!.autoDownload && !candidates.contains(newItem) && feedPrefs.autoDownloadFilter!!.shouldAutoDownload(newItem)) candidates.add(newItem) } // filter items that are not auto downloadable val it = candidates.iterator() @@ -96,7 +102,7 @@ object AutoDownloads { it.remove() } val autoDownloadableEpisodes = candidates.size - val downloadedEpisodes = getEpisodesCount(EpisodeFilter(EpisodeFilter.DOWNLOADED)) + val downloadedEpisodes = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name)) val deletedEpisodes = AutoCleanups.build().makeRoomForEpisodes(context, autoDownloadableEpisodes) val cacheIsUnlimited = episodeCacheSize == UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED val episodeCacheSize = episodeCacheSize @@ -112,10 +118,11 @@ object AutoDownloads { else Logd(TAG, "not auto downloaded networkShouldAutoDl: $networkShouldAutoDl powerShouldAutoDl $powerShouldAutoDl") } } + /** * @return true if the device is charging */ - private fun deviceCharging(context: Context): Boolean { + protected fun deviceCharging(context: Context): Boolean { // from http://developer.android.com/training/monitoring-device-state/battery-monitoring.html val iFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) val batteryStatus = context.registerReceiver(null, iFilter) @@ -127,4 +134,99 @@ object AutoDownloads { private val TAG: String = AutoDownloadAlgorithm::class.simpleName ?: "Anonymous" } } + + class FeedBasedAutoDLAlgorithm : AutoDownloadAlgorithm() { + + @UnstableApi + override fun autoDownloadEpisodeMedia(context: Context, feeds: List?): Runnable { + return Runnable { + // true if we should auto download based on network status + val networkShouldAutoDl = (isAutoDownloadAllowed && isEnableAutodownload) + // true if we should auto download based on power status + val powerShouldAutoDl = (deviceCharging(context) || isEnableAutodownloadOnBattery) + Logd(Companion.TAG, "autoDownloadEpisodeMedia prepare $networkShouldAutoDl $powerShouldAutoDl") + // we should only auto download if both network AND power are happy + if (networkShouldAutoDl && powerShouldAutoDl) { + Logd(Companion.TAG, "autoDownloadEpisodeMedia Performing auto-dl of undownloaded episodes") + val candidates: MutableSet = mutableSetOf() + val queueItems = curQueue.episodes.filter { it.media?.downloaded != true } + if (queueItems.isNotEmpty()) candidates.addAll(queueItems) + val feeds = feeds ?: getFeedList() + feeds.forEach { f -> + if (f.preferences?.autoDownload == true && !f.isLocalFeed) { + var episodes = mutableListOf() + val downloadedCount = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name), f.id) + val toDLCount = (f.preferences?.autoDLMaxEpisodes?:0) - downloadedCount + if (toDLCount > 0) { + var queryString = "feedId == ${f.id} AND isAutoDownloadEnabled == true AND media != nil AND media.downloaded == false" + when (f.preferences?.autoDLPolicy) { + FeedPreferences.AutoDLPolicy.ONLY_NEW -> { + queryString += " AND playState == -1 SORT(pubDate DESC) LIMIT(${3*toDLCount})" + episodes = realm.query(Episode::class).query(queryString).find().toMutableList() + } + FeedPreferences.AutoDLPolicy.NEWER -> { + queryString += " AND playState != 1 SORT(pubDate DESC) LIMIT(${3*toDLCount})" + episodes = realm.query(Episode::class).query(queryString).find().toMutableList() + } + FeedPreferences.AutoDLPolicy.OLDER -> { + queryString += " AND playState != 1 SORT(pubDate ASC) LIMIT(${3*toDLCount})" + episodes = realm.query(Episode::class).query(queryString).find().toMutableList() + } + else -> {} + } + if (episodes.isNotEmpty()) { + var count = 0 + for (e in episodes) { + if (f.preferences?.autoDownloadFilter?.shouldAutoDownload(e) == true) { + candidates.add(e) + if (++count >= toDLCount) break + } else upsertBlk(e) { it.setPlayed(true)} + } + } + } + episodes.clear() + Logd(TAG, "autoDownloadEpisodeMedia ${f.title} candidate size: ${candidates.size}") + } + runOnIOScope { + realm.write { + while (true) { + val episodesNew = query(Episode::class, "feedId == ${f.id} AND playState == -1 LIMIT(20)").find() + if (episodesNew.isEmpty()) break + Logd(TAG, "autoDownloadEpisodeMedia episodesNew: ${episodesNew.size}") + episodesNew.map { e -> + e.setPlayed(false) + Logd(TAG, "autoDownloadEpisodeMedia reset NEW ${e.title} ${e.playState}") + copyToRealm(e, UpdatePolicy.ALL) + } + } + } +// TODO: need to send an event + } + } + if (candidates.isNotEmpty()) { + val autoDownloadableCount = candidates.size + val downloadedCount = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name)) + val deletedCount = AutoCleanups.build().makeRoomForEpisodes(context, autoDownloadableCount) + val cacheIsUnlimited = episodeCacheSize == UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED + val allowedCount = + if (cacheIsUnlimited || episodeCacheSize >= downloadedCount + autoDownloadableCount) autoDownloadableCount + else episodeCacheSize - (downloadedCount - deletedCount) + if (allowedCount in 0..candidates.size) { + val itemsToDownload: MutableList = candidates.toMutableList().subList(0, allowedCount) + if (itemsToDownload.isNotEmpty()) { + Logd(TAG, "Enqueueing " + itemsToDownload.size + " items for download") + for (episode in itemsToDownload) DownloadServiceInterface.get()?.download(context, episode) + } + itemsToDownload.clear() + } + candidates.clear() + } + } + else Logd(TAG, "not auto downloaded networkShouldAutoDl: $networkShouldAutoDl powerShouldAutoDl $powerShouldAutoDl") + } + } + companion object { + private val TAG: String = FeedBasedAutoDLAlgorithm::class.simpleName ?: "Anonymous" + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt index c02e848c..881efb40 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt @@ -9,9 +9,9 @@ import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.InTheatre.curState import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying import ac.mdiq.podcini.playback.service.PlaybackService.Companion.ACTION_SHUTDOWN_PLAYBACK_SERVICE -import ac.mdiq.podcini.preferences.UserPreferences.PREF_REMOVDE_FROM_QUEUE_MARKED_PLAYED +import ac.mdiq.podcini.preferences.UserPreferences +import ac.mdiq.podcini.preferences.UserPreferences.Prefs import ac.mdiq.podcini.preferences.UserPreferences.appPrefs -import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueuesSync import ac.mdiq.podcini.storage.database.Queues.removeFromQueueSync import ac.mdiq.podcini.storage.database.RealmDB.realm @@ -26,7 +26,7 @@ import ac.mdiq.podcini.util.IntentUtils.sendLocalBroadcast import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.FlowEvent -import ac.mdiq.podcini.util.sorting.EpisodesPermutors.getPermutor +import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor import android.app.backup.BackupManager import android.content.Context import android.net.Uri @@ -58,9 +58,10 @@ object Episodes { return if (copy) realm.copyFromRealm(episodes) else episodes } - fun getEpisodesCount(filter: EpisodeFilter?): Int { + fun getEpisodesCount(filter: EpisodeFilter?, feedId: Long = -1): Int { Logd(TAG, "getEpisodesCount called") - val queryString = filter?.queryString()?:"id > 0" + var queryString = filter?.queryString()?:"id > 0" + if (feedId >= 0) queryString += " AND feedId == $feedId " return realm.query(Episode::class).query(queryString).count().find().toInt() } @@ -102,6 +103,10 @@ object Episodes { } } + fun shouldDeleteRemoveFromQueue(): Boolean { + return appPrefs.getBoolean(Prefs.prefDeleteRemovesFromQueue.name, false) + } + @OptIn(UnstableApi::class) fun deleteMediaSync(context: Context, episode: Episode): Episode { Logd(TAG, "deleteMediaSync called") @@ -287,6 +292,6 @@ object Episodes { } private fun shouldRemoveFromQueuesMarkPlayed(): Boolean { - return appPrefs.getBoolean(PREF_REMOVDE_FROM_QUEUE_MARKED_PLAYED, true) + return appPrefs.getBoolean(UserPreferences.Prefs.prefRemoveFromQueueMarkedPlayed.name, true) } } \ No newline at end of file diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt index d7625471..952e62a3 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt @@ -4,6 +4,8 @@ import ac.mdiq.podcini.BuildConfig import ac.mdiq.podcini.net.download.DownloadError import ac.mdiq.podcini.net.sync.model.EpisodeAction import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink +import ac.mdiq.podcini.preferences.UserPreferences.isAutoDelete +import ac.mdiq.podcini.preferences.UserPreferences.isAutoDeleteLocal import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodes import ac.mdiq.podcini.storage.database.LogsAndStats.addDownloadStatus import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueuesQuiet @@ -42,6 +44,10 @@ object Feeds { return feedMap.values.toList() } + fun getFeedCount(): Int { + return realm.query(Feed::class).count().find().toInt() + } + fun getTags(): List { return tags } @@ -374,7 +380,7 @@ object Feeds { feed.preferences = FeedPreferences(feed.id, false, AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, "", "") else feed.preferences!!.feedID = feed.id - Logd(TAG, "feed.episodes: ${feed.episodes.size}") + Logd(TAG, "feed.episodes count: ${feed.episodes.size}") for (episode in feed.episodes) { episode.id = idLong++ episode.feedId = feed.id @@ -450,6 +456,12 @@ object Feeds { } } + @JvmStatic + fun shouldAutoDeleteItem(feed: Feed): Boolean { + if (!isAutoDelete) return false + return !feed.isLocalFeed || isAutoDeleteLocal + } + /** * Compares the pubDate of two FeedItems for sorting in reverse order */ diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/LogsAndStats.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/LogsAndStats.kt index d482c3df..88b4e447 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/LogsAndStats.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/LogsAndStats.kt @@ -8,7 +8,7 @@ import ac.mdiq.podcini.storage.model.* import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.FlowEvent -import ac.mdiq.podcini.util.sorting.DownloadResultComparator +import ac.mdiq.podcini.storage.utils.DownloadResultComparator import kotlinx.coroutines.Job object LogsAndStats { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt index 559de613..611bad9c 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt @@ -3,22 +3,18 @@ package ac.mdiq.podcini.storage.database import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.playback.base.InTheatre.curMedia import ac.mdiq.podcini.playback.base.InTheatre.curQueue -import ac.mdiq.podcini.preferences.UserPreferences.PREF_ENQUEUE_LOCATION -import ac.mdiq.podcini.preferences.UserPreferences.PREF_QUEUE_KEEP_SORTED -import ac.mdiq.podcini.preferences.UserPreferences.PREF_QUEUE_KEEP_SORTED_ORDER -import ac.mdiq.podcini.preferences.UserPreferences.PREF_QUEUE_LOCKED +import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.appPrefs -import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia 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.EpisodesPermutors.getPermutor import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.FlowEvent -import ac.mdiq.podcini.util.sorting.EpisodesPermutors.getPermutor import android.content.Context import android.util.Log import androidx.annotation.OptIn @@ -206,7 +202,7 @@ object Queues { } else Logd(TAG, "Queue was not modified by call to removeQueueItem") // TODO: what's this for? - if (queue.id == curQueue.id && context != null) autodownloadEpisodeMedia(context) +// if (queue.id == curQueue.id && context != null) autodownloadEpisodeMedia(context) } suspend fun removeFromAllQueuesQuiet(episodeIds: List) { @@ -275,9 +271,9 @@ object Queues { } var isQueueLocked: Boolean - get() = appPrefs.getBoolean(PREF_QUEUE_LOCKED, false) + get() = appPrefs.getBoolean(UserPreferences.Prefs.prefQueueLocked.name, false) set(locked) { - appPrefs.edit().putBoolean(PREF_QUEUE_LOCKED, locked).apply() + appPrefs.edit().putBoolean(UserPreferences.Prefs.prefQueueLocked.name, locked).apply() } var isQueueKeepSorted: Boolean @@ -285,13 +281,13 @@ object Queues { * Returns if the queue is in keep sorted mode. * @see .queueKeepSortedOrder */ - get() = appPrefs.getBoolean(PREF_QUEUE_KEEP_SORTED, false) + get() = appPrefs.getBoolean(UserPreferences.Prefs.prefQueueKeepSorted.name, false) /** * Enables/disables the keep sorted mode of the queue. * @see .queueKeepSortedOrder */ set(keepSorted) { - appPrefs.edit().putBoolean(PREF_QUEUE_KEEP_SORTED, keepSorted).apply() + appPrefs.edit().putBoolean(UserPreferences.Prefs.prefQueueKeepSorted.name, keepSorted).apply() } var queueKeepSortedOrder: EpisodeSortOrder? @@ -301,7 +297,7 @@ object Queues { * @see .isQueueKeepSorted */ get() { - val sortOrderStr = appPrefs.getString(PREF_QUEUE_KEEP_SORTED_ORDER, "use-default") + val sortOrderStr = appPrefs.getString(UserPreferences.Prefs.prefQueueKeepSortedOrder.name, "use-default") return EpisodeSortOrder.parseWithDefault(sortOrderStr, EpisodeSortOrder.DATE_NEW_OLD) } /** @@ -310,12 +306,12 @@ object Queues { */ set(sortOrder) { if (sortOrder == null) return - appPrefs.edit().putString(PREF_QUEUE_KEEP_SORTED_ORDER, sortOrder.name).apply() + appPrefs.edit().putString(UserPreferences.Prefs.prefQueueKeepSortedOrder.name, sortOrder.name).apply() } var enqueueLocation: EnqueueLocation get() { - val valStr = appPrefs.getString(PREF_ENQUEUE_LOCATION, EnqueueLocation.BACK.name) + val valStr = appPrefs.getString(UserPreferences.Prefs.prefEnqueueLocation.name, EnqueueLocation.BACK.name) try { return EnqueueLocation.valueOf(valStr!!) } catch (t: Throwable) { @@ -325,7 +321,7 @@ object Queues { } } set(location) { - appPrefs.edit().putString(PREF_ENQUEUE_LOCATION, location.name).apply() + appPrefs.edit().putString(UserPreferences.Prefs.prefEnqueueLocation.name, location.name).apply() } class EnqueuePositionCalculator(private val enqueueLocation: EnqueueLocation) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt index 3e661a2b..91843efa 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt @@ -18,7 +18,7 @@ import kotlin.coroutines.ContinuationInterceptor object RealmDB { private val TAG: String = RealmDB::class.simpleName ?: "Anonymous" - private const val SCHEMA_VERSION_NUMBER = 7L + private const val SCHEMA_VERSION_NUMBER = 10L private val ioScope = CoroutineScope(Dispatchers.IO) @@ -29,6 +29,7 @@ object RealmDB { schema = setOf( Feed::class, FeedPreferences::class, + FeedMeasures::class, Episode::class, EpisodeMedia::class, CurrentState::class, diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeFilter.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeFilter.kt index 4305ba5b..32317bec 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeFilter.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeFilter.kt @@ -8,31 +8,35 @@ class EpisodeFilter(vararg properties: String) : Serializable { private val properties: Array = arrayOf(*properties.filter { it.isNotEmpty() }.map {it.trim()}.toTypedArray()) @JvmField - val showPlayed: Boolean = hasProperty(PLAYED) + val showPlayed: Boolean = hasProperty(States.played.name) @JvmField - val showUnplayed: Boolean = hasProperty(UNPLAYED) + val showUnplayed: Boolean = hasProperty(States.unplayed.name) @JvmField - val showPaused: Boolean = hasProperty(PAUSED) + val showPaused: Boolean = hasProperty(States.paused.name) @JvmField - val showNotPaused: Boolean = hasProperty(NOT_PAUSED) + val showNotPaused: Boolean = hasProperty(States.not_paused.name) @JvmField - val showNew: Boolean = hasProperty(NEW) + val showNew: Boolean = hasProperty(States.new.name) @JvmField - val showQueued: Boolean = hasProperty(QUEUED) + val showQueued: Boolean = hasProperty(States.queued.name) @JvmField - val showNotQueued: Boolean = hasProperty(NOT_QUEUED) + val showNotQueued: Boolean = hasProperty(States.not_queued.name) @JvmField - val showDownloaded: Boolean = hasProperty(DOWNLOADED) + val showDownloaded: Boolean = hasProperty(States.downloaded.name) @JvmField - val showNotDownloaded: Boolean = hasProperty(NOT_DOWNLOADED) + val showNotDownloaded: Boolean = hasProperty(States.not_downloaded.name) @JvmField - val showHasMedia: Boolean = hasProperty(HAS_MEDIA) + val showAutoDownloadable: Boolean = hasProperty(States.auto_downloadable.name) @JvmField - val showNoMedia: Boolean = hasProperty(NO_MEDIA) + val showNotAutoDownloadable: Boolean = hasProperty(States.not_auto_downloadable.name) @JvmField - val showIsFavorite: Boolean = hasProperty(IS_FAVORITE) + val showHasMedia: Boolean = hasProperty(States.has_media.name) @JvmField - val showNotFavorite: Boolean = hasProperty(NOT_FAVORITE) + val showNoMedia: Boolean = hasProperty(States.no_media.name) + @JvmField + val showIsFavorite: Boolean = hasProperty(States.is_favorite.name) + @JvmField + val showNotFavorite: Boolean = hasProperty(States.not_favorite.name) constructor(properties: String) : this(*(properties.split(",").toTypedArray())) @@ -55,6 +59,8 @@ class EpisodeFilter(vararg properties: String) : Serializable { showNotPaused && item.isInProgress -> return false showDownloaded && !item.isDownloaded -> return false showNotDownloaded && item.isDownloaded -> return false + showAutoDownloadable && !item.isAutoDownloadEnabled -> return false + showNotAutoDownloadable && item.isAutoDownloadEnabled -> return false showHasMedia && item.media == null -> return false showNoMedia && item.media != null -> return false showIsFavorite && !item.isFavorite -> return false @@ -84,6 +90,10 @@ class EpisodeFilter(vararg properties: String) : Serializable { showDownloaded -> statements.add("media.downloaded == true ") showNotDownloaded -> statements.add("media.downloaded == false ") } + when { + showAutoDownloadable -> statements.add("isAutoDownloadEnabled == true ") + showNotAutoDownloadable -> statements.add("isAutoDownloadEnabled == false ") + } when { showHasMedia -> statements.add("media != nil ") showNoMedia -> statements.add("media == nil ") @@ -104,21 +114,25 @@ class EpisodeFilter(vararg properties: String) : Serializable { return query.toString() } - + + enum class States { + played, + unplayed, + new, + paused, + not_paused, + is_favorite, + not_favorite, + has_media, + no_media, + queued, + not_queued, + downloaded, + not_downloaded, + auto_downloadable, + not_auto_downloadable + } companion object { - const val PLAYED: String = "played" - const val UNPLAYED: String = "unplayed" - const val NEW: String = "new" - const val PAUSED: String = "paused" - const val NOT_PAUSED: String = "not_paused" - const val IS_FAVORITE: String = "is_favorite" - const val NOT_FAVORITE: String = "not_favorite" - const val HAS_MEDIA: String = "has_media" - const val NO_MEDIA: String = "no_media" - const val QUEUED: String = "queued" - const val NOT_QUEUED: String = "not_queued" - const val DOWNLOADED: String = "downloaded" - const val NOT_DOWNLOADED: String = "not_downloaded" @JvmStatic fun unfiltered(): EpisodeFilter { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt index 83d9e259..5697d95c 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt @@ -33,6 +33,8 @@ class EpisodeMedia: EmbeddedRealmObject, Playable { // if (value && episode?.isNew == true) episode!!.setPlayed(false) // } + var downloadTime: Long = 0 + @get:JvmName("getDurationProperty") @set:JvmName("setDurationProperty") var duration = 0 @@ -93,6 +95,7 @@ class EpisodeMedia: EmbeddedRealmObject, Playable { this.downloadUrl = download_url } + // mostly used in tests constructor(id: Long, item: Episode?, duration: Int, position: Int, size: Long, mime_type: String?, file_url: String?, download_url: String?, downloaded: Boolean, playbackCompletionDate: Date?, played_duration: Int, @@ -152,6 +155,7 @@ class EpisodeMedia: EmbeddedRealmObject, Playable { fun setIsDownloaded() { downloaded = true + downloadTime = Date().time if (episode?.isNew == true) episode!!.setPlayed(false) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeSortOrder.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeSortOrder.kt index 920af2c8..67cf262f 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeSortOrder.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeSortOrder.kt @@ -18,6 +18,8 @@ enum class EpisodeSortOrder(@JvmField val code: Int, @JvmField val scope: Scope) PLAYED_DATE_NEW_OLD(12, Scope.INTRA_FEED), COMPLETED_DATE_OLD_NEW(13, Scope.INTRA_FEED), COMPLETED_DATE_NEW_OLD(14, Scope.INTRA_FEED), + DOWNLOAD_DATE_OLD_NEW(15, Scope.INTRA_FEED), + DOWNLOAD_DATE_NEW_OLD(16, Scope.INTRA_FEED), FEED_TITLE_A_Z(101, Scope.INTER_FEED), FEED_TITLE_Z_A(102, Scope.INTER_FEED), diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt index 6219db3d..2cba04a8 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt @@ -92,6 +92,8 @@ class Feed : RealmObject { var preferences: FeedPreferences? = null + var measures: FeedMeasures? = null + /** * Returns the value that uniquely identifies this Feed. If the * feedIdentifier attribute is not null, it will be returned. Else it will @@ -130,6 +132,15 @@ class Feed : RealmObject { preferences?.sortOrderCode = value.code } + @Ignore + var sortOrderAux: EpisodeSortOrder? = null + get() = fromCode(preferences?.sortOrderAuxCode ?: 0) + set(value) { + if (value == null) return + field = value + preferences?.sortOrderAuxCode = value.code + } + @Ignore val mostRecentItem: Episode? get() { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedAutoDownloadFilter.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedAutoDownloadFilter.kt index 9dfd5fef..19a4315f 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedAutoDownloadFilter.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedAutoDownloadFilter.kt @@ -9,8 +9,12 @@ import java.util.regex.Pattern // (we don't have to recreate it) // 2. We don't know if we'll actually be asked to parse anything anyways. -class FeedAutoDownloadFilter(val includeFilterRaw: String? = "", val excludeFilterRaw: String? = "", val minimalDurationFilter: Int = -1) : Serializable { +class FeedAutoDownloadFilter(val includeFilterRaw: String? = "", val excludeFilterRaw: String? = "", val minimalDurationFilter: Int = -1, val markExcludedPlayed: Boolean = false) : Serializable { + var includeTerms: List? = null + get() = field ?: parseTerms(includeFilterRaw) + var excludeTerms: List? = null + get() = field ?: parseTerms(excludeFilterRaw) /** * Parses the text in to a list of single words or quoted strings. * Example: "One "Two Three"" returns ["One", "Two Three"] @@ -32,11 +36,11 @@ class FeedAutoDownloadFilter(val includeFilterRaw: String? = "", val excludeFilt * @return true if the item should be downloaded */ fun shouldAutoDownload(item: Episode): Boolean { - val includeTerms = parseTerms(includeFilterRaw) - val excludeTerms = parseTerms(excludeFilterRaw) +// if (includeTerms == null) includeTerms = parseTerms(includeFilterRaw) +// if (excludeTerms == null) excludeTerms = parseTerms(excludeFilterRaw) // nothing has been specified, so include everything - if (includeTerms.isEmpty() && excludeTerms.isEmpty() && minimalDurationFilter <= -1) return true + if (includeTerms.isNullOrEmpty() && excludeTerms.isNullOrEmpty() && minimalDurationFilter <= -1) return true // Check if the episode is long enough if minimal duration filter is on if (hasMinimalDurationFilter() && item.media != null) { @@ -50,11 +54,11 @@ class FeedAutoDownloadFilter(val includeFilterRaw: String? = "", val excludeFilt // if it's explicitly excluded, it shouldn't be autodownloaded // even if it has include terms - for (term in excludeTerms) { + for (term in excludeTerms!!) { if (title.contains(term.trim { it <= ' ' }.lowercase(Locale.getDefault()))) return false } - for (term in includeTerms) { + for (term in includeTerms!!) { if (title.contains(term.trim { it <= ' ' }.lowercase(Locale.getDefault()))) return true } @@ -71,11 +75,13 @@ class FeedAutoDownloadFilter(val includeFilterRaw: String? = "", val excludeFilt } fun getIncludeFilter(): List { - return if (includeFilterRaw == null) ArrayList() else parseTerms(includeFilterRaw) + return includeTerms!! +// return if (includeFilterRaw == null) ArrayList() else parseTerms(includeFilterRaw) } fun getExcludeFilter(): List { - return if (excludeFilterRaw == null) ArrayList() else parseTerms(excludeFilterRaw) + return excludeTerms!! +// return if (excludeFilterRaw == null) ArrayList() else parseTerms(excludeFilterRaw) } /** diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedMeasures.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedMeasures.kt new file mode 100644 index 00000000..d8d8aafa --- /dev/null +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedMeasures.kt @@ -0,0 +1,19 @@ +package ac.mdiq.podcini.storage.model + +import io.realm.kotlin.types.EmbeddedRealmObject +import io.realm.kotlin.types.annotations.Index + +class FeedMeasures : EmbeddedRealmObject { + @Index + var feedID: Long = 0L + + var playedCount: Int = 0 + + var unplayedCount: Int = 0 + + var newCount: Int = 0 + + var downloadCount: Int = 0 + + constructor() {} +} \ No newline at end of file diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedPreferences.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedPreferences.kt index 5ed97e68..e057ce4b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedPreferences.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedPreferences.kt @@ -5,16 +5,13 @@ import io.realm.kotlin.ext.realmSetOf import io.realm.kotlin.types.EmbeddedRealmObject import io.realm.kotlin.types.RealmSet import io.realm.kotlin.types.annotations.Ignore -import io.realm.kotlin.types.annotations.Index /** * Contains preferences for a single feed. */ class FeedPreferences : EmbeddedRealmObject { - @Index var feedID: Long = 0L - - var autoDownload: Boolean = false + var feedID: Long = 0L /** * @return true if this feed should be refreshed when everything else is being refreshed @@ -48,36 +45,63 @@ class FeedPreferences : EmbeddedRealmObject { } var volumeAdaption: Int = 0 - var tags: RealmSet = realmSetOf() - @Ignore val tagsAsString: String get() = tags.joinToString(TAG_SEPARATOR) + var tags: RealmSet = realmSetOf() + + var autoDownload: Boolean = false @Ignore - var autoDownloadFilter: FeedAutoDownloadFilter = FeedAutoDownloadFilter() - get() = FeedAutoDownloadFilter(autoDLInclude, autoDLExclude, autoDLMinDuration) + var autoDownloadFilter: FeedAutoDownloadFilter? = null + get() = field ?: FeedAutoDownloadFilter(autoDLInclude, autoDLExclude, autoDLMinDuration, markExcludedPlayed) set(value) { field = value - autoDLInclude = field.includeFilterRaw - autoDLExclude = field.excludeFilterRaw - autoDLMinDuration = field.minimalDurationFilter + autoDLInclude = value?.includeFilterRaw ?: "" + autoDLExclude = value?.excludeFilterRaw ?: "" + autoDLMinDuration = value?.minimalDurationFilter ?: -1 + markExcludedPlayed = value?.markExcludedPlayed ?: false } var autoDLInclude: String? = "" var autoDLExclude: String? = "" var autoDLMinDuration: Int = -1 + var markExcludedPlayed: Boolean = false + + var autoDLMaxEpisodes: Int = 3 + + @Ignore + var autoDLPolicy: AutoDLPolicy = AutoDLPolicy.ONLY_NEW + get() = AutoDLPolicy.fromCode(autoDLPolicyCode) + set(value) { + field = value + autoDLPolicyCode = value.code + } + var autoDLPolicyCode: Int = 0 var filterString: String = "" var sortOrderCode: Int = 0 - enum class AutoDeleteAction(@JvmField val code: Int) { + var sortOrderAuxCode: Int = 0 + + enum class AutoDLPolicy(val code: Int) { + ONLY_NEW(0), + NEWER(1), + OLDER(2); + + companion object { + fun fromCode(code: Int): AutoDLPolicy { + return enumValues().firstOrNull { it.code == code } ?: ONLY_NEW + } + } + } + + enum class AutoDeleteAction(val code: Int) { GLOBAL(0), ALWAYS(1), NEVER(2); companion object { - @JvmStatic fun fromCode(code: Int): AutoDeleteAction { return enumValues().firstOrNull { it.code == code } ?: NEVER } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedSortOrder.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedSortOrder.kt index 9dc447e3..4af49c94 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedSortOrder.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedSortOrder.kt @@ -16,7 +16,9 @@ enum class FeedSortOrder(val code: Int, val index: Int) { MOST_DOWNLOADED_UNPLAYED(13, 6), LEAST_DOWNLAODED_UNPLAYED(14, 6), NEW_EPISODES_MOST(15, 7), - NEW_EPISODES_LEAST(16, 7); + NEW_EPISODES_LEAST(16, 7), + LAST_DOWNLOAD_NEW_OLD(17, 8), + LAST_DOWNLOAD_OLD_NEW(18, 8); companion object { fun getSortOrder(dir: Int, index: Int): FeedSortOrder? { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/util/AudioMediaOperation.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/AudioMediaTools.kt similarity index 96% rename from app/src/main/kotlin/ac/mdiq/podcini/util/AudioMediaOperation.kt rename to app/src/main/kotlin/ac/mdiq/podcini/storage/utils/AudioMediaTools.kt index c5dc6221..04dc1eb8 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/util/AudioMediaOperation.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/AudioMediaTools.kt @@ -1,11 +1,11 @@ -package ac.mdiq.podcini.util +package ac.mdiq.podcini.storage.utils import java.io.* import java.nio.ByteBuffer import java.nio.ByteOrder // converted to Kotlin from the java file: https://gist.github.com/DrustZ/d3d3fc8fcc1067433db4dd3079f8d187 -object AudioMediaOperation { +object AudioMediaTools { fun mergeAudios(selection: Array, outpath: String?, callback: OperationCallbacks?) { var RECORDER_SAMPLERATE = 0 @@ -192,9 +192,8 @@ object AudioMediaOperation { } } - @Throws(IOException::class) - fun fullyReadFileToBytes(f: File): ByteArray { + private fun fullyReadFileToBytes(f: File): ByteArray { val size = f.length().toInt() val bytes = ByteArray(size) val tmpBuff = ByteArray(size) @@ -214,12 +213,11 @@ object AudioMediaOperation { } finally { fis.close() } - return bytes } @Throws(IOException::class) - fun writeInt(output: DataOutputStream, value: Int) { + private fun writeInt(output: DataOutputStream, value: Int) { output.write(value shr 0) output.write(value shr 8) output.write(value shr 16) @@ -227,13 +225,13 @@ object AudioMediaOperation { } @Throws(IOException::class) - fun writeShort(output: DataOutputStream, value: Short) { + private fun writeShort(output: DataOutputStream, value: Short) { output.write(value.toInt() shr 0) output.write(value.toInt() shr 8) } @Throws(IOException::class) - fun writeString(output: DataOutputStream, value: String) { + private fun writeString(output: DataOutputStream, value: String) { for (element in value) { output.write(element.code) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/util/sorting/ChapterStartTimeComparator.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ChapterStartTimeComparator.kt similarity index 85% rename from app/src/main/kotlin/ac/mdiq/podcini/util/sorting/ChapterStartTimeComparator.kt rename to app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ChapterStartTimeComparator.kt index cd054ccd..ee97e8a5 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/util/sorting/ChapterStartTimeComparator.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ChapterStartTimeComparator.kt @@ -1,4 +1,4 @@ -package ac.mdiq.podcini.util.sorting +package ac.mdiq.podcini.storage.utils import ac.mdiq.podcini.storage.model.Chapter diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ChapterUtils.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ChapterUtils.kt index e7b0f307..39c03d05 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ChapterUtils.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ChapterUtils.kt @@ -5,7 +5,6 @@ import android.content.Context import android.net.Uri import android.util.Log import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient -import ac.mdiq.podcini.util.sorting.ChapterStartTimeComparator import ac.mdiq.podcini.storage.model.Chapter import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.Playable diff --git a/app/src/main/kotlin/ac/mdiq/podcini/util/sorting/DownloadResultComparator.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/DownloadResultComparator.kt similarity index 89% rename from app/src/main/kotlin/ac/mdiq/podcini/util/sorting/DownloadResultComparator.kt rename to app/src/main/kotlin/ac/mdiq/podcini/storage/utils/DownloadResultComparator.kt index fdd8472e..9e88f180 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/util/sorting/DownloadResultComparator.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/DownloadResultComparator.kt @@ -1,4 +1,4 @@ -package ac.mdiq.podcini.util.sorting +package ac.mdiq.podcini.storage.utils import ac.mdiq.podcini.storage.model.DownloadResult diff --git a/app/src/main/kotlin/ac/mdiq/podcini/util/Converter.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/DurationConverter.kt similarity index 97% rename from app/src/main/kotlin/ac/mdiq/podcini/util/Converter.kt rename to app/src/main/kotlin/ac/mdiq/podcini/storage/utils/DurationConverter.kt index 83a7922f..5d649dfa 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/util/Converter.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/DurationConverter.kt @@ -1,12 +1,11 @@ -package ac.mdiq.podcini.util +package ac.mdiq.podcini.storage.utils import ac.mdiq.podcini.R import android.content.Context -import android.content.res.Resources import java.util.* /** Provides methods for converting various units. */ -object Converter { +object DurationConverter { private const val HOURS_MIL = 3600000 private const val MINUTES_MIL = 60000 private const val SECONDS_MIL = 1000 @@ -64,9 +63,7 @@ object Converter { fun durationStringShortToMs(input: String, durationIsInHours: Boolean): Int { val parts = input.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() if (parts.size != 2) return 0 - val modifier = if (durationIsInHours) 60 else 1 - return (parts[0].toInt() * 60 * 1000 * modifier + parts[1].toInt() * 1000 * modifier) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodeUtil.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodeUtil.kt index 82048356..5d06a695 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodeUtil.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodeUtil.kt @@ -1,6 +1,7 @@ package ac.mdiq.podcini.storage.utils -import ac.mdiq.podcini.preferences.UserPreferences.PREF_SMART_MARK_AS_PLAYED_SECS +import ac.mdiq.podcini.preferences.UserPreferences +import ac.mdiq.podcini.preferences.UserPreferences.Prefs.prefSmartMarkAsPlayedSecs import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Playable @@ -8,7 +9,7 @@ import ac.mdiq.podcini.storage.model.Playable object EpisodeUtil { private val TAG: String = EpisodeUtil::class.simpleName ?: "Anonymous" val smartMarkAsPlayedSecs: Int - get() = appPrefs.getString(PREF_SMART_MARK_AS_PLAYED_SECS, "30")!!.toInt() + get() = appPrefs.getString(UserPreferences.Prefs.prefSmartMarkAsPlayedSecs.name, "30")!!.toInt() @JvmStatic fun indexOfItemWithId(episodes: List, id: Long): Int { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/util/sorting/EpisodesPermutors.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodesPermutors.kt similarity index 84% rename from app/src/main/kotlin/ac/mdiq/podcini/util/sorting/EpisodesPermutors.kt rename to app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodesPermutors.kt index a06fe3af..8fb59e5c 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/util/sorting/EpisodesPermutors.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/EpisodesPermutors.kt @@ -1,4 +1,4 @@ -package ac.mdiq.podcini.util.sorting +package ac.mdiq.podcini.storage.utils import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.EpisodeSortOrder @@ -19,21 +19,39 @@ object EpisodesPermutors { var permutor: Permutor? = null when (sortOrder) { - EpisodeSortOrder.EPISODE_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemTitle(f1).compareTo(itemTitle(f2)) } - EpisodeSortOrder.EPISODE_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemTitle(f2).compareTo(itemTitle(f1)) } - EpisodeSortOrder.DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> pubDate(f1).compareTo(pubDate(f2)) } - EpisodeSortOrder.DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> pubDate(f2).compareTo(pubDate(f1)) } - EpisodeSortOrder.DURATION_SHORT_LONG -> comparator = Comparator { f1: Episode?, f2: Episode? -> duration(f1).compareTo(duration(f2)) } - EpisodeSortOrder.DURATION_LONG_SHORT -> comparator = Comparator { f1: Episode?, f2: Episode? -> duration(f2).compareTo(duration(f1)) } - EpisodeSortOrder.EPISODE_FILENAME_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemLink(f1).compareTo(itemLink(f2)) } - EpisodeSortOrder.EPISODE_FILENAME_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemLink(f2).compareTo(itemLink(f1)) } - EpisodeSortOrder.PLAYED_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> playDate(f1).compareTo(playDate(f2)) } - EpisodeSortOrder.PLAYED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> playDate(f2).compareTo(playDate(f1)) } - EpisodeSortOrder.COMPLETED_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> completeDate(f1).compareTo(completeDate(f2)) } - EpisodeSortOrder.COMPLETED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> completeDate(f2).compareTo(completeDate(f1)) } - - EpisodeSortOrder.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.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.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 -> permutor = object : Permutor { override fun reorder(queue: MutableList?) { if (!queue.isNullOrEmpty()) queue.shuffle() @@ -49,8 +67,10 @@ object EpisodesPermutors { if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList, false) } } - EpisodeSortOrder.SIZE_SMALL_LARGE -> comparator = Comparator { f1: Episode?, f2: Episode? -> size(f1).compareTo(size(f2)) } - EpisodeSortOrder.SIZE_LARGE_SMALL -> comparator = Comparator { f1: Episode?, f2: Episode? -> size(f2).compareTo(size(f1)) } + EpisodeSortOrder.SIZE_SMALL_LARGE -> comparator = Comparator { f1: Episode?, f2: Episode? -> size(f1).compareTo( + size(f2)) } + EpisodeSortOrder.SIZE_LARGE_SMALL -> comparator = Comparator { f1: Episode?, f2: Episode? -> size(f2).compareTo( + size(f1)) } } if (comparator != null) { val comparator2: Comparator = comparator @@ -69,6 +89,10 @@ object EpisodesPermutors { 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) } @@ -128,8 +152,8 @@ object EpisodesPermutors { // Sort each individual list by PubDate (ascending/descending) val itemComparator: Comparator = - if (ascending) Comparator { f1: Episode, f2: Episode -> f1.pubDate?.compareTo(f2.pubDate)?:-1 } - else Comparator { f1: Episode, f2: Episode -> f2.pubDate?.compareTo(f1.pubDate)?:-1 } + if (ascending) Comparator { f1: Episode, f2: Episode -> f1.pubDate.compareTo(f2.pubDate) } + else Comparator { f1: Episode, f2: Episode -> f2.pubDate.compareTo(f1.pubDate) } val feeds: MutableList> = ArrayList() for ((_, value) in map) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageResourceUtils.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageResourceUtils.kt index b3c15901..1b9af649 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageResourceUtils.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageResourceUtils.kt @@ -4,7 +4,7 @@ import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.Playable import ac.mdiq.podcini.preferences.UserPreferences -import ac.mdiq.podcini.preferences.UserPreferences.PREF_USE_EPISODE_COVER +import ac.mdiq.podcini.preferences.UserPreferences.Prefs.prefEpisodeCover import ac.mdiq.podcini.preferences.UserPreferences.appPrefs /** @@ -15,7 +15,7 @@ object ImageResourceUtils { * @return `true` if episodes should use their own cover, `false` otherwise */ val useEpisodeCoverSetting: Boolean - get() = appPrefs.getBoolean(PREF_USE_EPISODE_COVER, true) + get() = appPrefs.getBoolean(UserPreferences.Prefs.prefEpisodeCover.name, true) /** * returns the image location, does prefer the episode cover if available and enabled in settings. diff --git a/app/src/main/kotlin/ac/mdiq/podcini/util/ImageUtils.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageUtils.kt similarity index 99% rename from app/src/main/kotlin/ac/mdiq/podcini/util/ImageUtils.kt rename to app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageUtils.kt index bbdc4dec..46e5f06c 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/util/ImageUtils.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageUtils.kt @@ -1,4 +1,4 @@ -package ac.mdiq.podcini.util +package ac.mdiq.podcini.storage.utils import android.graphics.Bitmap import kotlin.math.abs @@ -274,5 +274,4 @@ class ImageUtils { return bitmap } } - } \ No newline at end of file diff --git a/app/src/main/kotlin/ac/mdiq/podcini/util/TimeSpeedConverter.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/TimeSpeedConverter.kt similarity index 92% rename from app/src/main/kotlin/ac/mdiq/podcini/util/TimeSpeedConverter.kt rename to app/src/main/kotlin/ac/mdiq/podcini/storage/utils/TimeSpeedConverter.kt index 32cd8ef9..69717d59 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/util/TimeSpeedConverter.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/TimeSpeedConverter.kt @@ -1,4 +1,4 @@ -package ac.mdiq.podcini.util +package ac.mdiq.podcini.storage.utils import ac.mdiq.podcini.preferences.UserPreferences diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/TTSActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/TTSActionButton.kt index 6d391275..5e5fd3c7 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/TTSActionButton.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/TTSActionButton.kt @@ -11,7 +11,7 @@ import ac.mdiq.podcini.storage.utils.FilesUtils.getMediafilename import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment.Companion.tts import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment.Companion.ttsReady import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment.Companion.ttsWorking -import ac.mdiq.podcini.util.AudioMediaOperation.mergeAudios +import ac.mdiq.podcini.storage.utils.AudioMediaTools.mergeAudios import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.FlowEvent diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/swipeactions/TogglePlaybackStateSwipeAction.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/swipeactions/TogglePlaybackStateSwipeAction.kt index 1279193a..8780852c 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/swipeactions/TogglePlaybackStateSwipeAction.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/swipeactions/TogglePlaybackStateSwipeAction.kt @@ -3,11 +3,11 @@ package ac.mdiq.podcini.ui.actions.swipeactions import android.content.Context import androidx.fragment.app.Fragment import ac.mdiq.podcini.R -import ac.mdiq.podcini.preferences.UserPreferences.shouldAutoDeleteItem -import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue import ac.mdiq.podcini.storage.database.Episodes.deleteMediaSync import ac.mdiq.podcini.storage.database.Episodes.setPlayState import ac.mdiq.podcini.storage.database.Episodes.setPlayStateSync +import ac.mdiq.podcini.storage.database.Episodes.shouldDeleteRemoveFromQueue +import ac.mdiq.podcini.storage.database.Feeds.shouldAutoDeleteItem import ac.mdiq.podcini.storage.database.Queues.removeFromQueueSync import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.EpisodeMedia diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt index 8d350182..a867bc17 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt @@ -3,12 +3,12 @@ package ac.mdiq.podcini.ui.activity import ac.mdiq.podcini.BuildConfig import ac.mdiq.podcini.R import ac.mdiq.podcini.databinding.MainActivityBinding -import ac.mdiq.podcini.net.feed.discovery.ItunesTopListLoader import ac.mdiq.podcini.net.download.DownloadStatus +import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.feed.FeedUpdateManager import ac.mdiq.podcini.net.feed.FeedUpdateManager.restartUpdateAlarm import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnceOrAsk -import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface +import ac.mdiq.podcini.net.feed.discovery.ItunesTopListLoader import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink import ac.mdiq.podcini.playback.cast.CastEnabledActivity import ac.mdiq.podcini.playback.service.PlaybackService @@ -80,7 +80,6 @@ import com.google.common.util.concurrent.MoreExecutors import kotlinx.coroutines.* import kotlinx.coroutines.flow.collectLatest import org.apache.commons.lang3.ArrayUtils -import java.util.* import kotlin.math.min /** @@ -141,7 +140,7 @@ class MainActivity : CastEnabledActivity() { ItunesTopListLoader.getSharedPrefs(this@MainActivity) } - if (savedInstanceState != null) ensureGeneratedViewIdGreaterThan(savedInstanceState.getInt(KEY_GENERATED_VIEW_ID, 0)) + if (savedInstanceState != null) ensureGeneratedViewIdGreaterThan(savedInstanceState.getInt(Extras.generated_view_id.name, 0)) WindowCompat.setDecorFitsSystemWindows(window, false) super.onCreate(savedInstanceState) @@ -306,7 +305,7 @@ class MainActivity : CastEnabledActivity() { override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putInt(KEY_GENERATED_VIEW_ID, View.generateViewId()) + outState.putInt(Extras.generated_view_id.name, View.generateViewId()) } private var prevState: Int = 0 @@ -373,10 +372,10 @@ class MainActivity : CastEnabledActivity() { private fun checkFirstLaunch() { val prefs = getSharedPreferences(PREF_NAME, MODE_PRIVATE) - if (prefs.getBoolean(PREF_IS_FIRST_LAUNCH, true)) { + if (prefs.getBoolean(Extras.prefMainActivityIsFirstLaunch.name, true)) { restartUpdateAlarm(this, true) val edit = prefs.edit() - edit.putBoolean(PREF_IS_FIRST_LAUNCH, false) + edit.putBoolean(Extras.prefMainActivityIsFirstLaunch.name, false) edit.apply() } } @@ -602,13 +601,15 @@ class MainActivity : CastEnabledActivity() { } private var eventSink: Job? = null + private var eventStickySink: Job? = null private fun cancelFlowEvents() { eventSink?.cancel() eventSink = null + eventStickySink?.cancel() + eventStickySink = null } private fun procFlowEvents() { - if (eventSink != null) return - eventSink = lifecycleScope.launch { + if (eventSink == null) eventSink = lifecycleScope.launch { EventFlow.events.collectLatest { event -> Logd(TAG, "Received event: ${event.TAG}") when (event) { @@ -620,25 +621,33 @@ class MainActivity : CastEnabledActivity() { } } } + if (eventStickySink == null) eventStickySink = lifecycleScope.launch { + EventFlow.stickyEvents.collectLatest { event -> + Logd(TAG, "Received sticky event: ${event.TAG}") +// when (event) { +// else -> {} +// } + } + } } private fun handleNavIntent() { Logd(TAG, "handleNavIntent()") val intent = intent when { - intent.hasExtra(EXTRA_FEED_ID) -> { - val feedId = intent.getLongExtra(EXTRA_FEED_ID, 0) + intent.hasExtra(Extras.fragment_feed_id.name) -> { + val feedId = intent.getLongExtra(Extras.fragment_feed_id.name, 0) val args = intent.getBundleExtra(MainActivityStarter.EXTRA_FRAGMENT_ARGS) if (feedId > 0) { - val startedFromSearch = intent.getBooleanExtra(EXTRA_STARTED_FROM_SEARCH, false) - val addToBackStack = intent.getBooleanExtra(EXTRA_ADD_TO_BACK_STACK, false) + val startedFromSearch = intent.getBooleanExtra(Extras.started_from_search.name, false) + val addToBackStack = intent.getBooleanExtra(Extras.add_to_back_stack.name, false) if (startedFromSearch || addToBackStack) loadChildFragment(FeedEpisodesFragment.newInstance(feedId)) else loadFeedFragmentById(feedId, args) } bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED) } - intent.hasExtra(EXTRA_FEED_URL) -> { - val feedurl = intent.getStringExtra(EXTRA_FEED_URL) + intent.hasExtra(Extras.fragment_feed_url.name) -> { + val feedurl = intent.getStringExtra(Extras.fragment_feed_url.name) if (feedurl != null) loadChildFragment(OnlineFeedViewFragment.newInstance(feedurl)) } intent.hasExtra(MainActivityStarter.EXTRA_FRAGMENT_TAG) -> { @@ -660,7 +669,7 @@ class MainActivity : CastEnabledActivity() { if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_DOWNLOAD_LOGS, false)) DownloadLogFragment().show(supportFragmentManager, null) - if (intent.getBooleanExtra(EXTRA_REFRESH_ON_START, false)) runOnceOrAsk(this) + if (intent.getBooleanExtra(Extras.refresh_on_start.name, false)) runOnceOrAsk(this) // to avoid handling the intent twice when the configuration changes setIntent(Intent(this@MainActivity, MainActivity::class.java)) @@ -756,24 +765,26 @@ class MainActivity : CastEnabledActivity() { return super.onKeyUp(keyCode, event) } + @Suppress("EnumEntryName") + enum class Extras { + prefMainActivityIsFirstLaunch, + fragment_feed_id, + fragment_feed_url, + refresh_on_start, + started_from_search, + add_to_back_stack, + generated_view_id, + } + companion object { private val TAG: String = MainActivity::class.simpleName ?: "Anonymous" const val MAIN_FRAGMENT_TAG: String = "main" - const val PREF_NAME: String = "MainActivityPrefs" - const val PREF_IS_FIRST_LAUNCH: String = "prefMainActivityIsFirstLaunch" - - const val EXTRA_FEED_ID: String = "fragment_feed_id" - const val EXTRA_FEED_URL: String = "fragment_feed_url" - const val EXTRA_REFRESH_ON_START: String = "refresh_on_start" - const val EXTRA_STARTED_FROM_SEARCH: String = "started_from_search" - const val EXTRA_ADD_TO_BACK_STACK: String = "add_to_back_stack" - const val KEY_GENERATED_VIEW_ID: String = "generated_view_id" @JvmStatic fun getIntentToOpenFeed(context: Context, feedId: Long): Intent { val intent = Intent(context.applicationContext, MainActivity::class.java) - intent.putExtra(EXTRA_FEED_ID, feedId) + intent.putExtra(Extras.fragment_feed_id.name, feedId) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) return intent } @@ -781,7 +792,7 @@ class MainActivity : CastEnabledActivity() { @JvmStatic fun showOnlineFeed(context: Context, feedUrl: String): Intent { val intent = Intent(context.applicationContext, MainActivity::class.java) - intent.putExtra(EXTRA_FEED_URL, feedUrl) + intent.putExtra(Extras.fragment_feed_url.name, feedUrl) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) return intent } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/OnlineFeedViewActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/OnlineFeedViewActivity.kt index 03a9713d..512867f5 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/OnlineFeedViewActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/OnlineFeedViewActivity.kt @@ -39,7 +39,7 @@ class OnlineFeedViewActivity : AppCompatActivity() { Logd(TAG, "Activity was started with url $feedUrl") val intent = MainActivity.showOnlineFeed(this, feedUrl) - intent.putExtra(MainActivity.EXTRA_STARTED_FROM_SEARCH, getIntent().getBooleanExtra(MainActivity.EXTRA_STARTED_FROM_SEARCH, false)) + intent.putExtra(MainActivity.Extras.started_from_search.name, getIntent().getBooleanExtra(MainActivity.Extras.started_from_search.name, false)) startActivity(intent) finish() } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt index 2bf7cff1..72456277 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt @@ -5,7 +5,6 @@ import ac.mdiq.podcini.databinding.SubscriptionSelectionActivityBinding import ac.mdiq.podcini.preferences.ThemeSwitcher import ac.mdiq.podcini.storage.database.Feeds.getFeedList import ac.mdiq.podcini.storage.model.Feed -import ac.mdiq.podcini.ui.activity.MainActivity.Companion.EXTRA_FEED_ID import android.app.Activity import android.content.Intent import android.graphics.Bitmap @@ -77,7 +76,7 @@ class SelectSubscriptionActivity : AppCompatActivity() { val intent = Intent(this, MainActivity::class.java) intent.setAction(Intent.ACTION_MAIN) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) - intent.putExtra(EXTRA_FEED_ID, feed.id.toString()) + intent.putExtra(MainActivity.Extras.fragment_feed_id.name, feed.id.toString()) val id = "subscription-" + feed.id val icon: IconCompat = if (bitmap != null) IconCompat.createWithAdaptiveBitmap(bitmap) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/SplashActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/SplashActivity.kt index 7a95d872..caa11d1b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/SplashActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/SplashActivity.kt @@ -26,8 +26,6 @@ class SplashActivity : Activity() { val scope = CoroutineScope(Dispatchers.IO) scope.launch(Dispatchers.IO) { try { -// PodDBAdapter.getInstance().open() -// PodDBAdapter.getInstance().close() withContext(Dispatchers.Main) { val intent = Intent(this@SplashActivity, MainActivity::class.java) startActivity(intent) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/WidgetConfigActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/WidgetConfigActivity.kt index 73ec35a6..12f8839b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/WidgetConfigActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/WidgetConfigActivity.kt @@ -98,12 +98,12 @@ class WidgetConfigActivity : AppCompatActivity() { PlayerWidget.getSharedPrefs(this) // val prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE) - ckPlaybackSpeed.isChecked = prefs!!.getBoolean(PlayerWidget.KEY_WIDGET_PLAYBACK_SPEED + appWidgetId, true) - ckRewind.isChecked = prefs!!.getBoolean(PlayerWidget.KEY_WIDGET_REWIND + appWidgetId, true) - ckFastForward.isChecked = prefs!!.getBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + appWidgetId, true) - ckSkip.isChecked = prefs!!.getBoolean(PlayerWidget.KEY_WIDGET_SKIP + appWidgetId, true) + ckPlaybackSpeed.isChecked = prefs!!.getBoolean(PlayerWidget.Prefs.widget_playback_speed.name + appWidgetId, true) + ckRewind.isChecked = prefs!!.getBoolean(PlayerWidget.Prefs.widget_rewind.name + appWidgetId, true) + ckFastForward.isChecked = prefs!!.getBoolean(PlayerWidget.Prefs.widget_fast_forward.name + appWidgetId, true) + ckSkip.isChecked = prefs!!.getBoolean(PlayerWidget.Prefs.widget_skip.name + appWidgetId, true) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - val color = prefs!!.getInt(PlayerWidget.KEY_WIDGET_COLOR + appWidgetId, PlayerWidget.DEFAULT_COLOR) + val color = prefs!!.getInt(PlayerWidget.Prefs.widget_color.name + appWidgetId, PlayerWidget.DEFAULT_COLOR) val opacity = Color.alpha(color) * 100 / 0xFF opacitySeekBar.setProgress(opacity, false) @@ -127,11 +127,11 @@ class WidgetConfigActivity : AppCompatActivity() { Logd("WidgetConfigActivity", "confirmCreateWidget appWidgetId $appWidgetId") // val prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE) val editor = prefs!!.edit() - editor.putInt(PlayerWidget.KEY_WIDGET_COLOR + appWidgetId, backgroundColor) - editor.putBoolean(PlayerWidget.KEY_WIDGET_PLAYBACK_SPEED + appWidgetId, ckPlaybackSpeed.isChecked) - editor.putBoolean(PlayerWidget.KEY_WIDGET_SKIP + appWidgetId, ckSkip.isChecked) - editor.putBoolean(PlayerWidget.KEY_WIDGET_REWIND + appWidgetId, ckRewind.isChecked) - editor.putBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + appWidgetId, ckFastForward.isChecked) + editor.putInt(PlayerWidget.Prefs.widget_color.name + appWidgetId, backgroundColor) + editor.putBoolean(PlayerWidget.Prefs.widget_playback_speed.name + appWidgetId, ckPlaybackSpeed.isChecked) + editor.putBoolean(PlayerWidget.Prefs.widget_skip.name + appWidgetId, ckSkip.isChecked) + editor.putBoolean(PlayerWidget.Prefs.widget_rewind.name + appWidgetId, ckRewind.isChecked) + editor.putBoolean(PlayerWidget.Prefs.widget_fast_forward.name + appWidgetId, ckFastForward.isChecked) editor.apply() val resultValue = Intent() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/adapter/SelectableAdapter.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/adapter/SelectableAdapter.kt index bb8ca17d..6d955a40 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/adapter/SelectableAdapter.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/adapter/SelectableAdapter.kt @@ -26,7 +26,8 @@ abstract class SelectableAdapter(private val activ shouldSelectLazyLoadedItems = false selectedIds.clear() selectedIds.add(getItemId(pos)) - notifyDataSetChanged() + notifyItemChanged(pos, "foo") +// notifyDataSetChanged() actionMode = activity.startActionMode(object : ActionMode.Callback { override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/DrawerPreferencesDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/DrawerPreferencesDialog.kt index 21276ab0..79a9dc75 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/DrawerPreferencesDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/DrawerPreferencesDialog.kt @@ -8,6 +8,7 @@ import ac.mdiq.podcini.ui.fragment.NavDrawerFragment import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.defaultPage import ac.mdiq.podcini.preferences.UserPreferences.hiddenDrawerItems +import ac.mdiq.podcini.util.Logd import androidx.annotation.OptIn import androidx.media3.common.util.UnstableApi @@ -15,24 +16,24 @@ import androidx.media3.common.util.UnstableApi object DrawerPreferencesDialog { fun show(context: Context, callback: Runnable?) { - val hiddenDrawerItems = hiddenDrawerItems.toMutableList() + val hiddenItems = hiddenDrawerItems.map { it.trim() }.toMutableSet() val navTitles = context.resources.getStringArray(R.array.nav_drawer_titles) val checked = BooleanArray(NavDrawerFragment.NAV_DRAWER_TAGS.size) for (i in NavDrawerFragment.NAV_DRAWER_TAGS.indices) { val tag = NavDrawerFragment.NAV_DRAWER_TAGS[i] - if (!hiddenDrawerItems.contains(tag)) checked[i] = true + if (!hiddenItems.contains(tag)) checked[i] = true } val builder = MaterialAlertDialogBuilder(context) builder.setTitle(R.string.drawer_preferences) builder.setMultiChoiceItems(navTitles, checked) { _: DialogInterface?, which: Int, isChecked: Boolean -> - if (isChecked) hiddenDrawerItems.remove(NavDrawerFragment.NAV_DRAWER_TAGS[which]) - else hiddenDrawerItems.add(NavDrawerFragment.NAV_DRAWER_TAGS[which]) + if (isChecked) hiddenItems.remove(NavDrawerFragment.NAV_DRAWER_TAGS[which]) + else hiddenItems.add((NavDrawerFragment.NAV_DRAWER_TAGS[which]).trim()) } builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int -> - UserPreferences.hiddenDrawerItems = hiddenDrawerItems - if (hiddenDrawerItems.contains(defaultPage)) { + hiddenDrawerItems = hiddenItems.toList() + if (hiddenItems.contains(defaultPage)) { for (tag in NavDrawerFragment.NAV_DRAWER_TAGS) { - if (!hiddenDrawerItems.contains(tag)) { + if (!hiddenItems.contains(tag)) { defaultPage = tag break } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/EpisodeFilterDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/EpisodeFilterDialog.kt index 6cfe9a13..47e6863a 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/EpisodeFilterDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/EpisodeFilterDialog.kt @@ -117,12 +117,13 @@ abstract class EpisodeFilterDialog : BottomSheetDialogFragment() { abstract fun onFilterChanged(newFilterValues: Set) enum class FeedItemFilterGroup(vararg values: ItemProperties) { - PLAYED(ItemProperties(R.string.hide_played_episodes_label, EpisodeFilter.PLAYED), ItemProperties(R.string.not_played, EpisodeFilter.UNPLAYED)), - PAUSED(ItemProperties(R.string.hide_paused_episodes_label, EpisodeFilter.PAUSED), ItemProperties(R.string.not_paused, EpisodeFilter.NOT_PAUSED)), - FAVORITE(ItemProperties(R.string.hide_is_favorite_label, EpisodeFilter.IS_FAVORITE), ItemProperties(R.string.not_favorite, EpisodeFilter.NOT_FAVORITE)), - MEDIA(ItemProperties(R.string.has_media, EpisodeFilter.HAS_MEDIA), ItemProperties(R.string.no_media, EpisodeFilter.NO_MEDIA)), - QUEUED(ItemProperties(R.string.queued_label, EpisodeFilter.QUEUED), ItemProperties(R.string.not_queued_label, EpisodeFilter.NOT_QUEUED)), - DOWNLOADED(ItemProperties(R.string.hide_downloaded_episodes_label, EpisodeFilter.DOWNLOADED), ItemProperties(R.string.hide_not_downloaded_episodes_label, EpisodeFilter.NOT_DOWNLOADED)); + PLAYED(ItemProperties(R.string.hide_played_episodes_label, EpisodeFilter.States.played.name), ItemProperties(R.string.not_played, EpisodeFilter.States.unplayed.name)), + PAUSED(ItemProperties(R.string.hide_paused_episodes_label, EpisodeFilter.States.paused.name), ItemProperties(R.string.not_paused, EpisodeFilter.States.not_paused.name)), + FAVORITE(ItemProperties(R.string.hide_is_favorite_label, EpisodeFilter.States.is_favorite.name), ItemProperties(R.string.not_favorite, EpisodeFilter.States.not_favorite.name)), + MEDIA(ItemProperties(R.string.has_media, EpisodeFilter.States.has_media.name), ItemProperties(R.string.no_media, EpisodeFilter.States.no_media.name)), + QUEUED(ItemProperties(R.string.queued_label, EpisodeFilter.States.queued.name), ItemProperties(R.string.not_queued_label, EpisodeFilter.States.not_queued.name)), + DOWNLOADED(ItemProperties(R.string.downloaded_label, EpisodeFilter.States.downloaded.name), ItemProperties(R.string.not_downloaded_label, EpisodeFilter.States.not_downloaded.name)), + AUTO_DOWNLOADABLE(ItemProperties(R.string.auto_downloadable_label, EpisodeFilter.States.auto_downloadable.name), ItemProperties(R.string.not_auto_downloadable_label, EpisodeFilter.States.not_auto_downloadable.name)); @JvmField val values: Array = arrayOf(*values) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/EpisodeSortDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/EpisodeSortDialog.kt index cca097ea..ec05d9fe 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/EpisodeSortDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/EpisodeSortDialog.kt @@ -44,6 +44,7 @@ open class EpisodeSortDialog : BottomSheetDialogFragment() { onAddItem(R.string.feed_title, EpisodeSortOrder.FEED_TITLE_A_Z, EpisodeSortOrder.FEED_TITLE_Z_A, true) onAddItem(R.string.duration, EpisodeSortOrder.DURATION_SHORT_LONG, EpisodeSortOrder.DURATION_LONG_SHORT, true) onAddItem(R.string.publish_date, EpisodeSortOrder.DATE_OLD_NEW, EpisodeSortOrder.DATE_NEW_OLD, false) + onAddItem(R.string.download_date, EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW, EpisodeSortOrder.DOWNLOAD_DATE_NEW_OLD, false) onAddItem(R.string.last_played_date, EpisodeSortOrder.PLAYED_DATE_OLD_NEW, EpisodeSortOrder.PLAYED_DATE_NEW_OLD, false) onAddItem(R.string.completed_date, EpisodeSortOrder.COMPLETED_DATE_OLD_NEW, EpisodeSortOrder.COMPLETED_DATE_NEW_OLD, false) onAddItem(R.string.size, EpisodeSortOrder.SIZE_SMALL_LARGE, EpisodeSortOrder.SIZE_LARGE_SMALL, false) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/FeedSortDialogNew.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/FeedSortDialog.kt similarity index 91% rename from app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/FeedSortDialogNew.kt rename to app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/FeedSortDialog.kt index 41411cb4..8a365bc8 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/FeedSortDialogNew.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/FeedSortDialog.kt @@ -4,8 +4,7 @@ import ac.mdiq.podcini.R import ac.mdiq.podcini.databinding.SortDialogBinding import ac.mdiq.podcini.databinding.SortDialogItemActiveBinding import ac.mdiq.podcini.databinding.SortDialogItemBinding -import ac.mdiq.podcini.preferences.UserPreferences.PREF_DRAWER_FEED_ORDER -import ac.mdiq.podcini.preferences.UserPreferences.PREF_DRAWER_FEED_ORDER_DIRECTION +import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.storage.model.FeedSortOrder import ac.mdiq.podcini.storage.model.FeedSortOrder.Companion.getSortOrder @@ -27,8 +26,8 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment -open class FeedSortDialogNew : BottomSheetDialogFragment() { - private val TAG: String = FeedSortDialogNew::class.simpleName ?: "Anonymous" +open class FeedSortDialog : BottomSheetDialogFragment() { + private val TAG: String = FeedSortDialog::class.simpleName ?: "Anonymous" protected var _binding: SortDialogBinding? = null protected val binding get() = _binding!! @@ -44,7 +43,7 @@ open class FeedSortDialogNew : BottomSheetDialogFragment() { _binding = SortDialogBinding.inflate(inflater) binding.gridLayout.columnCount = 1 populateList() - binding.keepSortedCheckbox.setOnCheckedChangeListener { _: CompoundButton?, _: Boolean -> this@FeedSortDialogNew.onSelectionChanged() } + binding.keepSortedCheckbox.setOnCheckedChangeListener { _: CompoundButton?, _: Boolean -> this@FeedSortDialog.onSelectionChanged() } return binding.root } @@ -55,14 +54,15 @@ open class FeedSortDialogNew : BottomSheetDialogFragment() { private fun populateList() { binding.gridLayout.removeAllViews() - onAddItem(R.string.feed_order_unplayed_count, FeedSortOrder.UNPLAYED_OLD_NEW, FeedSortOrder.UNPLAYED_NEW_OLD, false) onAddItem(R.string.feed_order_alphabetical, FeedSortOrder.ALPHABETIC_A_Z, FeedSortOrder.ALPHABETIC_Z_A, true) onAddItem(R.string.feed_order_last_update, FeedSortOrder.LAST_UPDATED_OLD_NEW, FeedSortOrder.LAST_UPDATED_NEW_OLD, false) + onAddItem(R.string.feed_order_last_download, FeedSortOrder.LAST_DOWNLOAD_OLD_NEW, FeedSortOrder.LAST_DOWNLOAD_NEW_OLD, false) onAddItem(R.string.feed_order_last_unread_update, FeedSortOrder.LAST_UPDATED_UNPLAYED_OLD_NEW, FeedSortOrder.LAST_UPDATED_UNPLAYED_NEW_OLD, false) + onAddItem(R.string.feed_order_new_episodes, FeedSortOrder.NEW_EPISODES_LEAST, FeedSortOrder.NEW_EPISODES_MOST, false) + onAddItem(R.string.feed_order_unplayed_count, FeedSortOrder.UNPLAYED_OLD_NEW, FeedSortOrder.UNPLAYED_NEW_OLD, false) onAddItem(R.string.feed_order_most_played, FeedSortOrder.LEAST_PLAYED, FeedSortOrder.MOST_PLAYED, false) - onAddItem(R.string.feed_counter_downloaded, FeedSortOrder.LEAST_DOWNLAODED, FeedSortOrder.MOST_DOWNLOADED, false) onAddItem(R.string.feed_counter_downloaded_unplayed, FeedSortOrder.LEAST_DOWNLAODED_UNPLAYED, FeedSortOrder.MOST_DOWNLOADED_UNPLAYED, false) - onAddItem(R.string.feed_order_new_episodes, FeedSortOrder.NEW_EPISODES_LEAST, FeedSortOrder.NEW_EPISODES_MOST, false) + onAddItem(R.string.feed_counter_downloaded, FeedSortOrder.LEAST_DOWNLAODED, FeedSortOrder.MOST_DOWNLOADED, false) } protected open fun onAddItem(title: Int, ascending: FeedSortOrder, descending: FeedSortOrder, ascendingIsDefault: Boolean) { @@ -137,10 +137,10 @@ open class FeedSortDialogNew : BottomSheetDialogFragment() { private fun setFeedOrder(selected: String, dir: Int) { appPrefs.edit() - .putString(PREF_DRAWER_FEED_ORDER, selected) + .putString(UserPreferences.Prefs.prefDrawerFeedOrder.name, selected) .apply() appPrefs.edit() - .putInt(PREF_DRAWER_FEED_ORDER_DIRECTION, dir) + .putInt(UserPreferences.Prefs.prefDrawerFeedOrderDir.name, dir) .apply() } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt index ce9e48fb..074ae998 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt @@ -20,7 +20,7 @@ import ac.mdiq.podcini.preferences.SleepTimerPreferences.vibrate import ac.mdiq.podcini.storage.model.Playable import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion.TAG import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr -import ac.mdiq.podcini.util.Converter.getDurationStringLong +import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.FlowEvent diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt index a2ef53db..7ede9775 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt @@ -9,7 +9,7 @@ import ac.mdiq.podcini.playback.base.InTheatre.curMedia import ac.mdiq.podcini.playback.base.InTheatre.curState import ac.mdiq.podcini.playback.service.PlaybackService.Companion.currentMediaType import ac.mdiq.podcini.preferences.UserPreferences -import ac.mdiq.podcini.preferences.UserPreferences.PREF_PLAYBACK_SPEED_ARRAY +import ac.mdiq.podcini.preferences.UserPreferences.Prefs.prefPlaybackSpeedArray import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence import ac.mdiq.podcini.preferences.UserPreferences.videoPlaybackSpeed @@ -180,7 +180,7 @@ import java.util.* } var playbackSpeedArray: List - get() = readPlaybackSpeedArray(appPrefs.getString(PREF_PLAYBACK_SPEED_ARRAY, null)) + get() = readPlaybackSpeedArray(appPrefs.getString(UserPreferences.Prefs.prefPlaybackSpeedArray.name, null)) set(speeds) { val format = DecimalFormatSymbols(Locale.US) format.decimalSeparator = '.' @@ -189,7 +189,7 @@ import java.util.* for (speed in speeds) { jsonArray.put(speedFormat.format(speed.toDouble())) } - appPrefs.edit().putString(PREF_PLAYBACK_SPEED_ARRAY, jsonArray.toString()).apply() + appPrefs.edit().putString(UserPreferences.Prefs.prefPlaybackSpeedArray.name, jsonArray.toString()).apply() } private fun readPlaybackSpeedArray(valueFromPrefs: String?): List { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AddFeedFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AddFeedFragment.kt index bc7afb16..d0bf9178 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AddFeedFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AddFeedFragment.kt @@ -3,14 +3,16 @@ package ac.mdiq.podcini.ui.fragment import ac.mdiq.podcini.R import ac.mdiq.podcini.databinding.AddfeedBinding import ac.mdiq.podcini.databinding.EditTextDialogBinding -import ac.mdiq.podcini.net.feed.discovery.* import ac.mdiq.podcini.net.feed.FeedUpdateManager +import ac.mdiq.podcini.net.feed.discovery.* +import ac.mdiq.podcini.preferences.OpmlBackupAgent.Companion.isOPMLRestared +import ac.mdiq.podcini.preferences.OpmlBackupAgent.Companion.performRestore import ac.mdiq.podcini.storage.database.Feeds.updateFeed -import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.storage.model.EpisodeSortOrder +import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.activity.OpmlImportActivity -import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion +import ac.mdiq.podcini.ui.fragment.NavDrawerFragment.Companion.feedCount import ac.mdiq.podcini.util.Logd import android.content.* import android.net.Uri @@ -23,6 +25,7 @@ import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import android.widget.TextView import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog import androidx.documentfile.provider.DocumentFile import androidx.fragment.app.Fragment import androidx.media3.common.util.UnstableApi @@ -45,11 +48,10 @@ class AddFeedFragment : Fragment() { private var activity: MainActivity? = null private var displayUpArrow = false - private val chooseOpmlImportPathLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) - { uri: Uri? -> this.chooseOpmlImportPathResult(uri) } + private val chooseOpmlImportPathLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> + this.chooseOpmlImportPathResult(uri) } - private val addLocalFolderLauncher = registerForActivityResult(AddLocalFolder()) - { uri: Uri? -> this.addLocalFolderResult(uri) } + private val addLocalFolderLauncher = registerForActivityResult(AddLocalFolder()) { uri: Uri? -> this.addLocalFolderResult(uri) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { super.onCreateView(inflater, container, savedInstanceState) @@ -101,6 +103,20 @@ class AddFeedFragment : Fragment() { } binding.searchButton.setOnClickListener { performSearch() } + if (isOPMLRestared && feedCount == 0) { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.restore_subscriptions_label) + .setMessage(R.string.restore_subscriptions_summary) + .setPositiveButton("Yes") { dialog, _ -> + performRestore(requireContext()) + dialog.dismiss() + } + .setNegativeButton("No") { dialog, _ -> + dialog.dismiss() + } + .show() + } + return binding.root } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt index 4e546a78..942ad461 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt @@ -1,8 +1,7 @@ package ac.mdiq.podcini.ui.fragment import ac.mdiq.podcini.R -import ac.mdiq.podcini.preferences.UserPreferences.PREF_FILTER_ALL_EPISODES -import ac.mdiq.podcini.preferences.UserPreferences.PREF_SORT_ALL_EPISODES +import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.storage.database.Episodes.getEpisodes import ac.mdiq.podcini.storage.database.Episodes.getEpisodesCount @@ -65,9 +64,14 @@ import kotlin.math.min cancelFlowEvents() } + private var loadItemsRunning = false override fun loadData(): List { - allEpisodes = getEpisodes(0, Int.MAX_VALUE, getFilter(), allEpisodesSortOrder, false) - Logd(TAG, "loadData ${allEpisodes.size}") + if (!loadItemsRunning) { + loadItemsRunning = true + allEpisodes = getEpisodes(0, Int.MAX_VALUE, getFilter(), allEpisodesSortOrder, false) + Logd(TAG, "loadData ${allEpisodes.size}") + loadItemsRunning = false + } if (allEpisodes.isEmpty()) return listOf() return allEpisodes.subList(0, min(allEpisodes.size-1, page * EPISODES_PER_PAGE)) } @@ -98,8 +102,8 @@ import kotlin.math.min R.id.filter_items -> AllEpisodesFilterDialog.newInstance(getFilter()).show(childFragmentManager, null) R.id.action_favorites -> { val filter = ArrayList(getFilter().valuesList) - if (filter.contains(EpisodeFilter.IS_FAVORITE)) filter.remove(EpisodeFilter.IS_FAVORITE) - else filter.add(EpisodeFilter.IS_FAVORITE) + if (filter.contains(EpisodeFilter.States.is_favorite.name)) filter.remove(EpisodeFilter.States.is_favorite.name) + else filter.add(EpisodeFilter.States.is_favorite.name) onFilterChanged(FlowEvent.AllEpisodesFilterEvent(HashSet(filter))) } R.id.episodes_sort -> AllEpisodesSortDialog().show(childFragmentManager.beginTransaction(), "SortDialog") @@ -157,8 +161,11 @@ import kotlin.math.min sortOrder = allEpisodesSortOrder } override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) { - if (ascending == EpisodeSortOrder.DATE_OLD_NEW || ascending == EpisodeSortOrder.DURATION_SHORT_LONG - || ascending == EpisodeSortOrder.PLAYED_DATE_OLD_NEW || ascending == EpisodeSortOrder.COMPLETED_DATE_OLD_NEW + if (ascending == EpisodeSortOrder.DATE_OLD_NEW + || ascending == EpisodeSortOrder.DURATION_SHORT_LONG + || ascending == EpisodeSortOrder.PLAYED_DATE_OLD_NEW + || ascending == EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW + || ascending == EpisodeSortOrder.COMPLETED_DATE_OLD_NEW || ascending == EpisodeSortOrder.EPISODE_TITLE_A_Z) super.onAddItem(title, ascending, descending, ascendingIsDefault) } @@ -186,14 +193,14 @@ import kotlin.math.min val TAG = AllEpisodesFragment::class.simpleName ?: "Anonymous" const val PREF_NAME: String = "PrefAllEpisodesFragment" var allEpisodesSortOrder: EpisodeSortOrder? - get() = EpisodeSortOrder.fromCodeString(appPrefs.getString(PREF_SORT_ALL_EPISODES, "" + EpisodeSortOrder.DATE_NEW_OLD.code)) + get() = EpisodeSortOrder.fromCodeString(appPrefs.getString(UserPreferences.Prefs.prefEpisodesSort.name, "" + EpisodeSortOrder.DATE_NEW_OLD.code)) set(s) { - appPrefs.edit().putString(PREF_SORT_ALL_EPISODES, "" + s!!.code).apply() + appPrefs.edit().putString(UserPreferences.Prefs.prefEpisodesSort.name, "" + s!!.code).apply() } var prefFilterAllEpisodes: String - get() = appPrefs.getString(PREF_FILTER_ALL_EPISODES, "")?:"" + get() = appPrefs.getString(UserPreferences.Prefs.prefEpisodesFilter.name, "")?:"" set(filter) { - appPrefs.edit().putString(PREF_FILTER_ALL_EPISODES, filter).apply() + appPrefs.edit().putString(UserPreferences.Prefs.prefEpisodesFilter.name, filter).apply() } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt index 72c65b93..287b9493 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt @@ -38,12 +38,11 @@ import ac.mdiq.podcini.ui.dialog.MediaPlayerErrorDialog import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog import ac.mdiq.podcini.ui.dialog.SleepTimerDialog import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog -import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion import ac.mdiq.podcini.ui.view.ChapterSeekBar import ac.mdiq.podcini.ui.view.PlayButton -import ac.mdiq.podcini.util.Converter +import ac.mdiq.podcini.storage.utils.DurationConverter import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.TimeSpeedConverter +import ac.mdiq.podcini.storage.utils.TimeSpeedConverter import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.FlowEvent import android.app.Activity @@ -209,33 +208,38 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar // updatePosition(PlaybackPositionEvent(position, duration)) // } + private var loadItemsRunning = false fun loadMediaInfo(includingChapters: Boolean) { val actMain = (activity as MainActivity) if (curMedia == null) { if (actMain.isPlayerVisible()) actMain.setPlayerVisible(false) return } - if (!actMain.isPlayerVisible()) actMain.setPlayerVisible(true) - if (!isCollapsed && (currentMedia == null || curMedia?.getIdentifier() != currentMedia?.getIdentifier())) playerDetailsFragment?.updateInfo() - - if (currentMedia == null || curMedia?.getIdentifier() != currentMedia?.getIdentifier() || (includingChapters && !curMedia!!.chaptersLoaded())) { - Logd(TAG, "loadMediaInfo loading details ${curMedia?.getIdentifier()} chapter: $includingChapters") - lifecycleScope.launch { - withContext(Dispatchers.IO) { - curMedia!!.apply { - if (includingChapters) ChapterUtils.loadChapters(this, requireContext(), false) + if (!loadItemsRunning) { + loadItemsRunning = true + if (!actMain.isPlayerVisible()) actMain.setPlayerVisible(true) + if (!isCollapsed && (currentMedia == null || curMedia?.getIdentifier() != currentMedia?.getIdentifier())) playerDetailsFragment?.updateInfo() + + if (currentMedia == null || curMedia?.getIdentifier() != currentMedia?.getIdentifier() || (includingChapters && !curMedia!!.chaptersLoaded())) { + Logd(TAG, "loadMediaInfo loading details ${curMedia?.getIdentifier()} chapter: $includingChapters") + lifecycleScope.launch { + withContext(Dispatchers.IO) { + curMedia!!.apply { + if (includingChapters) ChapterUtils.loadChapters(this, requireContext(), false) + } } - } - currentMedia = curMedia - val item = (currentMedia as? EpisodeMedia)?.episode - if (item != null) playerDetailsFragment?.setItem(item) - updateUi() - playerUI?.updateUi(currentMedia) + currentMedia = curMedia + val item = (currentMedia as? EpisodeMedia)?.episode + if (item != null) playerDetailsFragment?.setItem(item) + updateUi() + playerUI?.updateUi(currentMedia) // TODO: disable for now // if (!includingChapters) loadMediaInfo(true) - }.invokeOnCompletion { throwable -> - if (throwable!= null) { - Log.e(TAG, Log.getStackTraceString(throwable)) + }.invokeOnCompletion { throwable -> + if (throwable != null) { + Log.e(TAG, Log.getStackTraceString(throwable)) + } + loadItemsRunning = false } } } @@ -310,8 +314,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar // } // } - private fun onEvenStartPlay(event: FlowEvent.PlayEvent) { - Logd(TAG, "onEvenStartPlay ${event.episode.title}") + private fun onPlayEvent(event: FlowEvent.PlayEvent) { + Logd(TAG, "onPlayEvent ${event.episode.title}") val currentitem = event.episode if (currentMedia?.getIdentifier() == null || currentitem.media?.getIdentifier() != currentMedia?.getIdentifier()) { currentMedia = currentitem.media @@ -338,7 +342,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar (activity as MainActivity).bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED playerUI?.onPlaybackServiceChanged(event) } - is FlowEvent.PlayEvent -> onEvenStartPlay(event) + is FlowEvent.PlayEvent -> onPlayEvent(event) is FlowEvent.FavoritesEvent -> onFavoriteEvent(event) is FlowEvent.PlayerErrorEvent -> MediaPlayerErrorDialog.show(activity as Activity, event) is FlowEvent.SleepTimerUpdatedEvent -> if (event.isCancelled || event.wasJustEnabled()) loadMediaInfo(false) @@ -375,8 +379,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar // updateUi(controller!!.getMedia) // sbPosition.highlightCurrentChapter() // } - binding.txtvSeek.text = curMedia?.getChapters()?.get(newChapterIndex)?.title ?: ("\n${Converter.getDurationStringLong(position)}") - } else binding.txtvSeek.text = Converter.getDurationStringLong(position) + binding.txtvSeek.text = curMedia?.getChapters()?.get(newChapterIndex)?.title ?: ("\n${DurationConverter.getDurationStringLong(position)}") + } else binding.txtvSeek.text = DurationConverter.getDurationStringLong(position) } duration != playbackService?.curDuration -> updateUi() } @@ -628,18 +632,18 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar Log.w(TAG, "Could not react to position observer update because of invalid time") return } - binding.txtvPosition.text = Converter.getDurationStringLong(currentPosition) + binding.txtvPosition.text = DurationConverter.getDurationStringLong(currentPosition) binding.txtvPosition.setContentDescription(getString(R.string.position, - Converter.getDurationStringLocalized(requireContext(), currentPosition.toLong()))) + DurationConverter.getDurationStringLocalized(requireContext(), currentPosition.toLong()))) val showTimeLeft = UserPreferences.shouldShowRemainingTime() if (showTimeLeft) { txtvLength.setContentDescription(getString(R.string.remaining_time, - Converter.getDurationStringLocalized(requireContext(), remainingTime.toLong()))) - txtvLength.text = (if (remainingTime > 0) "-" else "") + Converter.getDurationStringLong(remainingTime) + DurationConverter.getDurationStringLocalized(requireContext(), remainingTime.toLong()))) + txtvLength.text = (if (remainingTime > 0) "-" else "") + DurationConverter.getDurationStringLong(remainingTime) } else { txtvLength.setContentDescription(getString(R.string.chapter_duration, - Converter.getDurationStringLocalized(requireContext(), duration.toLong()))) - txtvLength.text = Converter.getDurationStringLong(duration) + DurationConverter.getDurationStringLocalized(requireContext(), duration.toLong()))) + txtvLength.text = DurationConverter.getDurationStringLong(duration) } if (sbPosition.visibility == View.INVISIBLE && playbackService?.isServiceReady() == true) sbPosition.visibility = View.VISIBLE diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/ChaptersFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/ChaptersFragment.kt index 31ea7c35..6b14284b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/ChaptersFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/ChaptersFragment.kt @@ -15,10 +15,9 @@ import ac.mdiq.podcini.playback.PlaybackController.Companion.curPosition import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo import ac.mdiq.podcini.storage.model.Chapter import ac.mdiq.podcini.storage.model.EmbeddedChapterImage -import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion import ac.mdiq.podcini.ui.view.CircularProgressBar -import ac.mdiq.podcini.util.Converter.getDurationStringLocalized -import ac.mdiq.podcini.util.Converter.getDurationStringLong +import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLocalized +import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong import ac.mdiq.podcini.util.IntentUtils.openInBrowser import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.event.EventFlow diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadLogFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadLogFragment.kt index e85ae73f..e891ffee 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadLogFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadLogFragment.kt @@ -15,14 +15,13 @@ import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.ui.actions.actionbutton.DownloadActionButton import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.dialog.DownloadLogDetailsDialog -import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion import ac.mdiq.podcini.ui.utils.EmptyViewHandler import ac.mdiq.podcini.ui.utils.ThemeUtils import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.error.DownloadErrorLabel import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.FlowEvent -import ac.mdiq.podcini.util.sorting.DownloadResultComparator +import ac.mdiq.podcini.storage.utils.DownloadResultComparator import android.app.Activity import android.content.Context import android.os.Bundle diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt index 2309a6ab..bdaac9db 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt @@ -6,7 +6,7 @@ import ac.mdiq.podcini.databinding.SimpleListFragmentBinding import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.feed.FeedUpdateManager import ac.mdiq.podcini.playback.base.InTheatre.isCurMedia -import ac.mdiq.podcini.preferences.UserPreferences.PREF_DOWNLOADS_SORTED_ORDER +import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.storage.database.Episodes.getEpisodes import ac.mdiq.podcini.storage.database.RealmDB.realm @@ -100,7 +100,7 @@ import java.util.* swipeActions = SwipeActions(this, TAG).attachTo(recyclerView) lifecycle.addObserver(swipeActions) - swipeActions.setFilter(EpisodeFilter(EpisodeFilter.DOWNLOADED)) + swipeActions.setFilter(EpisodeFilter(EpisodeFilter.States.downloaded.name)) refreshSwipeTelltale() binding.leftActionIcon.setOnClickListener { swipeActions.showDialog() @@ -322,35 +322,41 @@ import java.util.* if (swipeActions.actions?.right != null) binding.rightActionIcon.setImageResource(swipeActions.actions!!.right!!.getActionIcon()) } + private var loadItemsRunning = false private fun loadItems() { emptyView.hide() - lifecycleScope.launch { - try { - withContext(Dispatchers.IO) { - val sortOrder: EpisodeSortOrder? = downloadsSortedOrder - val downloadedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.DOWNLOADED), sortOrder) - if (runningDownloads.isEmpty()) episodes = downloadedItems.toMutableList() - else { - val mediaUrls: MutableList = ArrayList() - for (url in runningDownloads) { - if (EpisodeUtil.indexOfItemWithDownloadUrl(downloadedItems, url) != -1) continue - mediaUrls.add(url) + if (!loadItemsRunning) { + loadItemsRunning = true + lifecycleScope.launch { + try { + withContext(Dispatchers.IO) { + val sortOrder: EpisodeSortOrder? = downloadsSortedOrder + val downloadedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.downloaded.name), sortOrder) + if (runningDownloads.isEmpty()) episodes = downloadedItems.toMutableList() + else { + val mediaUrls: MutableList = ArrayList() + for (url in runningDownloads) { + if (EpisodeUtil.indexOfItemWithDownloadUrl(downloadedItems, url) != -1) continue + mediaUrls.add(url) + } + val currentDownloads = getEpisdesWithUrl(mediaUrls).toMutableList() + currentDownloads.addAll(downloadedItems) + episodes = currentDownloads } - val currentDownloads = getEpisdesWithUrl(mediaUrls).toMutableList() - currentDownloads.addAll(downloadedItems) - episodes = currentDownloads } - } - withContext(Dispatchers.Main) { + withContext(Dispatchers.Main) { // adapter.setDummyViews(0) - binding.progLoading.visibility = View.GONE - adapter.updateItems(episodes) - refreshInfoBar() - } - } catch (e: Throwable) { + binding.progLoading.visibility = View.GONE + adapter.updateItems(episodes) + refreshInfoBar() + } + } catch (e: Throwable) { // adapter.setDummyViews(0) - adapter.updateItems(emptyList()) - Log.e(TAG, Log.getStackTraceString(e)) + adapter.updateItems(emptyList()) + Log.e(TAG, Log.getStackTraceString(e)) + } finally { + loadItemsRunning = false + } } } } @@ -416,10 +422,14 @@ import java.util.* } override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) { - if (ascending == EpisodeSortOrder.DATE_OLD_NEW || ascending == EpisodeSortOrder.PLAYED_DATE_OLD_NEW + if (ascending == EpisodeSortOrder.DATE_OLD_NEW + || ascending == EpisodeSortOrder.PLAYED_DATE_OLD_NEW || ascending == EpisodeSortOrder.COMPLETED_DATE_OLD_NEW - || ascending == EpisodeSortOrder.DURATION_SHORT_LONG || ascending == EpisodeSortOrder.EPISODE_TITLE_A_Z - || ascending == EpisodeSortOrder.SIZE_SMALL_LARGE || ascending == EpisodeSortOrder.FEED_TITLE_A_Z) { + || ascending == EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW + || ascending == EpisodeSortOrder.DURATION_SHORT_LONG + || ascending == EpisodeSortOrder.EPISODE_TITLE_A_Z + || ascending == EpisodeSortOrder.SIZE_SMALL_LARGE + || ascending == EpisodeSortOrder.FEED_TITLE_A_Z) { super.onAddItem(title, ascending, descending, ascendingIsDefault) } } @@ -440,11 +450,11 @@ import java.util.* // the sort order for the downloads. var downloadsSortedOrder: EpisodeSortOrder? get() { - val sortOrderStr = appPrefs.getString(PREF_DOWNLOADS_SORTED_ORDER, "" + EpisodeSortOrder.DATE_NEW_OLD.code) + val sortOrderStr = appPrefs.getString(UserPreferences.Prefs.prefDownloadSortedOrder.name, "" + EpisodeSortOrder.DATE_NEW_OLD.code) return EpisodeSortOrder.fromCodeString(sortOrderStr) } set(sortOrder) { - appPrefs.edit().putString(PREF_DOWNLOADS_SORTED_ORDER, "" + sortOrder!!.code).apply() + appPrefs.edit().putString(UserPreferences.Prefs.prefDownloadSortedOrder.name, "" + sortOrder!!.code).apply() } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeHomeFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeHomeFragment.kt index 8fc919db..2d0af608 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeHomeFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeHomeFragment.kt @@ -53,10 +53,8 @@ class EpisodeHomeFragment : Fragment() { @UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { super.onCreateView(inflater, container, savedInstanceState) - _binding = EpisodeHomeFragmentBinding.inflate(inflater, container, false) Logd(TAG, "fragment onCreateView") - toolbar = binding.toolbar toolbar.title = "" toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() } @@ -67,7 +65,6 @@ class EpisodeHomeFragment : Fragment() { Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show() parentFragmentManager.popBackStack() } - binding.webView.apply { webViewClient = object : WebViewClient() { override fun onPageFinished(view: WebView?, url: String?) { @@ -76,7 +73,6 @@ class EpisodeHomeFragment : Fragment() { } } } - updateAppearance() return binding.root } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt index 7749421b..3f16d3db 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt @@ -19,11 +19,10 @@ import ac.mdiq.podcini.storage.model.MediaType import ac.mdiq.podcini.ui.actions.actionbutton.* import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler import ac.mdiq.podcini.ui.activity.MainActivity -import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion import ac.mdiq.podcini.ui.utils.ShownotesCleaner import ac.mdiq.podcini.ui.utils.ThemeUtils import ac.mdiq.podcini.ui.view.ShownotesWebView -import ac.mdiq.podcini.util.Converter +import ac.mdiq.podcini.storage.utils.DurationConverter import ac.mdiq.podcini.util.DateFormatter import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.event.EventFlow @@ -161,7 +160,8 @@ import kotlin.math.max cancelFlowEvents() } - @OptIn(UnstableApi::class) private fun showOnDemandConfigBalloon(offerStreaming: Boolean) { + @OptIn(UnstableApi::class) + private fun showOnDemandConfigBalloon(offerStreaming: Boolean) { val isLocaleRtl = (TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL) val balloon: Balloon = Balloon.Builder(requireContext()) .setArrowOrientation(ArrowOrientation.TOP) @@ -334,8 +334,8 @@ import kotlin.math.max } else { binding.noMediaLabel.visibility = View.GONE if (media.getDuration() > 0) { - binding.txtvDuration.text = Converter.getDurationStringLong(media.getDuration()) - binding.txtvDuration.setContentDescription(Converter.getDurationStringLocalized(requireContext(), media.getDuration().toLong())) + binding.txtvDuration.text = DurationConverter.getDurationStringLong(media.getDuration()) + binding.txtvDuration.setContentDescription(DurationConverter.getDurationStringLocalized(requireContext(), media.getDuration().toLong())) } if (episode != null) { actionButton1 = when { @@ -449,25 +449,30 @@ import kotlin.math.max if (itemLoaded && activity != null) updateButtons() } + private var loadItemsRunning = false @UnstableApi private fun load() { if (!itemLoaded) binding.progbarLoading.visibility = View.VISIBLE - Logd(TAG, "load() called") - lifecycleScope.launch { - try { - withContext(Dispatchers.IO) { - if (episode != null) { - val duration = episode!!.media?.getDuration()?: Int.MAX_VALUE - webviewData = shownotesCleaner.processShownotes(episode!!.description?:"", duration) + if (!loadItemsRunning) { + loadItemsRunning = true + lifecycleScope.launch { + try { + withContext(Dispatchers.IO) { + if (episode != null) { + val duration = episode!!.media?.getDuration() ?: Int.MAX_VALUE + webviewData = shownotesCleaner.processShownotes(episode!!.description ?: "", duration) + } } + withContext(Dispatchers.Main) { + binding.progbarLoading.visibility = View.GONE + onFragmentLoaded() + itemLoaded = true + } + } catch (e: Throwable) { + Log.e(TAG, Log.getStackTraceString(e)) + } finally { + loadItemsRunning = false } - withContext(Dispatchers.Main) { - binding.progbarLoading.visibility = View.GONE - onFragmentLoaded() - itemLoaded = true - } - } catch (e: Throwable) { - Log.e(TAG, Log.getStackTraceString(e)) } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt index afa81718..cfa744d6 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt @@ -30,7 +30,6 @@ import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.adapter.EpisodesAdapter import ac.mdiq.podcini.ui.adapter.SelectableAdapter import ac.mdiq.podcini.ui.dialog.* -import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion import ac.mdiq.podcini.ui.utils.ToolbarIconTintManager import ac.mdiq.podcini.ui.utils.TransitionEffect import ac.mdiq.podcini.ui.view.viewholder.EpisodeViewHolder @@ -39,7 +38,7 @@ import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.ShareUtils import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.FlowEvent -import ac.mdiq.podcini.util.sorting.EpisodesPermutors.getPermutor +import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor import android.app.Activity import android.content.Context import android.content.res.Configuration @@ -341,7 +340,7 @@ import java.util.concurrent.Semaphore if (item.feedId != feed!!.id) continue val pos: Int = EpisodeUtil.indexOfItemWithId(episodes, item.id) if (pos >= 0) { - Logd(TAG, "episode event: ${item.title} ${item.playState}") + Logd(TAG, "episode event: ${item.title} ${item.playState} ${item.isDownloaded}") episodes[pos] = item adapter.notifyItemChangedCompat(pos) } @@ -358,7 +357,7 @@ import java.util.concurrent.Semaphore if (item.feedId != feed!!.id) continue val pos: Int = EpisodeUtil.indexOfItemWithId(episodes, item.id) if (pos >= 0) { - episodes[pos] = item +// episodes[pos] = item adapter.notifyItemChangedCompat(pos) // episodes[pos].playState = item.playState // adapter.notifyItemChangedCompat(pos) @@ -367,8 +366,8 @@ import java.util.concurrent.Semaphore } } - private fun onEvenStartPlay(event: FlowEvent.PlayEvent) { - Logd(TAG, "onEvenStartPlay ${event.episode.title}") + private fun onPlayEvent(event: FlowEvent.PlayEvent) { + Logd(TAG, "onPlayEvent ${event.episode.title}") if (feed != null) { val pos: Int = EpisodeUtil.indexOfItemWithId(episodes, event.episode.id) if (pos >= 0) adapter.notifyItemChangedCompat(pos) @@ -381,8 +380,13 @@ import java.util.concurrent.Semaphore val pos: Int = EpisodeUtil.indexOfItemWithId(episodes, item.id) if (pos >= 0) { Logd(TAG, "played item: ${item.title} ${item.playState}") - episodes[pos] = item - adapter.notifyItemChangedCompat(pos) + if (enableFilter && ((feed!!.episodeFilter.showUnplayed && item.isPlayed()) || feed!!.episodeFilter.showPlayed && !item.isPlayed())) { + episodes.removeAt(pos) + adapter.updateItems(episodes) + } else { + episodes[pos] = item + adapter.notifyItemChangedCompat(pos) + } // episodes[pos].playState = item.playState // adapter.notifyItemChangedCompat(pos) } @@ -412,6 +416,7 @@ import java.util.concurrent.Semaphore private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) { val item = (event.media as? EpisodeMedia)?.episode ?: return + if (loadItemsRunning) return val pos = if (curIndex in 0.. onQueueEvent(event) is FlowEvent.FavoritesEvent -> onFavoriteEvent(event) - is FlowEvent.PlayEvent -> onEvenStartPlay(event) + is FlowEvent.PlayEvent -> onPlayEvent(event) is FlowEvent.PlaybackPositionEvent -> onPlaybackPositionEvent(event) is FlowEvent.FeedPrefsChangeEvent -> if (feed?.id == event.feed.id) loadItems() is FlowEvent.EpisodeEvent -> onEpisodeEvent(event) @@ -553,6 +558,8 @@ import java.util.concurrent.Semaphore binding.header.butFilter.setOnLongClickListener { if (feed != null) { enableFilter = !enableFilter + waitForLoading() + loadItemsRunning = true episodes.clear() if (enableFilter) { binding.header.butFilter.setColorFilter(Color.WHITE) @@ -566,6 +573,7 @@ import java.util.concurrent.Semaphore if (sortOrder != null) getPermutor(sortOrder).reorder(episodes) binding.header.counts.text = episodes.size.toString() adapter.updateItems(episodes, feed) + loadItemsRunning = false } true } @@ -605,64 +613,73 @@ import java.util.concurrent.Semaphore } } - @UnstableApi private fun loadItems() { - lifecycleScope.launch { - try { - feed = withContext(Dispatchers.IO) { - val feed_ = getFeed(feedID, fromDB = true) - if (feed_ != null) { - episodes.clear() - if (enableFilter && !feed_.preferences?.filterString.isNullOrEmpty()) { - val episodes_ = feed_.episodes.filter { feed_.episodeFilter.matches(it) } - episodes.addAll(episodes_) - } else episodes.addAll(feed_.episodes) - val sortOrder = fromCode(feed_.preferences?.sortOrderCode?:0) - if (sortOrder != null) getPermutor(sortOrder).reorder(episodes) -// episodes.forEach { -// Logd(TAG, "Episode: ${it.title} ${it.media?.downloaded} ${it.media?.fileUrl}") -// } - if (onInit) { - var hasNonMediaItems = false - for (item in episodes) { - if (item.media == null) { - hasNonMediaItems = true - break + private var loadItemsRunning = false + private fun waitForLoading() { + while (loadItemsRunning) Thread.sleep(50) + } + + @UnstableApi + private fun loadItems() { + if (!loadItemsRunning) { + loadItemsRunning = true + lifecycleScope.launch { + try { + feed = withContext(Dispatchers.IO) { + val feed_ = getFeed(feedID, fromDB = true) + if (feed_ != null) { + Logd(TAG, "loadItems feed_.episodes.size: ${feed_.episodes.size}") + episodes.clear() + if (enableFilter && !feed_.preferences?.filterString.isNullOrEmpty()) { + val episodes_ = feed_.episodes.filter { feed_.episodeFilter.matches(it) } + episodes.addAll(episodes_) + } else episodes.addAll(feed_.episodes) + val sortOrder = fromCode(feed_.preferences?.sortOrderCode ?: 0) + if (sortOrder != null) getPermutor(sortOrder).reorder(episodes) + if (onInit) { + var hasNonMediaItems = false + for (item in episodes) { + if (item.media == null) { + hasNonMediaItems = true + break + } } - } - if (hasNonMediaItems) { - ioScope.launch { - withContext(Dispatchers.IO) { - if (!ttsReady) { - initializeTTS(requireContext()) - semaphore.acquire() + if (hasNonMediaItems) { + ioScope.launch { + withContext(Dispatchers.IO) { + if (!ttsReady) { + initializeTTS(requireContext()) + semaphore.acquire() + } } } } + onInit = false } - onInit = false } + feed_ } - feed_ - } - withContext(Dispatchers.Main) { - Logd(TAG, "loadItems subscribe called ${feed?.title}") - swipeActions.setFilter(feed?.episodeFilter) - refreshHeaderView() - binding.progressBar.visibility = View.GONE + withContext(Dispatchers.Main) { + Logd(TAG, "loadItems subscribe called ${feed?.title}") + swipeActions.setFilter(feed?.episodeFilter) + refreshHeaderView() + binding.progressBar.visibility = View.GONE // adapter.setDummyViews(0) - if (feed != null) { - adapter.updateItems(episodes, feed) - binding.header.counts.text = episodes.size.toString() + if (feed != null) { + adapter.updateItems(episodes, feed) + binding.header.counts.text = episodes.size.toString() + } + updateToolbar() } + } catch (e: Throwable) { + feed = null + refreshHeaderView() +// adapter.setDummyViews(0) + adapter.updateItems(emptyList()) updateToolbar() + Log.e(TAG, Log.getStackTraceString(e)) + } finally { + loadItemsRunning = false } - } catch (e: Throwable) { - feed = null - refreshHeaderView() -// adapter.setDummyViews(0) - adapter.updateItems(emptyList()) - updateToolbar() - Log.e(TAG, Log.getStackTraceString(e)) } } } @@ -710,8 +727,12 @@ import java.util.concurrent.Semaphore sortOrder = feed?.sortOrder ?: EpisodeSortOrder.DATE_NEW_OLD } override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) { - if (ascending == EpisodeSortOrder.DATE_OLD_NEW || ascending == EpisodeSortOrder.PLAYED_DATE_OLD_NEW || ascending == EpisodeSortOrder.COMPLETED_DATE_OLD_NEW - || ascending == EpisodeSortOrder.DURATION_SHORT_LONG || ascending == EpisodeSortOrder.RANDOM + if (ascending == EpisodeSortOrder.DATE_OLD_NEW + || ascending == EpisodeSortOrder.PLAYED_DATE_OLD_NEW + || ascending == EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW + || ascending == EpisodeSortOrder.COMPLETED_DATE_OLD_NEW + || ascending == EpisodeSortOrder.DURATION_SHORT_LONG + || ascending == EpisodeSortOrder.RANDOM || ascending == EpisodeSortOrder.EPISODE_TITLE_A_Z || (feed?.isLocalFeed == true && ascending == EpisodeSortOrder.EPISODE_FILENAME_A_Z)) { super.onAddItem(title, ascending, descending, ascendingIsDefault) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt index c75efea2..6c01a3fb 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt @@ -10,14 +10,13 @@ import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences import ac.mdiq.podcini.storage.database.RealmDB.unmanaged import ac.mdiq.podcini.storage.model.Feed +import ac.mdiq.podcini.storage.model.FeedAutoDownloadFilter import ac.mdiq.podcini.storage.model.FeedPreferences import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction -import ac.mdiq.podcini.storage.model.FeedAutoDownloadFilter import ac.mdiq.podcini.storage.model.VolumeAdaptionSetting import ac.mdiq.podcini.ui.adapter.SimpleChipAdapter import ac.mdiq.podcini.ui.dialog.AuthenticationDialog import ac.mdiq.podcini.ui.dialog.TagSettingsDialog -import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion import ac.mdiq.podcini.ui.utils.ItemOffsetDecoration import ac.mdiq.podcini.util.Logd import android.content.Context @@ -78,7 +77,6 @@ class FeedSettingsFragment : Fragment() { private var notificationPermissionDenied: Boolean = false private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> if (isGranted) return@registerForActivityResult - if (notificationPermissionDenied) { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) val uri = Uri.fromParts("package", requireContext().packageName, null) @@ -89,7 +87,6 @@ class FeedSettingsFragment : Fragment() { Toast.makeText(context, R.string.notification_permission_denied, Toast.LENGTH_LONG).show() notificationPermissionDenied = true } - override fun onCreateRecyclerView(inflater: LayoutInflater, parent: ViewGroup, state: Bundle?): RecyclerView { val view = super.onCreateRecyclerView(inflater, parent, state) // To prevent transition animation because of summary update @@ -100,7 +97,7 @@ class FeedSettingsFragment : Fragment() { @OptIn(UnstableApi::class) override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.feed_settings) // To prevent displaying partially loaded data - findPreference(PREF_SCREEN)!!.isVisible = false + findPreference(Prefs.feedSettingsScreen.name)!!.isVisible = false if (feed != null) { if (feed!!.preferences == null) { feed!!.preferences = FeedPreferences(feed!!.id, false, AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, "", "") @@ -113,6 +110,10 @@ class FeedSettingsFragment : Fragment() { setupAutoDeletePreference() setupVolumeAdaptationPreferences() setupAuthentificationPreference() + updateAutoDownloadPolicy() + setupAutoDownloadPolicy() + updateAutoDownloadCacheSize() + setupAutoDownloadCacheSize() setupAutoDownloadFilterPreference() setupPlaybackSpeedPreference() setupFeedAutoSkipPreference() @@ -121,14 +122,19 @@ class FeedSettingsFragment : Fragment() { updateVolumeAdaptationValue() updateAutoDownloadEnabled() if (feed!!.isLocalFeed) { - findPreference(PREF_AUTHENTICATION)!!.isVisible = false - findPreference(PREF_CATEGORY_AUTO_DOWNLOAD)!!.isVisible = false + findPreference(Prefs.authentication.name)!!.isVisible = false + findPreference(Prefs.autoDownloadCategory.name)!!.isVisible = false } - findPreference(PREF_SCREEN)!!.isVisible = true + findPreference(Prefs.feedSettingsScreen.name)!!.isVisible = true } } + override fun onDestroyView() { + Logd(TAG, "onDestroyView") + feed = null + super.onDestroyView() + } private fun setupFeedAutoSkipPreference() { - findPreference(PREF_AUTO_SKIP)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.feedAutoSkip.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { if (feedPrefs != null) { object : FeedPreferenceSkipDialog(requireContext(), feedPrefs!!.introSkip, feedPrefs!!.endingSkip) { @UnstableApi @@ -143,7 +149,7 @@ class FeedSettingsFragment : Fragment() { } } @UnstableApi private fun setupPlaybackSpeedPreference() { - val feedPlaybackSpeedPreference = findPreference(PREF_FEED_PLAYBACK_SPEED) + val feedPlaybackSpeedPreference = findPreference(Prefs.feedPlaybackSpeed.name) feedPlaybackSpeedPreference!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { val binding = PlaybackSpeedFeedSettingDialogBinding.inflate(layoutInflater) binding.seekBar.setProgressChangedListener { speed: Float? -> @@ -175,10 +181,60 @@ class FeedSettingsFragment : Fragment() { true } } + @UnstableApi private fun setupAutoDownloadPolicy() { + val policyPref = findPreference(Prefs.feedAutoDownloadPolicy.name) + policyPref!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? -> + if (feedPrefs != null) { + feedPrefs!!.autoDLPolicyCode = newValue.toString().toInt() + persistFeedPreferences(feed!!) + updateAutoDownloadPolicy() + } + false + } + } + private fun updateAutoDownloadPolicy() { + if (feedPrefs == null) return + val policyPref = findPreference(Prefs.feedAutoDownloadPolicy.name) + when (feedPrefs!!.autoDLPolicy) { + FeedPreferences.AutoDLPolicy.ONLY_NEW -> policyPref!!.value = FeedPreferences.AutoDLPolicy.ONLY_NEW.ordinal.toString() + FeedPreferences.AutoDLPolicy.NEWER -> policyPref!!.value = FeedPreferences.AutoDLPolicy.NEWER.ordinal.toString() + FeedPreferences.AutoDLPolicy.OLDER -> policyPref!!.value = FeedPreferences.AutoDLPolicy.OLDER.ordinal.toString() + } + } + @UnstableApi private fun setupAutoDownloadCacheSize() { + val cachePref = findPreference(Prefs.feedEpisodeCacheSize.name) + cachePref!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? -> + if (feedPrefs != null) { + feedPrefs!!.autoDLMaxEpisodes = newValue.toString().toInt() + persistFeedPreferences(feed!!) + updateAutoDownloadCacheSize() + } + false + } + } + private fun updateAutoDownloadCacheSize() { + if (feedPrefs == null) return + val cachePref = findPreference(Prefs.feedEpisodeCacheSize.name) + cachePref!!.value = feedPrefs!!.autoDLMaxEpisodes.toString() + } private fun setupAutoDownloadFilterPreference() { - findPreference(PREF_EPISODE_FILTER)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.episodeInclusiveFilter.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { if (feedPrefs != null) { - object : AutoDownloadFilterPrefDialog(requireContext(), feedPrefs!!.autoDownloadFilter) { + object : AutoDownloadFilterPrefDialog(requireContext(), feedPrefs!!.autoDownloadFilter!!, 1) { + @UnstableApi + override fun onConfirmed(filter: FeedAutoDownloadFilter) { + if (feedPrefs != null) { + feedPrefs!!.autoDownloadFilter = filter + persistFeedPreferences(feed!!) + } + } + }.show() + } + false + } + findPreference(Prefs.episodeExclusiveFilter.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + if (feedPrefs != null) { + object : AutoDownloadFilterPrefDialog(requireContext(), feedPrefs!!.autoDownloadFilter!!, -1) { @UnstableApi override fun onConfirmed(filter: FeedAutoDownloadFilter) { if (feedPrefs != null) { @@ -192,7 +248,7 @@ class FeedSettingsFragment : Fragment() { } } private fun setupAuthentificationPreference() { - findPreference(PREF_AUTHENTICATION)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.authentication.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { if (feedPrefs != null) { object : AuthenticationDialog(requireContext(), R.string.authentication_label, true, feedPrefs!!.username, feedPrefs!!.password) { @UnstableApi @@ -210,7 +266,7 @@ class FeedSettingsFragment : Fragment() { } } @UnstableApi private fun setupAutoDeletePreference() { - findPreference(PREF_AUTO_DELETE)!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? -> + findPreference(Prefs.autoDelete.name)!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any? -> if (feedPrefs != null) { when (newValue as? String) { "global" -> feedPrefs!!.autoDeleteAction = AutoDeleteAction.GLOBAL @@ -226,7 +282,7 @@ class FeedSettingsFragment : Fragment() { } private fun updateAutoDeleteSummary() { if (feedPrefs == null) return - val autoDeletePreference = findPreference(PREF_AUTO_DELETE) + val autoDeletePreference = findPreference(Prefs.autoDelete.name) when (feedPrefs!!.autoDeleteAction) { AutoDeleteAction.GLOBAL -> { autoDeletePreference!!.setSummary(R.string.global_default) @@ -289,16 +345,19 @@ class FeedSettingsFragment : Fragment() { } private fun setupAutoDownloadGlobalPreference() { if (!isEnableAutodownload) { - val autodl = findPreference("autoDownload") + val autodl = findPreference(Prefs.autoDownload.name) autodl!!.isChecked = false autodl.isEnabled = false autodl.setSummary(R.string.auto_download_disabled_globally) - findPreference(PREF_EPISODE_FILTER)!!.isEnabled = false + findPreference(Prefs.feedAutoDownloadPolicy.name)!!.isEnabled = false + findPreference(Prefs.episodeInclusiveFilter.name)!!.isEnabled = false + findPreference(Prefs.episodeExclusiveFilter.name)!!.isEnabled = false + findPreference(Prefs.feedEpisodeCacheSize.name)!!.isEnabled = false } } @OptIn(UnstableApi::class) private fun setupAutoDownloadPreference() { if (feedPrefs == null) return - val pref = findPreference("autoDownload") + val pref = findPreference(Prefs.autoDownload.name) pref!!.isEnabled = isEnableAutodownload if (isEnableAutodownload) pref.isChecked = feedPrefs!!.autoDownload else { @@ -319,11 +378,14 @@ class FeedSettingsFragment : Fragment() { private fun updateAutoDownloadEnabled() { if (feed?.preferences != null) { val enabled = feed!!.preferences!!.autoDownload && isEnableAutodownload - findPreference(PREF_EPISODE_FILTER)!!.isEnabled = enabled + findPreference(Prefs.feedAutoDownloadPolicy.name)!!.isEnabled = enabled + findPreference(Prefs.episodeInclusiveFilter.name)!!.isEnabled = enabled + findPreference(Prefs.episodeExclusiveFilter.name)!!.isEnabled = enabled + findPreference(Prefs.feedEpisodeCacheSize.name)!!.isEnabled = enabled } } private fun setupTags() { - findPreference(PREF_TAGS)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findPreference(Prefs.tags.name)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { if (feedPrefs != null) TagSettingsDialog.newInstance(listOf(feed!!)).show(childFragmentManager, TagSettingsDialog.TAG) true } @@ -332,16 +394,22 @@ class FeedSettingsFragment : Fragment() { feed = feed_ } - companion object { - private val PREF_EPISODE_FILTER: CharSequence = "episodeFilter" - private val PREF_SCREEN: CharSequence = "feedSettingsScreen" - private val PREF_AUTHENTICATION: CharSequence = "authentication" - private val PREF_AUTO_DELETE: CharSequence = "autoDelete" - private val PREF_CATEGORY_AUTO_DOWNLOAD: CharSequence = "autoDownloadCategory" - private const val PREF_FEED_PLAYBACK_SPEED = "feedPlaybackSpeed" - private const val PREF_AUTO_SKIP = "feedAutoSkip" - private const val PREF_TAGS = "tags" + private enum class Prefs { + feedSettingsScreen, + authentication, + autoDelete, + feedPlaybackSpeed, + feedAutoSkip, + tags, + autoDownloadCategory, + autoDownload, + episodeInclusiveFilter, + episodeExclusiveFilter, + feedEpisodeCacheSize, + feedAutoDownloadPolicy + } + companion object { fun newInstance(feed: Feed): FeedSettingsPreferenceFragment { val fragment = FeedSettingsPreferenceFragment() fragment.setFeed(feed) @@ -375,27 +443,37 @@ class FeedSettingsFragment : Fragment() { /** * Displays a dialog with a text box for filtering episodes and two radio buttons for exclusion/inclusion */ - abstract class AutoDownloadFilterPrefDialog(context: Context, filter: FeedAutoDownloadFilter) : MaterialAlertDialogBuilder(context) { + abstract class AutoDownloadFilterPrefDialog(context: Context, val filter: FeedAutoDownloadFilter, val inexcl: Int) : MaterialAlertDialogBuilder(context) { private val binding = AutodownloadFilterDialogBinding.inflate(LayoutInflater.from(context)) - private val termList: MutableList + private var termList: MutableList = mutableListOf() init { setTitle(R.string.episode_filters_label) setView(binding.root) - binding.durationCheckBox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> - binding.episodeFilterDurationText.isEnabled = isChecked - } - if (filter.hasMinimalDurationFilter()) { - binding.durationCheckBox.isChecked = true - // Store minimal duration in seconds, show in minutes - binding.episodeFilterDurationText.setText((filter.minimalDurationFilter / 60).toString()) - } else binding.episodeFilterDurationText.isEnabled = false - if (filter.excludeOnly()) { - termList = filter.getExcludeFilter().toMutableList() - binding.excludeRadio.isChecked = true + if (inexcl == -1) { +// exclusive + binding.durationCheckBox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> + binding.episodeFilterDurationText.isEnabled = isChecked + } + if (filter.hasMinimalDurationFilter()) { + binding.durationCheckBox.isChecked = true + // Store minimal duration in seconds, show in minutes + binding.episodeFilterDurationText.setText((filter.minimalDurationFilter / 60).toString()) + } else binding.episodeFilterDurationText.isEnabled = false + if (filter.hasExcludeFilter()) { + termList = filter.getExcludeFilter().toMutableList() + binding.excludeRadio.isChecked = true + } + binding.includeRadio.visibility = View.GONE } else { - termList = filter.getIncludeFilter().toMutableList() - binding.includeRadio.isChecked = true +// inclusive + binding.durationBlock.visibility = View.GONE + if (filter.hasIncludeFilter()) { + termList = filter.getIncludeFilter().toMutableList() + binding.includeRadio.isChecked = true + } + binding.excludeRadio.visibility = View.GONE + binding.markPlayedCheckBox.visibility = View.GONE } setupWordsList() setNegativeButton(R.string.cancel_label, null) @@ -426,20 +504,24 @@ class FeedSettingsFragment : Fragment() { } protected abstract fun onConfirmed(filter: FeedAutoDownloadFilter) private fun onConfirmClick(dialog: DialogInterface, which: Int) { - var minimalDuration = -1 - if (binding.durationCheckBox.isChecked) { - try { - // Store minimal duration in seconds - minimalDuration = binding.episodeFilterDurationText.text.toString().toInt() * 60 - } catch (e: NumberFormatException) { - // Do not change anything on error + if (inexcl == -1) { + var minimalDuration = -1 + if (binding.durationCheckBox.isChecked) { + try { + // Store minimal duration in seconds + minimalDuration = binding.episodeFilterDurationText.text.toString().toInt() * 60 + } catch (e: NumberFormatException) { + // Do not change anything on error + } } + var excludeFilter = "" + excludeFilter = toFilterString(termList) + onConfirmed(FeedAutoDownloadFilter(filter.includeFilterRaw, excludeFilter, minimalDuration, binding.markPlayedCheckBox.isChecked)) + } else { + var includeFilter = "" + includeFilter = toFilterString(termList) + onConfirmed(FeedAutoDownloadFilter(includeFilter, filter.excludeFilterRaw, filter.minimalDurationFilter, filter.markExcludedPlayed)) } - var excludeFilter = "" - var includeFilter = "" - if (binding.includeRadio.isChecked) includeFilter = toFilterString(termList) - else excludeFilter = toFilterString(termList) - onConfirmed(FeedAutoDownloadFilter(includeFilter, excludeFilter, minimalDuration)) } private fun toFilterString(words: List?): String { val result = StringBuilder() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt index f2092e57..c7b76879 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt @@ -12,13 +12,12 @@ import ac.mdiq.podcini.ui.adapter.EpisodesAdapter import ac.mdiq.podcini.ui.dialog.ConfirmationDialog import ac.mdiq.podcini.ui.dialog.DatesFilterDialog import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog -import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion import ac.mdiq.podcini.ui.view.viewholder.EpisodeViewHolder import ac.mdiq.podcini.util.DateFormatter import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.FlowEvent -import ac.mdiq.podcini.util.sorting.EpisodesPermutors.getPermutor +import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor import android.content.DialogInterface import android.os.Bundle import android.view.* @@ -168,8 +167,13 @@ import kotlin.math.min } } + private var loadItemsRunning = false override fun loadData(): List { - allHistory = getHistory(0, Int.MAX_VALUE, startDate, endDate, sortOrder).toMutableList() + if (!loadItemsRunning) { + loadItemsRunning = true + allHistory = getHistory(0, Int.MAX_VALUE, startDate, endDate, sortOrder).toMutableList() + loadItemsRunning = false + } if (allHistory.isEmpty()) return listOf() return allHistory.subList(0, min(allHistory.size-1, page * EPISODES_PER_PAGE)) } @@ -198,10 +202,14 @@ import kotlin.math.min class HistorySortDialog : EpisodeSortDialog() { override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) { - if (ascending == EpisodeSortOrder.DATE_OLD_NEW || ascending == EpisodeSortOrder.PLAYED_DATE_OLD_NEW + if (ascending == EpisodeSortOrder.DATE_OLD_NEW + || ascending == EpisodeSortOrder.PLAYED_DATE_OLD_NEW || ascending == EpisodeSortOrder.COMPLETED_DATE_OLD_NEW - || ascending == EpisodeSortOrder.DURATION_SHORT_LONG || ascending == EpisodeSortOrder.EPISODE_TITLE_A_Z - || ascending == EpisodeSortOrder.SIZE_SMALL_LARGE || ascending == EpisodeSortOrder.FEED_TITLE_A_Z) { + || ascending == EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW + || ascending == EpisodeSortOrder.DURATION_SHORT_LONG + || ascending == EpisodeSortOrder.EPISODE_TITLE_A_Z + || ascending == EpisodeSortOrder.SIZE_SMALL_LARGE + || ascending == EpisodeSortOrder.FEED_TITLE_A_Z) { super.onAddItem(title, ascending, descending, ascendingIsDefault) } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt index 4c23c28c..2f914a9a 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt @@ -11,7 +11,7 @@ import ac.mdiq.podcini.preferences.UserPreferences.episodeCacheSize import ac.mdiq.podcini.preferences.UserPreferences.hiddenDrawerItems import ac.mdiq.podcini.storage.algorithms.AutoCleanups import ac.mdiq.podcini.storage.database.Episodes.getEpisodesCount -import ac.mdiq.podcini.storage.database.Feeds.getFeedList +import ac.mdiq.podcini.storage.database.Feeds.getFeedCount import ac.mdiq.podcini.storage.model.DatasetStats import ac.mdiq.podcini.storage.model.EpisodeFilter import ac.mdiq.podcini.storage.model.EpisodeFilter.Companion.unfiltered @@ -19,7 +19,6 @@ import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.activity.PreferenceActivity import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter import ac.mdiq.podcini.ui.dialog.DrawerPreferencesDialog -import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion import ac.mdiq.podcini.ui.statistics.StatisticsFragment import ac.mdiq.podcini.ui.utils.ThemeUtils import ac.mdiq.podcini.util.Logd @@ -170,12 +169,12 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener { appPrefs.registerOnSharedPreferenceChangeListener(this@NavListAdapter) } override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { - if (UserPreferences.PREF_HIDDEN_DRAWER_ITEMS == key) loadItems() + if (UserPreferences.Prefs.prefHiddenDrawerItems.name == key) loadItems() } @OptIn(UnstableApi::class) private fun loadItems() { val newTags: MutableList = ArrayList(listOf(*NAV_DRAWER_TAGS)) val hiddenFragments = hiddenDrawerItems - newTags.removeAll(hiddenFragments) + newTags.removeAll(hiddenFragments.map { it.trim() }) fragmentTags.clear() fragmentTags.addAll(newTags) notifyDataSetChanged() @@ -368,6 +367,8 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener { const val PREF_NAME: String = "NavDrawerPrefs" var prefs: SharedPreferences? = null + var feedCount: Int = 0 + fun getSharedPrefs(context: Context) { if (prefs == null) prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) } @@ -404,13 +405,13 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener { */ fun getDatasetStats(): DatasetStats { Logd(TAG, "getNavDrawerData() called") - val numDownloadedItems = getEpisodesCount(EpisodeFilter(EpisodeFilter.DOWNLOADED)) + val numDownloadedItems = getEpisodesCount(EpisodeFilter(EpisodeFilter.States.downloaded.name)) val numItems = getEpisodesCount(unfiltered()) - val numFeeds = getFeedList().size + feedCount = getFeedCount() while (curQueue.name.isEmpty()) runBlocking { delay(100) } val queueSize = curQueue.episodeIds.size Logd(TAG, "getDatasetStats: queueSize: $queueSize") - return DatasetStats(queueSize, numDownloadedItems, AutoCleanups.build().getReclaimableItems(), numItems, numFeeds) + return DatasetStats(queueSize, numDownloadedItems, AutoCleanups.build().getReclaimableItems(), numItems, feedCount) } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedViewFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedViewFragment.kt index bc8545b9..80d7df59 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedViewFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedViewFragment.kt @@ -199,7 +199,7 @@ import kotlin.concurrent.Volatile var url: String? = null val searcher = CombinedSearcher() val query = "${error.trackName} ${error.artistName}" - val results = searcher.search1(query) + val results = searcher.search(query) if (results.isEmpty()) return@launch for (result in results) { if (result?.feedUrl != null && result.author != null && result.author.equals(error.artistName, diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineSearchFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineSearchFragment.kt index d92f5ea7..5b1c7ee2 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineSearchFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineSearchFragment.kt @@ -149,7 +149,7 @@ class OnlineSearchFragment : Fragment() { showOnlyProgressBar() lifecycleScope.launch(Dispatchers.IO) { try { - val result = searchProvider?.search1(query) + val result = searchProvider?.search(query) withContext(Dispatchers.Main) { searchResults = result progressBar.visibility = View.GONE diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt index 173178d1..6b58ac64 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt @@ -410,24 +410,6 @@ class PlayerDetailsFragment : Fragment() { savePreference() } -// private var eventSink: Job? = null -// private fun cancelFlowEvents() { -// eventSink?.cancel() -// eventSink = null -// } -// private fun procFlowEvents() { -// if (eventSink != null) return -// eventSink = lifecycleScope.launch { -// EventFlow.events.collectLatest { event -> -// Logd(TAG, "Received event: ${event.TAG}") -// when (event) { -// is FlowEvent.PlaybackPositionEvent -> onPlaybackPositionEvent(event) -// else -> {} -// } -// } -// } -// } - fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) { if (playable?.getIdentifier() != event.media?.getIdentifier()) return val newChapterIndex: Int = ChapterUtils.getCurrentChapterIndex(playable, event.position) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt index f35ccb22..cdbe9817 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueueFragment.kt @@ -37,11 +37,11 @@ import ac.mdiq.podcini.ui.utils.EmptyViewHandler import ac.mdiq.podcini.ui.utils.LiftOnScrollListener import ac.mdiq.podcini.ui.view.EpisodesRecyclerView import ac.mdiq.podcini.ui.view.viewholder.EpisodeViewHolder -import ac.mdiq.podcini.util.Converter +import ac.mdiq.podcini.storage.utils.DurationConverter import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.FlowEvent -import ac.mdiq.podcini.util.sorting.EpisodesPermutors.getPermutor +import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor import android.content.Context import android.content.DialogInterface import android.content.SharedPreferences @@ -125,7 +125,7 @@ import java.util.* swipeActions = QueueSwipeActions() lifecycle.addObserver(swipeActions) - swipeActions.setFilter(EpisodeFilter(EpisodeFilter.QUEUED)) + swipeActions.setFilter(EpisodeFilter(EpisodeFilter.States.queued.name)) swipeActions.attachTo(recyclerView) refreshSwipeTelltale() binding.leftActionIcon.setOnClickListener { swipeActions.showDialog() } @@ -530,28 +530,33 @@ import java.util.* } } info += " • " - info += Converter.getDurationStringLocalized(requireActivity(), timeLeft) + info += DurationConverter.getDurationStringLocalized(requireActivity(), timeLeft) } binding.infoBar.text = info toolbar.title = "${getString(R.string.queue_label)}: ${curQueue.name}" } + private var loadItemsRunning = false private fun loadItems(restoreScrollPosition: Boolean) { - Logd(TAG, "loadItems() called") - while (curQueue.name.isEmpty()) runBlocking { delay(100) } - curQueue.episodes.clear() - curQueue.episodes.addAll(realm.copyFromRealm(realm.query(Episode::class, "id IN $0", curQueue.episodeIds) - .find().sortedBy { curQueue.episodeIds.indexOf(it.id) })) - - if (queueItems.isEmpty()) emptyView.hide() - - queueItems.clear() - queueItems.addAll(curQueue.episodes) - binding.progressBar.visibility = View.GONE + if (!loadItemsRunning) { + loadItemsRunning = true + Logd(TAG, "loadItems() called") + while (curQueue.name.isEmpty()) runBlocking { delay(100) } + curQueue.episodes.clear() + curQueue.episodes.addAll(realm.copyFromRealm(realm.query(Episode::class, "id IN $0", curQueue.episodeIds) + .find().sortedBy { curQueue.episodeIds.indexOf(it.id) })) + + if (queueItems.isEmpty()) emptyView.hide() + + queueItems.clear() + queueItems.addAll(curQueue.episodes) + binding.progressBar.visibility = View.GONE // adapter?.setDummyViews(0) - adapter?.updateItems(queueItems) - if (restoreScrollPosition) recyclerView.restoreScrollPosition(TAG) - refreshInfoBar() + adapter?.updateItems(queueItems) + if (restoreScrollPosition) recyclerView.restoreScrollPosition(TAG) + refreshInfoBar() + loadItemsRunning = false + } } override fun onStartSelectMode() { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt index 155f26e7..3653b44e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt @@ -4,10 +4,6 @@ import ac.mdiq.podcini.R import ac.mdiq.podcini.databinding.* import ac.mdiq.podcini.net.feed.FeedUpdateManager import ac.mdiq.podcini.preferences.UserPreferences -import ac.mdiq.podcini.preferences.UserPreferences.FEED_ORDER_UNPLAYED -import ac.mdiq.podcini.preferences.UserPreferences.PREF_DRAWER_FEED_ORDER -import ac.mdiq.podcini.preferences.UserPreferences.PREF_DRAWER_FEED_ORDER_DIRECTION -import ac.mdiq.podcini.preferences.UserPreferences.PREF_FEED_GRID_LAYOUT import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.storage.database.Feeds.getFeedList import ac.mdiq.podcini.storage.database.Feeds.getTags @@ -16,11 +12,12 @@ import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.storage.model.FeedPreferences +import ac.mdiq.podcini.storage.model.FeedSortOrder import ac.mdiq.podcini.ui.actions.menuhandler.FeedMenuHandler import ac.mdiq.podcini.ui.actions.menuhandler.MenuItemUtils import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.adapter.SelectableAdapter -import ac.mdiq.podcini.ui.dialog.FeedSortDialogNew +import ac.mdiq.podcini.ui.dialog.FeedSortDialog import ac.mdiq.podcini.ui.dialog.RemoveFeedDialog import ac.mdiq.podcini.ui.dialog.TagSettingsDialog import ac.mdiq.podcini.ui.utils.CoverLoader @@ -90,8 +87,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec private val tags: MutableList = mutableListOf() private var useGrid: Boolean? = null - val useGridLayout: Boolean - get() = appPrefs.getBoolean(PREF_FEED_GRID_LAYOUT, false) + private val useGridLayout: Boolean + get() = appPrefs.getBoolean(UserPreferences.Prefs.prefFeedGridLayout.name, false) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -297,7 +294,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec val itemId = item.itemId when (itemId) { R.id.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance()) - R.id.subscriptions_sort -> FeedSortDialogNew().show(childFragmentManager, "FeedSortDialog") + R.id.subscriptions_sort -> FeedSortDialog().show(childFragmentManager, "FeedSortDialog") R.id.refresh_item -> FeedUpdateManager.runOnceOrAsk(requireContext()) else -> return false } @@ -312,30 +309,34 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec emptyView.attachToRecyclerView(recyclerView) } - @OptIn(UnstableApi::class) private fun loadSubscriptions() { + private var loadItemsRunning = false + @OptIn(UnstableApi::class) + private fun loadSubscriptions() { emptyView.hide() - lifecycleScope.launch { - try { - withContext(Dispatchers.IO) { - sortFeeds() - resetTags() - } - withContext(Dispatchers.Main) { - // We have fewer items. This can result in items being selected that are no longer visible. - if ( feedListFiltered.size > feedList.size) adapter.endSelectMode() - filterOnTag() - binding.progressBar.visibility = View.GONE - adapter.setItems(feedListFiltered) - binding.count.text = feedListFiltered.size.toString() + " / " + feedList.size.toString() - emptyView.updateVisibility() + if (!loadItemsRunning) { + loadItemsRunning = true + lifecycleScope.launch { + try { + withContext(Dispatchers.IO) { + sortFeeds() + resetTags() + } + withContext(Dispatchers.Main) { + // We have fewer items. This can result in items being selected that are no longer visible. + if (feedListFiltered.size > feedList.size) adapter.endSelectMode() + filterOnTag() + binding.progressBar.visibility = View.GONE + adapter.setItems(feedListFiltered) + binding.count.text = feedListFiltered.size.toString() + " / " + feedList.size.toString() + emptyView.updateVisibility() + } + } catch (e: Throwable) { + Log.e(TAG, Log.getStackTraceString(e)) + } finally { + loadItemsRunning = false } - } catch (e: Throwable) { - Log.e(TAG, Log.getStackTraceString(e)) } } - -// if (UserPreferences.subscriptionsFilter.isEnabled) feedsFilteredMsg.visibility = View.VISIBLE -// else feedsFilteredMsg.visibility = View.GONE } private fun sortFeeds() { @@ -343,12 +344,12 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec val feedOrder = feedOrderBy val dir = 1 - 2*feedOrderDir // get from 0, 1 to 1, -1 val comparator: Comparator = when (feedOrder) { - UserPreferences.FEED_ORDER_UNPLAYED -> { + FeedSortOrder.UNPLAYED_NEW_OLD.index -> { val episodes = realm.query(Episode::class).query("(playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED})").find() val counterMap = counterMap(episodes) comparator(counterMap, dir) } - UserPreferences.FEED_ORDER_ALPHABETICAL -> { + FeedSortOrder.ALPHABETIC_A_Z.index -> { Comparator { lhs: Feed, rhs: Feed -> val t1 = lhs.title val t2 = rhs.title @@ -359,12 +360,12 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec } } } - UserPreferences.FEED_ORDER_MOST_PLAYED -> { + FeedSortOrder.MOST_PLAYED.index -> { val episodes = realm.query(Episode::class).query("playState == ${Episode.PLAYED}").find() val counterMap = counterMap(episodes) comparator(counterMap, dir) } - UserPreferences.FEED_ORDER_LAST_UPDATED -> { + FeedSortOrder.LAST_UPDATED_NEW_OLD.index -> { val episodes = realm.query(Episode::class).sort("pubDate", Sort.DESCENDING).find() val counterMap: MutableMap = mutableMapOf() for (episode in episodes) { @@ -374,7 +375,17 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec } comparator(counterMap, dir) } - UserPreferences.FEED_ORDER_LAST_UNREAD_UPDATED -> { + FeedSortOrder.LAST_DOWNLOAD_NEW_OLD.index -> { + val episodes = realm.query(Episode::class).sort("media.downloadTime", Sort.DESCENDING).find() + val counterMap: MutableMap = mutableMapOf() + for (episode in episodes) { + val feedId = episode.feedId ?: continue + val pDownloadOld = counterMap[feedId] ?: 0 + if (pDownloadOld < (episode.media?.downloadTime?:0)) counterMap[feedId] = episode.media?.downloadTime ?: 0 + } + comparator(counterMap, dir) + } + FeedSortOrder.LAST_UPDATED_UNPLAYED_NEW_OLD.index -> { val episodes = realm.query(Episode::class) .query("playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED}").find() val counterMap: MutableMap = mutableMapOf() @@ -385,12 +396,12 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec } comparator(counterMap, dir) } - UserPreferences.FEED_ORDER_DOWNLOADED -> { + FeedSortOrder.MOST_DOWNLOADED.index -> { val episodes = realm.query(Episode::class).query("media.downloaded == true").find() val counterMap = counterMap(episodes) comparator(counterMap, dir) } - UserPreferences.FEED_ORDER_DOWNLOADED_UNPLAYED -> { + FeedSortOrder.MOST_DOWNLOADED_UNPLAYED.index -> { val episodes = realm.query(Episode::class) .query("(playState == ${Episode.NEW} OR playState == ${Episode.UNPLAYED}) AND media.downloaded == true").find() val counterMap = counterMap(episodes) @@ -884,13 +895,13 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec val feedOrderBy: Int get() { - val value = appPrefs.getString(PREF_DRAWER_FEED_ORDER, "" + FEED_ORDER_UNPLAYED) + val value = appPrefs.getString(UserPreferences.Prefs.prefDrawerFeedOrder.name, "" + FeedSortOrder.UNPLAYED_NEW_OLD.index) return value!!.toInt() } val feedOrderDir: Int get() { - val value = appPrefs.getInt(PREF_DRAWER_FEED_ORDER_DIRECTION, 0) + val value = appPrefs.getInt(UserPreferences.Prefs.prefDrawerFeedOrderDir.name, 0) return value } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/VideoEpisodeFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/VideoEpisodeFragment.kt index bb386a63..a66ace89 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/VideoEpisodeFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/VideoEpisodeFragment.kt @@ -23,13 +23,12 @@ import ac.mdiq.podcini.ui.activity.VideoplayerActivity import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog -import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion import ac.mdiq.podcini.ui.utils.PictureInPictureUtil import ac.mdiq.podcini.ui.utils.ShownotesCleaner import ac.mdiq.podcini.ui.view.ShownotesWebView -import ac.mdiq.podcini.util.Converter.getDurationStringLong +import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.TimeSpeedConverter +import ac.mdiq.podcini.storage.utils.TimeSpeedConverter import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.FlowEvent import android.os.Bundle @@ -243,45 +242,47 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener { } } + private var loadItemsRunning = false @UnstableApi private fun load() { Logd(TAG, "load() called") - lifecycleScope.launch { - try { - item = withContext(Dispatchers.IO) { - loadInBackground() - } - withContext(Dispatchers.Main) { - Logd(TAG, "load() item ${item?.id}") - if (item != null) { - val isFav = item!!.isFavorite - if (isFavorite != isFav) { - isFavorite = isFav - invalidateOptionsMenu(requireActivity()) + if (!loadItemsRunning) { + loadItemsRunning = true + lifecycleScope.launch { + try { + item = withContext(Dispatchers.IO) { + val feedItem = (curMedia as? EpisodeMedia)?.episode + if (feedItem != null) { + val duration = feedItem.media?.getDuration() ?: Int.MAX_VALUE + webviewData = ShownotesCleaner(requireContext()).processShownotes(feedItem.description ?: "", duration) + } + feedItem + } + withContext(Dispatchers.Main) { + Logd(TAG, "load() item ${item?.id}") + if (item != null) { + val isFav = item!!.isFavorite + if (isFavorite != isFav) { + isFavorite = isFav + invalidateOptionsMenu(requireActivity()) + } } + if (webviewData != null && !itemsLoaded) + webvDescription.loadDataWithBaseURL("https://127.0.0.1", + webviewData!!, + "text/html", + "utf-8", + "about:blank") + itemsLoaded = true } - onFragmentLoaded() - itemsLoaded = true + } catch (e: Throwable) { + Log.e(TAG, Log.getStackTraceString(e)) + } finally { + loadItemsRunning = false } - } catch (e: Throwable) { - Log.e(TAG, Log.getStackTraceString(e)) } } } - private fun loadInBackground(): Episode? { - val feedItem = (curMedia as? EpisodeMedia)?.episode - if (feedItem != null) { - val duration = feedItem.media?.getDuration()?: Int.MAX_VALUE - webviewData = ShownotesCleaner(requireContext()).processShownotes(feedItem.description?:"", duration) - } - return feedItem - } - - @UnstableApi private fun onFragmentLoaded() { - if (webviewData != null && !itemsLoaded) - webvDescription.loadDataWithBaseURL("https://127.0.0.1", webviewData!!, "text/html", "utf-8", "about:blank") - } - @UnstableApi private fun setupView() { showTimeLeft = shouldShowRemainingTime() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/statistics/FeedStatisticsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/statistics/FeedStatisticsFragment.kt index 0562402f..7efd56b0 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/statistics/FeedStatisticsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/statistics/FeedStatisticsFragment.kt @@ -3,7 +3,7 @@ package ac.mdiq.podcini.ui.statistics import ac.mdiq.podcini.databinding.FeedStatisticsBinding import ac.mdiq.podcini.storage.database.LogsAndStats.getStatistics import ac.mdiq.podcini.storage.model.StatisticsItem -import ac.mdiq.podcini.util.Converter.shortLocalizedDuration +import ac.mdiq.podcini.storage.utils.DurationConverter.shortLocalizedDuration import android.os.Bundle import android.text.format.Formatter import android.view.LayoutInflater diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/statistics/StatisticsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/statistics/StatisticsFragment.kt index 725f1d80..a521440c 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/statistics/StatisticsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/statistics/StatisticsFragment.kt @@ -12,10 +12,8 @@ import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter import ac.mdiq.podcini.ui.dialog.ConfirmationDialog import ac.mdiq.podcini.ui.dialog.DatesFilterDialog -import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment -import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion import ac.mdiq.podcini.ui.statistics.PieChartView.PieChartData -import ac.mdiq.podcini.util.Converter.shortLocalizedDuration +import ac.mdiq.podcini.storage.utils.DurationConverter.shortLocalizedDuration import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.FlowEvent diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/LocalDeleteModal.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/LocalDeleteModal.kt index c936f78c..5479265f 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/LocalDeleteModal.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/LocalDeleteModal.kt @@ -12,9 +12,8 @@ object LocalDeleteModal { fun deleteEpisodesWarnLocal(context: Context, items: Iterable) { val localItems: MutableList = mutableListOf() for (item in items) { - if (item.feed?.isLocalFeed == true) { - localItems.add(item) - } else deleteMediaOfEpisode(context, item) + if (item.feed?.isLocalFeed == true) localItems.add(item) + else deleteMediaOfEpisode(context, item) } if (localItems.isNotEmpty()) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/NotificationUtils.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/NotificationUtils.kt index e59ee297..bd1f46e9 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/NotificationUtils.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/NotificationUtils.kt @@ -1,8 +1,7 @@ package ac.mdiq.podcini.ui.utils import ac.mdiq.podcini.R -import ac.mdiq.podcini.preferences.UserPreferences.PREF_GPODNET_NOTIFICATIONS -import ac.mdiq.podcini.preferences.UserPreferences.PREF_SHOW_DOWNLOAD_REPORT +import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import android.content.Context import androidx.core.app.NotificationChannelCompat @@ -10,27 +9,31 @@ import androidx.core.app.NotificationChannelGroupCompat import androidx.core.app.NotificationManagerCompat object NotificationUtils { - const val CHANNEL_ID_USER_ACTION: String = "user_action" - const val CHANNEL_ID_DOWNLOADING: String = "downloading" - const val CHANNEL_ID_PLAYING: String = "playing" - const val CHANNEL_ID_DOWNLOAD_ERROR: String = "error" - const val CHANNEL_ID_SYNC_ERROR: String = "sync_error" - const val CHANNEL_ID_EPISODE_NOTIFICATIONS: String = "episode_notifications" + enum class CHANNEL_ID { + user_action, + downloading, + playing, + error, + sync_error, + episode_notifications + } - const val GROUP_ID_ERRORS: String = "group_errors" - const val GROUP_ID_NEWS: String = "group_news" + enum class GROUP_ID { + group_errors, + group_news + } /** * Used for migration of the preference to system notification channels. */ val showDownloadReportRaw: Boolean - get() = appPrefs.getBoolean(PREF_SHOW_DOWNLOAD_REPORT, true) + get() = appPrefs.getBoolean(UserPreferences.Prefs.prefShowDownloadReport.name, true) /** * Used for migration of the preference to system notification channels. */ val gpodnetNotificationsEnabledRaw: Boolean - get() = appPrefs.getBoolean(PREF_GPODNET_NOTIFICATIONS, true) + get() = appPrefs.getBoolean(UserPreferences.Prefs.pref_gpodnet_notifications.name, true) fun createChannels(context: Context) { val mNotificationManager = NotificationManagerCompat.from(context) @@ -51,22 +54,22 @@ object NotificationUtils { ) mNotificationManager.createNotificationChannelsCompat(channels) - mNotificationManager.deleteNotificationChannelGroup(GROUP_ID_NEWS) - mNotificationManager.deleteNotificationChannel(CHANNEL_ID_EPISODE_NOTIFICATIONS) + mNotificationManager.deleteNotificationChannelGroup(GROUP_ID.group_news.name) + mNotificationManager.deleteNotificationChannel(CHANNEL_ID.episode_notifications.name) } private fun createChannelUserAction(c: Context): NotificationChannelCompat { return NotificationChannelCompat.Builder( - CHANNEL_ID_USER_ACTION, NotificationManagerCompat.IMPORTANCE_HIGH) + CHANNEL_ID.user_action.name, NotificationManagerCompat.IMPORTANCE_HIGH) .setName(c.getString(R.string.notification_channel_user_action)) .setDescription(c.getString(R.string.notification_channel_user_action_description)) - .setGroup(GROUP_ID_ERRORS) + .setGroup(GROUP_ID.group_errors.name) .build() } private fun createChannelDownloading(c: Context): NotificationChannelCompat { return NotificationChannelCompat.Builder( - CHANNEL_ID_DOWNLOADING, NotificationManagerCompat.IMPORTANCE_LOW) + CHANNEL_ID.downloading.name, NotificationManagerCompat.IMPORTANCE_LOW) .setName(c.getString(R.string.notification_channel_downloading)) .setDescription(c.getString(R.string.notification_channel_downloading_description)) .setShowBadge(false) @@ -75,7 +78,7 @@ object NotificationUtils { private fun createChannelPlaying(c: Context): NotificationChannelCompat { return NotificationChannelCompat.Builder( - CHANNEL_ID_PLAYING, NotificationManagerCompat.IMPORTANCE_LOW) + CHANNEL_ID.playing.name, NotificationManagerCompat.IMPORTANCE_LOW) .setName(c.getString(R.string.notification_channel_playing)) .setDescription(c.getString(R.string.notification_channel_playing_description)) .setShowBadge(false) @@ -84,10 +87,10 @@ object NotificationUtils { private fun createChannelError(c: Context): NotificationChannelCompat { val notificationChannel = NotificationChannelCompat.Builder( - CHANNEL_ID_DOWNLOAD_ERROR, NotificationManagerCompat.IMPORTANCE_HIGH) + CHANNEL_ID.error.name, NotificationManagerCompat.IMPORTANCE_HIGH) .setName(c.getString(R.string.notification_channel_download_error)) .setDescription(c.getString(R.string.notification_channel_download_error_description)) - .setGroup(GROUP_ID_ERRORS) + .setGroup(GROUP_ID.group_errors.name) if (!showDownloadReportRaw) { // Migration from app managed setting: disable notification @@ -98,10 +101,10 @@ object NotificationUtils { private fun createChannelSyncError(c: Context): NotificationChannelCompat { val notificationChannel = NotificationChannelCompat.Builder( - CHANNEL_ID_SYNC_ERROR, NotificationManagerCompat.IMPORTANCE_HIGH) + CHANNEL_ID.sync_error.name, NotificationManagerCompat.IMPORTANCE_HIGH) .setName(c.getString(R.string.notification_channel_sync_error)) .setDescription(c.getString(R.string.notification_channel_sync_error_description)) - .setGroup(GROUP_ID_ERRORS) + .setGroup(GROUP_ID.group_errors.name) if (!gpodnetNotificationsEnabledRaw) { // Migration from app managed setting: disable notification @@ -120,7 +123,7 @@ object NotificationUtils { // } private fun createGroupErrors(c: Context): NotificationChannelGroupCompat { - return NotificationChannelGroupCompat.Builder(GROUP_ID_ERRORS) + return NotificationChannelGroupCompat.Builder(GROUP_ID.group_errors.name) .setName(c.getString(R.string.notification_group_errors)) .build() } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/ShownotesCleaner.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/ShownotesCleaner.kt index d1947d67..18152050 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/ShownotesCleaner.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/ShownotesCleaner.kt @@ -1,8 +1,8 @@ package ac.mdiq.podcini.ui.utils import ac.mdiq.podcini.R -import ac.mdiq.podcini.util.Converter.durationStringLongToMs -import ac.mdiq.podcini.util.Converter.durationStringShortToMs +import ac.mdiq.podcini.storage.utils.DurationConverter.durationStringLongToMs +import ac.mdiq.podcini.storage.utils.DurationConverter.durationStringShortToMs import ac.mdiq.podcini.util.Logd import android.content.Context import android.graphics.Color diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/view/ShownotesWebView.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/view/ShownotesWebView.kt index 79d238ce..0bf4a1e1 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/view/ShownotesWebView.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/view/ShownotesWebView.kt @@ -2,6 +2,7 @@ package ac.mdiq.podcini.ui.view import ac.mdiq.podcini.R import ac.mdiq.podcini.net.utils.NetworkUtils +import ac.mdiq.podcini.storage.utils.DurationConverter import ac.mdiq.podcini.ui.actions.menuhandler.MenuItemUtils import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.utils.ShownotesCleaner @@ -136,7 +137,7 @@ class ShownotesWebView : WebView, View.OnLongClickListener { if (ShownotesCleaner.isTimecodeLink(selectedUrl)) { menu.add(Menu.NONE, R.id.go_to_position_item, Menu.NONE, R.string.go_to_position_label) - menu.setHeaderTitle(Converter.getDurationStringLong(ShownotesCleaner.getTimecodeLinkTime(selectedUrl))) + menu.setHeaderTitle(DurationConverter.getDurationStringLong(ShownotesCleaner.getTimecodeLinkTime(selectedUrl))) } else { val uri = Uri.parse(selectedUrl) val intent = Intent(Intent.ACTION_VIEW, uri) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/view/viewholder/EpisodeViewHolder.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/view/viewholder/EpisodeViewHolder.kt index ea124b5e..3aeaad32 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/view/viewholder/EpisodeViewHolder.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/view/viewholder/EpisodeViewHolder.kt @@ -20,10 +20,9 @@ import ac.mdiq.podcini.ui.utils.CoverLoader import ac.mdiq.podcini.ui.utils.ThemeUtils import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr import ac.mdiq.podcini.ui.view.CircularProgressBar -import ac.mdiq.podcini.util.Converter +import ac.mdiq.podcini.storage.utils.DurationConverter import ac.mdiq.podcini.util.DateFormatter import ac.mdiq.podcini.util.Logd -import android.graphics.PorterDuff import android.text.Layout import android.text.format.Formatter import android.util.Log @@ -36,7 +35,6 @@ import android.widget.ProgressBar import android.widget.TextView import androidx.appcompat.content.res.AppCompatResources import androidx.cardview.widget.CardView -import androidx.core.content.ContextCompat import androidx.media3.common.util.UnstableApi import androidx.recyclerview.widget.RecyclerView import com.google.android.material.elevation.SurfaceColors @@ -195,22 +193,22 @@ open class EpisodeViewHolder(private val activity: MainActivity, parent: ViewGro } } - duration.text = Converter.getDurationStringLong(media.getDuration()) + duration.text = DurationConverter.getDurationStringLong(media.getDuration()) duration.setContentDescription(activity.getString(R.string.chapter_duration, - Converter.getDurationStringLocalized(activity, media.getDuration().toLong()))) + DurationConverter.getDurationStringLocalized(activity, media.getDuration().toLong()))) if (isCurMedia || this.episode?.isInProgress == true) { val progress: Int = (100.0 * media.getPosition() / media.getDuration()).toInt() val remainingTime = max((media.getDuration() - media.getPosition()).toDouble(), 0.0).toInt() progressBar.progress = progress - position.text = Converter.getDurationStringLong(media.getPosition()) + position.text = DurationConverter.getDurationStringLong(media.getPosition()) position.setContentDescription(activity.getString(R.string.position, - Converter.getDurationStringLocalized(activity, media.getPosition().toLong()))) + DurationConverter.getDurationStringLocalized(activity, media.getPosition().toLong()))) progressBar.visibility = View.VISIBLE position.visibility = View.VISIBLE if (UserPreferences.shouldShowRemainingTime()) { - duration.text = (if ((remainingTime > 0)) "-" else "") + Converter.getDurationStringLong(remainingTime) + duration.text = (if ((remainingTime > 0)) "-" else "") + DurationConverter.getDurationStringLong(remainingTime) duration.setContentDescription(activity.getString(R.string.chapter_duration, - Converter.getDurationStringLocalized(activity, (media.getDuration() - media.getPosition()).toLong()))) + DurationConverter.getDurationStringLocalized(activity, (media.getDuration() - media.getPosition()).toLong()))) } } else { progressBar.visibility = View.GONE @@ -269,15 +267,15 @@ open class EpisodeViewHolder(private val activity: MainActivity, parent: ViewGro val currentPosition = item.media?.position ?: 0 val timeDuration = item.media?.duration ?: 0 progressBar.progress = (100.0 * currentPosition / timeDuration).toInt() - position.text = Converter.getDurationStringLong(currentPosition) + position.text = DurationConverter.getDurationStringLong(currentPosition) val remainingTime = max((timeDuration - currentPosition).toDouble(), 0.0).toInt() if (currentPosition == Playable.INVALID_TIME || timeDuration == Playable.INVALID_TIME) { Log.w(TAG, "Could not react to position observer update because of invalid time") return } - if (UserPreferences.shouldShowRemainingTime()) duration.text = (if (remainingTime > 0) "-" else "") + Converter.getDurationStringLong(remainingTime) - else duration.text = Converter.getDurationStringLong(timeDuration) + if (UserPreferences.shouldShowRemainingTime()) duration.text = (if (remainingTime > 0) "-" else "") + DurationConverter.getDurationStringLong(remainingTime) + else duration.text = DurationConverter.getDurationStringLong(timeDuration) duration.visibility = View.VISIBLE // Even if the duration was previously unknown, it is now known } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/widget/WidgetUpdater.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/widget/WidgetUpdater.kt index 1e487361..9fc0e2ee 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/widget/WidgetUpdater.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/widget/WidgetUpdater.kt @@ -13,9 +13,9 @@ import ac.mdiq.podcini.storage.model.Playable import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter import ac.mdiq.podcini.ui.activity.starter.PlaybackSpeedActivityStarter import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter -import ac.mdiq.podcini.util.Converter.getDurationStringLong +import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.TimeSpeedConverter +import ac.mdiq.podcini.storage.utils.TimeSpeedConverter import android.appwidget.AppWidgetManager import android.content.ComponentName import android.content.Context @@ -155,10 +155,10 @@ object WidgetUpdater { if (columns < 3) views.setViewVisibility(R.id.layout_center, View.INVISIBLE) else views.setViewVisibility(R.id.layout_center, View.VISIBLE) - val showPlaybackSpeed = prefs!!.getBoolean(PlayerWidget.KEY_WIDGET_PLAYBACK_SPEED + id, true) - val showRewind = prefs!!.getBoolean(PlayerWidget.KEY_WIDGET_REWIND + id, true) - val showFastForward = prefs!!.getBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + id, true) - val showSkip = prefs!!.getBoolean(PlayerWidget.KEY_WIDGET_SKIP + id, true) + val showPlaybackSpeed = prefs!!.getBoolean(PlayerWidget.Prefs.widget_playback_speed.name + id, true) + val showRewind = prefs!!.getBoolean(PlayerWidget.Prefs.widget_rewind.name + id, true) + val showFastForward = prefs!!.getBoolean(PlayerWidget.Prefs.widget_fast_forward.name + id, true) + val showSkip = prefs!!.getBoolean(PlayerWidget.Prefs.widget_skip.name + id, true) if (showPlaybackSpeed || showRewind || showSkip || showFastForward) { views.setInt(R.id.extendedButtonsContainer, "setVisibility", View.VISIBLE) @@ -172,7 +172,7 @@ object WidgetUpdater { views.setInt(R.id.butPlay, "setVisibility", View.VISIBLE) } - val backgroundColor = prefs!!.getInt(PlayerWidget.KEY_WIDGET_COLOR + id, PlayerWidget.DEFAULT_COLOR) + val backgroundColor = prefs!!.getInt(PlayerWidget.Prefs.widget_color.name + id, PlayerWidget.DEFAULT_COLOR) views.setInt(R.id.widgetLayout, "setBackgroundColor", backgroundColor) manager.updateAppWidget(id, views) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/util/ShareUtils.kt b/app/src/main/kotlin/ac/mdiq/podcini/util/ShareUtils.kt index f7742312..e8562e47 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/util/ShareUtils.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/util/ShareUtils.kt @@ -4,7 +4,7 @@ import ac.mdiq.podcini.R import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.Feed -import ac.mdiq.podcini.util.Converter.getDurationStringLong +import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong import android.content.Context import android.util.Log import androidx.core.app.ShareCompat.IntentBuilder diff --git a/app/src/main/res/layout/autodownload_filter_dialog.xml b/app/src/main/res/layout/autodownload_filter_dialog.xml index 8580c542..a37e4566 100644 --- a/app/src/main/res/layout/autodownload_filter_dialog.xml +++ b/app/src/main/res/layout/autodownload_filter_dialog.xml @@ -66,6 +66,7 @@ android:background="?android:attr/listDivider" /> + + + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 529f31f4..2c7edc35 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -740,8 +740,8 @@ المصفاة في المفضلة ليست مفضلة - تم التنزيل - لم يتم التنزيل + تم التنزيل + لم يتم التنزيل ضمن لائحة الاستماع ليست ضمن لائحة الاستماع فيها وسائط diff --git a/app/src/main/res/values-ast/strings.xml b/app/src/main/res/values-ast/strings.xml index 016a5e75..82576a21 100644 --- a/app/src/main/res/values-ast/strings.xml +++ b/app/src/main/res/values-ast/strings.xml @@ -360,8 +360,8 @@ Nun ye posible aniciar el xestor de ficheros del sistema Peñera Ta en Favoritos - Baxó - Nun baxó + Baxó + Nun baxó Na cola Nun ta na cola Tien multimedia diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml index a4bd906f..b1531dcf 100644 --- a/app/src/main/res/values-br/strings.xml +++ b/app/src/main/res/values-br/strings.xml @@ -600,8 +600,8 @@ Sil Ur sined eo N\'eo ket ur sined - Pellgarget - N\'eo ket pellgarget + Pellgarget + N\'eo ket pellgarget Lakaet el lost N\'emañ ket el lost Gant ur media diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index c7f4c002..c35bc16e 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -675,8 +675,8 @@ Filtra És favorit No és favorit - Baixat - No baixat + Baixat + No baixat En cua No encuat Té mitjans diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 0e09b4f7..085ecbc2 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -736,8 +736,8 @@ Filtr Je mezi oblíbenými Není mezi oblíbenými - Stažené - Nestažené + Stažené + Nestažené Ve frontě Mimo frontu Obsahuje média diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 33a27feb..91a39a2a 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -706,8 +706,8 @@ Filtrér Er favorit Ikke favorit - Overførte - Ikke overførte + Overførte + Ikke overførte Sat i kø Ikke sat i kø Har medier diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index b8d3e0be..d66dbabe 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -710,8 +710,8 @@ Filtern Favorit Nicht favorisiert - Heruntergeladen - Nicht heruntergeladen + Heruntergeladen + Nicht heruntergeladen In Warteschlange Nicht in Warteschlange Hat Medien diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 8c644b9f..bb6d0964 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -727,8 +727,8 @@ Filtro Es favorito No favorito - Descargados - No descargados + Descargados + No descargados En cola No están en cola Tiene multimedia diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 27cccfde..76bd171e 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -507,8 +507,8 @@ Filter On lemmik Pole lemmik - Alla laaditud - Pole alla laaditud + Alla laaditud + Pole alla laaditud Järjekorras Pole järjekorras On meediafaile diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 86036ed3..36053bd2 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -628,8 +628,8 @@ Iragazia Gogokoa da Ez da gogokoa - Deskargatuta - Deskargatu gabe + Deskargatuta + Deskargatu gabe Ilaran Ez dago ilaran Media du diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 3971ab17..95fac2f7 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -674,8 +674,8 @@ فیلتر مورد علاقه مورد علاقه نیست - بارگیری‌شده - بارگیری‌نشده + بارگیری‌شده + بارگیری‌نشده در صف گذاشته شد در صف گذاشته نشد رسانه دارد diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index e694e4de..adadbfd0 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -651,8 +651,8 @@ Suodin On suosikki Ei suosikki - Ladattu - Ei ladattu + Ladattu + Ei ladattu Jonossa Ei jonossa On media diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 7f7709b1..772fb2d8 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -727,8 +727,8 @@ Filtrer Favori Pas favori - Téléchargé - Non téléchargé + Téléchargé + Non téléchargé Dans la liste de lecture Pas dans la liste de lecture Avec média diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index cf8049be..fc7c7625 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -706,8 +706,8 @@ Filtrado É favorito Non favorito - Descargado - Non descargado + Descargado + Non descargado Na cola Fóra da cola Ten multimedia diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 5c1f3aec..7992398a 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -558,8 +558,8 @@ Szűrő Kedvenc Nem kedvenc - Letöltött - Nem letöltött + Letöltött + Nem letöltött Sorba állított Nem sorba állított Médiát tartalmaz diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 6590e578..f3a9c63c 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -570,8 +570,8 @@ Filter Favorit Bukan favorit - Diunduh - Tidak diunduh + Diunduh + Tidak diunduh Diantre Tidak diantre Ada media diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 11f9de6c..72dc7b1c 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -728,8 +728,8 @@ Filtra Preferiti Non preferiti - Scaricati - Non scaricati + Scaricati + Non scaricati In coda Non in coda Con media diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 54b3acb1..a50bc7d4 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -736,8 +736,8 @@ מסנן עם סימון מועדף לא במועדפים - הורד - לא הורד + הורד + לא הורד בתור לא בתור יש מדיה diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index c3a685c7..7f0abc0f 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -517,8 +517,8 @@ フィルター お気に入り お気に入り外 - ダウンロード済 - 未ダウンロード + ダウンロード済 + 未ダウンロード キューに追加済 キューに未追加 メディア有り diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 1cb8adac..70e83a69 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -650,8 +650,8 @@ 필터 즐겨찾기 포함 즐겨찾기 아님 - 다운로드함 - 다운로드 안 함 + 다운로드함 + 다운로드 안 함 대기열에 있음 대기열에 없음 미디어 있음 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 3b4d612d..f416df91 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -524,8 +524,8 @@ Filtruoti Mėgstami Ne mėgstami - Atsiųsti - Neatsiųsti + Atsiųsti + Neatsiųsti Eilėje Ne eilėje Turintys medijos failų diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index f2918fd3..cd4119fc 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -662,8 +662,8 @@ Filter Er favoritt Ikke favoritt - Nedlastet - Ikke nedlastet + Nedlastet + Ikke nedlastet I kø Ikke i kø Har media diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 7c128ef8..668746c7 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -617,8 +617,8 @@ Filter Gemarkeerd als favoriet Geen favoriet - Gedownload - Niet gedownload + Gedownload + Niet gedownload In de wachtrij Niet in de wachtrij Bevat media diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 4bdb0e2a..2cf70c2f 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -676,8 +676,8 @@ Filtruj Ulubione Nie oznaczone jako ulubione - Pobrane - Nie pobrane + Pobrane + Nie pobrane W kolejce Nie w kolejce Ma media diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 2d22c43a..665fe18e 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -665,8 +665,8 @@ Filtrar É favorito Não favorito - Baixado - Não baixado + Baixado + Não baixado Enfileirado Não enfileirado Possui mídia diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 24337d5f..cfd024d1 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -722,8 +722,8 @@ Filtro Favorito Não favorito - Descarregado - Não descarregado + Descarregado + Não descarregado Na fila Não na fila Tem ficheiro diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 8095f317..68c278cf 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -719,8 +719,8 @@ Filtrează Este favorit Nu este favorit - Descărcat - Nu este descărcat + Descărcat + Nu este descărcat În coada de redare Nu este în coada de redare Are parte media diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ad73899f..2cd53cd3 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -701,8 +701,8 @@ Фильтровать В избранном Не в избранном - Загружено - Не загружено + Загружено + Не загружено В очереди Не в очереди С медиа diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 6fb74552..d818df71 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -742,8 +742,8 @@ Filter Je v obľúbených Nie je v obľúbených - Stiahnuté - Nestiahnuté + Stiahnuté + Nestiahnuté V poradí Nie je v poradí Obsahuje médiá diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index bc6ece94..0a1c6339 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -506,8 +506,8 @@ Filter Je med priljubljenimi Ni priljubljeno - Prenešeno - Ni prenešeno + Prenešeno + Ni prenešeno V čakalni vrsti Ni v čakalni vrsti Ima predstavnostno vsebino diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 81d220eb..fe186e31 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -702,8 +702,8 @@ Filtrera Är favorit Ej favorit - Nedladdade - Ej nedladdade + Nedladdade + Ej nedladdade Köad Ej köad Har media diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 3f5c0eab..4eda2a21 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -672,8 +672,8 @@ Filtre Favori Favori değil - İndirildi - İndirilmedi + İndirildi + İndirilmedi Kuyrukta Kuyrukta değil Medya var diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 505bc8b2..92b9b0d0 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -742,8 +742,8 @@ Фільтр В улюблених Не в улюблених - Завантажені - Не завантажені + Завантажені + Не завантажені В черзі Не в черзі З медіа diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 20190c22..0bc3b43f 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -689,8 +689,8 @@ 过滤器 收藏的 未收藏 - 已下载 - 未下载 + 已下载 + 未下载 已在播放列表中 不在播放列表中 包含媒体 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 43121bf6..94d6aae1 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -412,8 +412,8 @@ 篩選 為最愛 未標記為最愛的 - 已下載 - 未下載 + 已下載 + 未下載 已列入待播清單 未列入待播清單 有額外媒體檔 diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 1044b51c..d5e6cec2 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -1,6 +1,17 @@ + + @string/feed_auto_download_new + @string/feed_auto_download_newer + @string/feed_auto_download_older + + + 0 + 1 + 2 + + @string/global_default @string/feed_auto_download_always @@ -92,6 +103,27 @@ -1 + + 2 + 3 + 4 + 5 + 10 + 15 + 20 + @string/pref_episode_cache_unlimited + + + 2 + 3 + 4 + 5 + 10 + 15 + 20 + -1 + + @string/pref_mobileUpdate_refresh @string/pref_mobileUpdate_episode_download @@ -188,27 +220,6 @@ 3 - - - - - - - - - - - - - - - - - - - - - @string/skip_episode_label @string/next_chapter diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3ccebe59..ae2f7273 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -81,6 +81,7 @@ Title Publish date + Download date Unplayed publish date Played count New count @@ -140,6 +141,11 @@ Always Never + Auto download policy + Only new items + Newest unplayed + Oldest unplayed + Nothing Never When not favorited @@ -354,6 +360,7 @@ Sort Keep sorted Publish date + Download date Date Played date Completed date @@ -392,7 +399,7 @@ Synchronization Synchronize with other devices Auto backup of OPML - Auto backup OPML files to Google. Once enabled, after Podcini is uninstalled and re-installed, the subscriptions will be automatically restored. + Auto backup OPML files to Google. Once enabled, after Podcini is uninstalled and re-installed, the subscriptions can be restored. Toggle this setting will restart Podcini. Automation Details Import/Export @@ -648,6 +655,9 @@ Favorites export Export saved favorites to file + Restore subscriptions + Previous podcasts subscriptions can be restores. Would you like to restore them? + Set sleep timer Disable sleep timer @@ -774,11 +784,14 @@ {faw_info_circle} Only common tags from all selected subscriptions are shown. Other tags stay unaffected. Auto download settings Episode filter + Episode inclusive filter + Episode exclusive filter List of terms used to decide if an episode should be included or excluded when auto downloading Add term Exclude episodes containing any of the terms below Include only episodes containing any of the terms below Exclude episodes shorter than + Mark excluded episodes played Keep updated Include this podcast when (auto-)refreshing all podcasts Auto download is disabled in the main Podcini settings @@ -827,8 +840,10 @@ Filter Is favorite Not favorite - Downloaded - Not downloaded + Downloaded + Not downloaded + Auto downloadable + Not auto downloadable Queued Not queued Has media diff --git a/app/src/main/res/xml/feed_settings.xml b/app/src/main/res/xml/feed_settings.xml index d421f401..3d109a08 100644 --- a/app/src/main/res/xml/feed_settings.xml +++ b/app/src/main/res/xml/feed_settings.xml @@ -55,9 +55,25 @@ + + + android:title="@string/episode_inclusive_filters_label" /> + diff --git a/app/src/test/kotlin/ac/mdiq/podcini/feed/FeedAutoDownloadFilterTest.kt b/app/src/test/kotlin/ac/mdiq/podcini/feed/FeedAutoDownloadFilterTest.kt index 2810b24e..ae056024 100644 --- a/app/src/test/kotlin/ac/mdiq/podcini/feed/FeedAutoDownloadFilterTest.kt +++ b/app/src/test/kotlin/ac/mdiq/podcini/feed/FeedAutoDownloadFilterTest.kt @@ -1,6 +1,6 @@ package ac.mdiq.podcini.feed -import ac.mdiq.podcini.util.Converter.durationStringShortToMs +import ac.mdiq.podcini.storage.utils.DurationConverter.durationStringShortToMs import ac.mdiq.podcini.storage.model.FeedAutoDownloadFilter import ac.mdiq.podcini.storage.model.Episode import org.junit.Assert diff --git a/app/src/test/kotlin/ac/mdiq/podcini/storage/DbCleanupTests.kt b/app/src/test/kotlin/ac/mdiq/podcini/storage/DbCleanupTests.kt index 7e3614e4..811a8cfd 100644 --- a/app/src/test/kotlin/ac/mdiq/podcini/storage/DbCleanupTests.kt +++ b/app/src/test/kotlin/ac/mdiq/podcini/storage/DbCleanupTests.kt @@ -49,17 +49,10 @@ open class DbCleanupTests { Assert.assertTrue(destFolder.exists()) Assert.assertTrue(destFolder.canWrite()) - // create new database -// PodDBAdapter.init(context) -// deleteDatabase() -// val adapter = getInstance() -// adapter.open() -// adapter.close() - val prefEdit = PreferenceManager.getDefaultSharedPreferences(context.applicationContext).edit() - prefEdit.putString(UserPreferences.PREF_EPISODE_CACHE_SIZE, EPISODE_CACHE_SIZE.toString()) - prefEdit.putString(UserPreferences.PREF_EPISODE_CLEANUP, cleanupAlgorithm.toString()) - prefEdit.putBoolean(UserPreferences.PREF_ENABLE_AUTODL, true) + prefEdit.putString(UserPreferences.Prefs.prefEpisodeCacheSize.name, EPISODE_CACHE_SIZE.toString()) + prefEdit.putString(UserPreferences.Prefs.prefEpisodeCleanup.name, cleanupAlgorithm.toString()) + prefEdit.putBoolean(UserPreferences.Prefs.prefEnableAutoDl.name, true) prefEdit.commit() UserPreferences.init(context) diff --git a/app/src/test/kotlin/ac/mdiq/podcini/storage/DbNullCleanupAlgorithmTest.kt b/app/src/test/kotlin/ac/mdiq/podcini/storage/DbNullCleanupAlgorithmTest.kt index 61a28475..e7ee55ef 100644 --- a/app/src/test/kotlin/ac/mdiq/podcini/storage/DbNullCleanupAlgorithmTest.kt +++ b/app/src/test/kotlin/ac/mdiq/podcini/storage/DbNullCleanupAlgorithmTest.kt @@ -44,8 +44,8 @@ class DbNullCleanupAlgorithmTest { // adapter.close() val prefEdit = PreferenceManager.getDefaultSharedPreferences(context!!.applicationContext).edit() - prefEdit.putString(UserPreferences.PREF_EPISODE_CACHE_SIZE, EPISODE_CACHE_SIZE.toString()) - prefEdit.putString(UserPreferences.PREF_EPISODE_CLEANUP, UserPreferences.EPISODE_CLEANUP_NULL.toString()) + prefEdit.putString(UserPreferences.Prefs.prefEpisodeCacheSize.name, EPISODE_CACHE_SIZE.toString()) + prefEdit.putString(UserPreferences.Prefs.prefEpisodeCleanup.name, UserPreferences.EPISODE_CLEANUP_NULL.toString()) prefEdit.commit() UserPreferences.init(context!!) diff --git a/app/src/test/kotlin/ac/mdiq/podcini/storage/DbReaderTest.kt b/app/src/test/kotlin/ac/mdiq/podcini/storage/DbReaderTest.kt index 6a97565a..187c9f95 100644 --- a/app/src/test/kotlin/ac/mdiq/podcini/storage/DbReaderTest.kt +++ b/app/src/test/kotlin/ac/mdiq/podcini/storage/DbReaderTest.kt @@ -223,7 +223,7 @@ class DbReaderTest { fun testGetDownloadedItems() { val numItems = 10 val downloaded = saveDownloadedItems(numItems) - val downloadedSaved = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.DOWNLOADED), EpisodeSortOrder.DATE_NEW_OLD) + val downloadedSaved = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.downloaded.name), EpisodeSortOrder.DATE_NEW_OLD) Assert.assertNotNull(downloadedSaved) Assert.assertEquals(downloaded.size.toLong(), downloadedSaved.size.toLong()) for (item in downloadedSaved) { @@ -266,7 +266,7 @@ class DbReaderTest { for (i in newItems.indices) { unreadIds[i] = newItems[i].id } - val newItemsSaved = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.NEW), EpisodeSortOrder.DATE_NEW_OLD) + val newItemsSaved = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.new.name), EpisodeSortOrder.DATE_NEW_OLD) Assert.assertNotNull(newItemsSaved) Assert.assertEquals(newItemsSaved.size.toLong(), newItems.size.toLong()) for (feedItem in newItemsSaved) { diff --git a/app/src/test/kotlin/ac/mdiq/podcini/storage/DbWriterTest.kt b/app/src/test/kotlin/ac/mdiq/podcini/storage/DbWriterTest.kt index f8fcf434..5e549afb 100644 --- a/app/src/test/kotlin/ac/mdiq/podcini/storage/DbWriterTest.kt +++ b/app/src/test/kotlin/ac/mdiq/podcini/storage/DbWriterTest.kt @@ -4,7 +4,6 @@ import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.preferences.UserPreferences -import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue import ac.mdiq.podcini.storage.database.Episodes.addToHistory import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodes import ac.mdiq.podcini.storage.database.Episodes.deleteMediaOfEpisode @@ -12,6 +11,7 @@ import ac.mdiq.podcini.storage.database.Episodes.getEpisode import ac.mdiq.podcini.storage.database.Episodes.getEpisodeMedia import ac.mdiq.podcini.storage.database.Episodes.persistEpisode import ac.mdiq.podcini.storage.database.Episodes.persistEpisodeMedia +import ac.mdiq.podcini.storage.database.Episodes.shouldDeleteRemoveFromQueue import ac.mdiq.podcini.storage.database.Feeds.deleteFeedSync import ac.mdiq.podcini.storage.database.Queues import ac.mdiq.podcini.storage.database.Queues.addToQueue @@ -71,7 +71,7 @@ class DbWriterTest { val prefEdit = PreferenceManager.getDefaultSharedPreferences( context.applicationContext).edit() - prefEdit.putBoolean(UserPreferences.PREF_DELETE_REMOVES_FROM_QUEUE, true).commit() + prefEdit.putBoolean(UserPreferences.Prefs.prefDeleteRemovesFromQueue.name, true).commit() } @After diff --git a/app/src/test/kotlin/ac/mdiq/podcini/util/ConverterTest.kt b/app/src/test/kotlin/ac/mdiq/podcini/util/DurationConverterTest.kt similarity index 74% rename from app/src/test/kotlin/ac/mdiq/podcini/util/ConverterTest.kt rename to app/src/test/kotlin/ac/mdiq/podcini/util/DurationConverterTest.kt index 60023c61..f23426db 100644 --- a/app/src/test/kotlin/ac/mdiq/podcini/util/ConverterTest.kt +++ b/app/src/test/kotlin/ac/mdiq/podcini/util/DurationConverterTest.kt @@ -1,16 +1,16 @@ package ac.mdiq.podcini.util -import ac.mdiq.podcini.util.Converter.durationStringLongToMs -import ac.mdiq.podcini.util.Converter.durationStringShortToMs -import ac.mdiq.podcini.util.Converter.getDurationStringLong -import ac.mdiq.podcini.util.Converter.getDurationStringShort +import ac.mdiq.podcini.storage.utils.DurationConverter.durationStringLongToMs +import ac.mdiq.podcini.storage.utils.DurationConverter.durationStringShortToMs +import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong +import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringShort import org.junit.Assert import org.junit.Test /** * Test class for converter */ -class ConverterTest { +class DurationConverterTest { @Test fun testGetDurationStringLong() { val expected = "13:05:10" diff --git a/app/src/test/kotlin/ac/mdiq/podcini/util/EpisodePermutorsTest.kt b/app/src/test/kotlin/ac/mdiq/podcini/util/EpisodePermutorsTest.kt index 18791cfe..89ea6867 100644 --- a/app/src/test/kotlin/ac/mdiq/podcini/util/EpisodePermutorsTest.kt +++ b/app/src/test/kotlin/ac/mdiq/podcini/util/EpisodePermutorsTest.kt @@ -1,6 +1,6 @@ package ac.mdiq.podcini.util -import ac.mdiq.podcini.util.sorting.EpisodesPermutors.getPermutor +import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.EpisodeMedia diff --git a/changelog.md b/changelog.md index bd1ea44d..0a635883 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,30 @@ +# 6.1.0 + +* in FeedEpisode view fixed filtering after an episode's play state is changed +* fixed refreshing a feed causes duplicate episodes in FeedEpisodes view +* fixed the non-functioning of "Set navigation drawer items" +* fixed crash when changing filter during media playing +* item selection in list views only updates the selected item (rather than the whole list) +* fixed episode action button not updated when deleting the media in episode list views +* skipped concurrent calls for loading data in multiple views +* toggle "Auto backup of OPML" in Settings will restart Podcini +* automatically restoring backup of OPML upon new install is disabled. Instead, in AddFeed view, when subscription is empty and OPML backup is available, a dialog is shown to ask about restoring. +* added audo downloadable to episodes filter +* added download date to episodes sorting +* added download date to feed sorting +* auto download algorithm is changed to individual feed based. + * When auto download is enabled in the Settings, feeds to be auto-downloaded need to be separately enabled in the feed settings. + * Each feed also has its own download policy (only new episodes, newest episodes, and oldest episodes. newest episodes meaning most recent episodes new or old) + * Each feed has its own limit (Episode cache) for number of episodes downloaded, this limit rules in combination of the overall limit for the app. + * After auto download run, episodes with New status is changed to Unplayed. + * auto download feed setting dialog is also changed: + * there are now separate dialogs for inclusive and exclusive filters where filter tokens can be specified independently + * on exclusive dialog, there are optional check boxes "Exclude episodes shorter than" and "Mark excluded episodes played" +* set default value of "Include in auto downloads" in feed setting to false +* remove an episode from queue no longer triggers auto download +* got rid of many string delegates, in favor of enums +* some class restructuring + # 6.0.13 * removed from preferences "Choose data folder", it's not suitable for newer Androids diff --git a/fastlane/metadata/android/en-US/changelogs/3020214.txt b/fastlane/metadata/android/en-US/changelogs/3020214.txt new file mode 100644 index 00000000..fe4be406 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020214.txt @@ -0,0 +1,27 @@ + +Version 6.0.14 brings several changes: + +* in FeedEpisode view fixed filtering after an episode's play state is changed +* fixed refreshing a feed causes duplicate episodes in FeedEpisodes view +* fixed the non-functioning of "Set navigation drawer items" +* fixed crash when changing filter during media playing +* item selection in list views only updates the selected item (rather than the whole list) +* fixed episode action button not updated when deleting the media in episode list views +* skipped concurrent calls for loading data in multiple views +* toggle "Auto backup of OPML" in Settings will restart Podcini +* automatically restoring backup of OPML upon new install is disabled. Instead, in AddFeed view, when subscription is empty and OPML backup is available, a dialog is shown to ask about restoring. +* added audo downloadable to episodes filter +* added download date to episodes sorting +* added download date to feed sorting +* auto download algorithm is changed to individual feed based. + * When auto download is enabled in the Settings, feeds to be auto-downloaded need to be separately enabled in the feed settings. + * Each feed also has its own download policy (only new episodes, newest episodes, and oldest episodes. newest episodes meaning most recent episodes new or old) + * Each feed has its own limit (Episode cache) for number of episodes downloaded, this limit rules in combination of the overall limit for the app. + * After auto download run, episodes with New status is changed to Unplayed. + * auto download feed setting dialog is also changed: + * there are now separate dialogs for inclusive and exclusive filters where filter tokens can be specified independently + * on exclusive dialog, there are optional check boxes "Exclude episodes shorter than" and "Mark excluded episodes played" +* set default value of "Include in auto downloads" in feed setting to false +* remove an episode from queue no longer triggers auto download +* got rid of many string delegates, in favor of enums +* some class restructuring