diff --git a/README.md b/README.md index e39572df..4f497e08 100644 --- a/README.md +++ b/README.md @@ -83,10 +83,10 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c * Long-press filter button in FeedEpisodes view enables/disables filters without changing filter settings * History view shows time of last play, and allows filters and sorts * 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 + * 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 * Every queue is circular: if the final item in queue finished, the first item in queue (if exists) will get played -* Every queue has a bin of past episodes added to the queue +* Every queue has a bin containing past episodes removed from the queue ### Podcast/Episode @@ -127,13 +127,13 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c * 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 * Settings/Preferences can now be exported and imported * Play history/progress can be separately exported/imported as Json files * Downloaded media files can be exported/imported +* Reconsile feature (accessed from Downloads view) is added to ensure downloaded media files are in sync with specs in DB * There is a setting to disable/enable auto backup of OPML files to Google * Upon re-install of Podcini, the OPML file previously backed up to Google is not imported automatically but based on user confirmation. diff --git a/app/build.gradle b/app/build.gradle index c4feeaf9..b6cbcbc0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -126,8 +126,8 @@ android { buildConfig true } defaultConfig { - versionCode 3020219 - versionName "6.1.5" + versionCode 3020220 + versionName "6.1.6" applicationId "ac.mdiq.podcini.R" def commit = "" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/gpoddernet/GpodnetService.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/gpoddernet/GpodnetService.kt index 9c5b23a2..a248e51a 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/gpoddernet/GpodnetService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/gpoddernet/GpodnetService.kt @@ -50,7 +50,6 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?, /** * Searches the podcast directory for a given string. - * * @param query The search query * @param scaledLogoSize The size of the logos that are returned by the search query. * Must be in range 1..256. If the value is out of range, the @@ -82,10 +81,7 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?, val devices: List /** * Returns all devices of a given user. - * - * * This method requires authentication. - * * @throws GpodnetServiceAuthenticationException If there is an authentication error. */ get() { @@ -110,10 +106,7 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?, /** * Configures the device of a given user. - * - * * This method requires authentication. - * * @param deviceId The ID of the device that should be configured. * @throws GpodnetServiceAuthenticationException If there is an authentication error. */ @@ -147,13 +140,9 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?, /** * Uploads the subscriptions of a specific device. - * - * * This method requires authentication. - * * @param deviceId The ID of the device whose subscriptions should be updated. - * @param subscriptions A list of feed URLs containing all subscriptions of the - * device. + * @param subscriptions A list of feed URLs containing all subscriptions of the device. * @throws IllegalArgumentException If username, deviceId or subscriptions is null. * @throws GpodnetServiceAuthenticationException If there is an authentication error. */ @@ -181,16 +170,11 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?, /** * Updates the subscription list of a specific device. - * - * * This method requires authentication. - * * @param added Collection of feed URLs of added feeds. This Collection MUST NOT contain any duplicates * @param removed Collection of feed URLs of removed feeds. This Collection MUST NOT contain any duplicates - * @return a GpodnetUploadChangesResponse. See [GpodnetUploadChangesResponse] - * for details. - * @throws GpodnetServiceException if added or removed contain duplicates or if there - * is an authentication error. + * @return a GpodnetUploadChangesResponse. See [GpodnetUploadChangesResponse] for details. + * @throws GpodnetServiceException if added or removed contain duplicates or if there is an authentication error. */ @Throws(GpodnetServiceException::class) override fun uploadSubscriptionChanges(added: List, removed: List): UploadChangesResponse { @@ -220,13 +204,8 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?, } /** - * Returns all subscription changes of a specific device. - * - * - * This method requires authentication. - * - * @param timestamp A timestamp that can be used to receive all changes since a - * specific point in time. + * Returns all subscription changes of a specific device. This method requires authentication. + * @param timestamp A timestamp that can be used to receive all changes since a specific point in time. * @throws GpodnetServiceAuthenticationException If there is an authentication error. */ @Throws(GpodnetServiceException::class) @@ -254,16 +233,10 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?, } /** - * Updates the episode actions - * - * - * This method requires authentication. - * + * Updates the episode actions. This method requires authentication. * @param episodeActions Collection of episode actions. - * @return a GpodnetUploadChangesResponse. See [GpodnetUploadChangesResponse] - * for details. - * @throws GpodnetServiceException if added or removed contain duplicates or if there - * is an authentication error. + * @return a GpodnetUploadChangesResponse. See [GpodnetUploadChangesResponse] for details. + * @throws GpodnetServiceException if added or removed contain duplicates or if there is an authentication error. */ @Throws(SyncServiceException::class) override fun uploadEpisodeActions(episodeActions: List): UploadChangesResponse? { @@ -287,7 +260,7 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?, val list = JSONArray() for (i in from until to) { val episodeAction = episodeActions[i] - val obj = episodeAction!!.writeToJsonObject() + val obj = episodeAction!!.writeToJsonObjectForServer() if (obj != null) { obj.put("device", deviceId) list.put(obj) @@ -312,13 +285,8 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?, } /** - * Returns all subscription changes of a specific device. - * - * - * This method requires authentication. - * - * @param timestamp A timestamp that can be used to receive all changes since a - * specific point in time. + * Returns all subscription changes of a specific device. This method requires authentication. + * @param timestamp A timestamp that can be used to receive all changes since a specific point in time. * @throws SyncServiceException If there is an authentication error. */ @Throws(SyncServiceException::class) @@ -346,9 +314,7 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?, } /** - * Logs in a specific user. This method must be called if any of the methods - * that require authentication is used. - * + * Logs in a specific user. This method must be called if any of the methods that require authentication is used. * @throws IllegalArgumentException If username or password is null. */ @Throws(GpodnetServiceException::class) @@ -508,9 +474,7 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?, open class GpodnetServiceException : SyncServiceException { constructor(message: String?) : super(message) - constructor(e: Throwable?) : super(e) - companion object { private const val serialVersionUID = 1L } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/model/EpisodeAction.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/model/EpisodeAction.kt index 859571e9..9a985ca2 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/model/EpisodeAction.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/model/EpisodeAction.kt @@ -17,14 +17,12 @@ class EpisodeAction private constructor(builder: Builder) { /** * Returns the position (in seconds) at which the client started playback. - * * @return start position (in seconds) */ val started: Int /** * Returns the position (in seconds) at which the client stopped playback. - * * @return stop position (in seconds) */ val position: Int @@ -33,7 +31,6 @@ class EpisodeAction private constructor(builder: Builder) { /** * Returns the total length of the file in seconds. - * * @return total length in seconds */ val total: Int @@ -82,7 +79,6 @@ class EpisodeAction private constructor(builder: Builder) { /** * Returns a JSON object representation of this object. - * * @return JSON object representation, or null if the object is invalid */ fun writeToJsonObject(): JSONObject? { @@ -110,6 +106,32 @@ class EpisodeAction private constructor(builder: Builder) { return obj } + /** + * Returns a JSON object representation of this object. + * @return JSON object representation, or null if the object is invalid + */ + fun writeToJsonObjectForServer(): JSONObject? { + val obj = JSONObject() + try { + obj.putOpt("podcast", this.podcast) + obj.putOpt("episode", this.episode) + obj.putOpt("guid", this.guid) + obj.put("action", this.actionString) + val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US) + formatter.timeZone = TimeZone.getTimeZone("UTC") + if (this.timestamp != null) obj.put("timestamp", formatter.format(this.timestamp)) + if (this.action == Action.PLAY) { + obj.put("started", this.started) + obj.put("position", this.position) + obj.put("total", this.total) + } + } catch (e: JSONException) { + Log.e(TAG, "writeToJSONObject(): " + e.message) + return null + } + return obj + } + override fun toString(): String { return ("EpisodeAction{podcast='$podcast', episode='$episode', guid='$guid', action=$action, timestamp=$timestamp, started=$started, position=$position, total=$total playState=$playState isFavorite=$isFavorite}") } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/nextcloud/NextcloudSyncService.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/nextcloud/NextcloudSyncService.kt index 6bd8969c..6e9b0f8a 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/nextcloud/NextcloudSyncService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/nextcloud/NextcloudSyncService.kt @@ -94,7 +94,7 @@ class NextcloudSyncService(private val httpClient: OkHttpClient, baseHosturl: St val list = JSONArray() for (i in from until to) { val episodeAction = queuedEpisodeActions!![i] - val obj = episodeAction!!.writeToJsonObject() + val obj = episodeAction!!.writeToJsonObjectForServer() if (obj != null) list.put(obj) } val url: HttpUrl.Builder = makeUrl("/index.php/apps/gpoddersync/episode_action/create") diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/queue/SynchronizationQueueStorage.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/queue/SynchronizationQueueStorage.kt index 7b89f82b..230b3613 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/queue/SynchronizationQueueStorage.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/queue/SynchronizationQueueStorage.kt @@ -124,7 +124,7 @@ class SynchronizationQueueStorage(context: Context) { val json = sharedPreferences.getString(QUEUED_EPISODE_ACTIONS, "[]") try { val queue = JSONArray(json) - queue.put(action.writeToJsonObject()) + queue.put(action.writeToJsonObjectForServer()) sharedPreferences.edit().putString(QUEUED_EPISODE_ACTIONS, queue.toString()).apply() } catch (jsonException: JSONException) { jsonException.printStackTrace() 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 e36f15f4..81247cc8 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 @@ -193,7 +193,7 @@ object Queues { return runOnIOScope { curQueue.update() curQueue.episodes.clear() - curQueue.idsBin.addAll(curQueue.episodeIds) + curQueue.idsBinList.addAll(curQueue.episodeIds) curQueue.episodeIds.clear() upsert(curQueue) {} EventFlow.postEvent(FlowEvent.QueueEvent.cleared()) @@ -242,7 +242,9 @@ object Queues { } if (indicesToRemove.isNotEmpty()) { for (i in indicesToRemove.indices.reversed()) { - queue.idsBin.add(qItems[indicesToRemove[i]].id) + val id = qItems[indicesToRemove[i]].id + queue.idsBinList.remove(id) + queue.idsBinList.add(id) qItems.removeAt(indicesToRemove[i]) } queue.update() @@ -265,7 +267,8 @@ object Queues { if (q.id == curQueue.id) continue idsInQueuesToRemove = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet() if (idsInQueuesToRemove.isNotEmpty()) { - q.idsBin.addAll(idsInQueuesToRemove) + q.idsBinList.removeAll(idsInQueuesToRemove) + q.idsBinList.addAll(idsInQueuesToRemove) val qeids = q.episodeIds.minus(idsInQueuesToRemove) upsert(q) { it.episodeIds.clear() @@ -278,7 +281,8 @@ object Queues { val q = curQueue idsInQueuesToRemove = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet() if (idsInQueuesToRemove.isNotEmpty()) { - q.idsBin.addAll(idsInQueuesToRemove) + q.idsBinList.removeAll(idsInQueuesToRemove) + q.idsBinList.addAll(idsInQueuesToRemove) val qeids = q.episodeIds.minus(idsInQueuesToRemove) upsert(q) { it.episodeIds.clear() 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 977373c1..1cdc4008 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 = 13L + private const val SCHEMA_VERSION_NUMBER = 14L private val ioScope = CoroutineScope(Dispatchers.IO) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/PlayQueue.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/PlayQueue.kt index 5c6b8123..711bdacd 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/PlayQueue.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/PlayQueue.kt @@ -24,10 +24,7 @@ class PlayQueue : RealmObject { @Ignore val episodes: MutableList = mutableListOf() - var idsBin: RealmSet = realmSetOf() - -// @Ignore -// val episodesBin: MutableList = mutableListOf() + var idsBinList: RealmList = realmListOf() fun isInQueue(episode: Episode): Boolean { return episodeIds.contains(episode.id) 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 e0918b4b..13cca235 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 @@ -9,12 +9,15 @@ 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 +import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope import ac.mdiq.podcini.storage.database.RealmDB.unmanaged +import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.EpisodeFilter import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeSortOrder import ac.mdiq.podcini.storage.utils.EpisodeUtil +import ac.mdiq.podcini.storage.utils.FileNameGenerator.generateFileName import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectHandler import ac.mdiq.podcini.ui.actions.actionbutton.DeleteActionButton import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler @@ -35,7 +38,9 @@ import ac.mdiq.podcini.util.event.FlowEvent import android.os.Bundle import android.util.Log import android.view.* +import android.widget.Toast import androidx.appcompat.widget.Toolbar +import androidx.core.app.ShareCompat.IntentBuilder import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi @@ -50,6 +55,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.io.File import java.util.* /** @@ -180,11 +186,58 @@ import java.util.* R.id.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance()) R.id.downloads_sort -> DownloadsSortDialog().show(childFragmentManager, "SortDialog") R.id.switch_queue -> SwitchQueueDialog(activity as MainActivity).show() + R.id.reconsile -> reconsile() else -> return false } return true } + private val nameEpisodeMap: MutableMap = mutableMapOf() + private val filesRemoved: MutableList = mutableListOf() + private fun reconsile() { + runOnIOScope { + nameEpisodeMap.clear() + episodes.forEach { e -> + var fileUrl = e.media?.fileUrl ?: return@forEach + fileUrl = fileUrl.substring(fileUrl.lastIndexOf('/') + 1) + Logd(TAG, "reconsile: fileUrl: $fileUrl") + nameEpisodeMap[fileUrl] = e + } + val mediaDir = requireContext().getExternalFilesDir("media") ?: return@runOnIOScope + mediaDir.listFiles()?.forEach { file -> traverse(file, mediaDir) } + Logd(TAG, "reconsile: end, episodes missing file: ${nameEpisodeMap.size}") + if (nameEpisodeMap.isNotEmpty()) { + for (e in nameEpisodeMap.values) { + upsertBlk(e) { + e.media?.setfileUrlOrNull(null) + } + } + } + withContext(Dispatchers.Main) { + Toast.makeText(requireContext(), "Episodes reconsiled: ${nameEpisodeMap.size}\nFiles removed: ${filesRemoved.size}", Toast.LENGTH_LONG) + } + } + } + + private fun traverse(srcFile: File, srcRootDir: File) { + val relativePath = srcFile.absolutePath.substring(srcRootDir.absolutePath.length+1) + if (srcFile.isDirectory) { + Logd(TAG, "traverse folder title: $relativePath") + val dirFiles = srcFile.listFiles() + dirFiles?.forEach { file -> traverse(file, srcFile) } + } else { + Logd(TAG, "traverse: $srcFile") + val episode = nameEpisodeMap.remove(relativePath) + if (episode == null) { + Logd(TAG, "traverse: error: episode not exist in map: $relativePath") + filesRemoved.add(relativePath) + srcFile.delete() + return + } + Logd(TAG, "traverse found episode: ${episode.title}") + } + } + private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) { val newRunningDownloads: MutableSet = HashSet() for (url in event.urls) { 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 0de5963b..fbd20dda 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 @@ -453,7 +453,7 @@ import java.util.* toolbar.menu?.findItem(R.id.queue_lock)?.setChecked(isQueueLocked) toolbar.menu?.findItem(R.id.queue_lock)?.setVisible(!keepSorted) // toolbar.menu.findItem(R.id.switch_queue).setVisible(false) - toolbar.menu.findItem(R.id.refresh_item).setVisible(false) +// toolbar.menu.findItem(R.id.refresh_item).setVisible(false) } @UnstableApi override fun onMenuItemClick(item: MenuItem): Boolean { @@ -462,13 +462,11 @@ import java.util.* R.id.show_bin -> { showBin = !showBin if (showBin) { - item.setIcon(R.drawable.ic_delete) + item.setIcon(R.drawable.playlist_play) speedDialView.addActionItem(addToQueueActionItem) - swipeActions.detach() } else { item.setIcon(R.drawable.trash_can_arrow_up_solid) speedDialView.removeActionItem(addToQueueActionItem) - swipeActions.attachTo(recyclerView) } loadItems(false) } @@ -486,8 +484,9 @@ import java.util.* conDialog.createNewDialog().show() } R.id.clear_bin -> { - curQueue.idsBin.clear() + curQueue.idsBinList.clear() upsertBlk(curQueue) {} + if (showBin) loadItems(false) } R.id.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance()) // R.id.switch_queue -> SwitchQueueDialog(activity as MainActivity).show() @@ -618,8 +617,8 @@ import java.util.* if (queueItems.isEmpty()) emptyView.hide() queueItems.clear() if (showBin) { - queueItems.addAll(realm.copyFromRealm(realm.query(Episode::class, "id IN $0", curQueue.idsBin) - .find().sortedBy { curQueue.idsBin.indexOf(it.id) })) + queueItems.addAll(realm.copyFromRealm(realm.query(Episode::class, "id IN $0", curQueue.idsBinList) + .find().sortedBy { curQueue.idsBinList.indexOf(it.id) })) } else { curQueue.episodes.clear() curQueue.episodes.addAll(realm.copyFromRealm(realm.query(Episode::class, "id IN $0", curQueue.episodeIds) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/RemoteEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/RemoteEpisodesFragment.kt index c07fcd34..0c9d224e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/RemoteEpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/RemoteEpisodesFragment.kt @@ -79,7 +79,7 @@ import kotlin.math.min override fun updateToolbar() { binding.toolbar.menu.findItem(R.id.episodes_sort).setVisible(false) - binding.toolbar.menu.findItem(R.id.refresh_item).setVisible(false) +// binding.toolbar.menu.findItem(R.id.refresh_item).setVisible(false) binding.toolbar.menu.findItem(R.id.action_search).setVisible(false) binding.toolbar.menu.findItem(R.id.action_favorites).setVisible(false) binding.toolbar.menu.findItem(R.id.filter_items).setVisible(false) diff --git a/app/src/main/res/menu/downloads_completed.xml b/app/src/main/res/menu/downloads_completed.xml index 53d8876e..8c2be5e8 100644 --- a/app/src/main/res/menu/downloads_completed.xml +++ b/app/src/main/res/menu/downloads_completed.xml @@ -17,10 +17,16 @@ custom:showAsAction="always" /> + + + + + + + diff --git a/app/src/main/res/menu/episodes.xml b/app/src/main/res/menu/episodes.xml index cad2cfb4..a4670bee 100644 --- a/app/src/main/res/menu/episodes.xml +++ b/app/src/main/res/menu/episodes.xml @@ -29,11 +29,11 @@ android:title="@string/sort" custom:showAsAction="ifRoom" /> - + + + + + @@ -14,11 +14,11 @@ custom:showAsAction="ifRoom" android:title="@string/search_label"/> - + + + + + Error An error occurred: Refresh + Reconsile Chapters No chapters Duration: %1$s diff --git a/changelog.md b/changelog.md index 93b4d853..eb721944 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,11 @@ +# 6.1.6 + +* enabled swipe actions in Queue bin view (same actions as in Queue) +* both icons of show bin and back to queue are changed to be more intuitive +* bin items are sorted based on the update time +* added a reconsile feature in Downloads view that verifies episodes' download status with media files in system and performs cleanup +* likely fixed syncing with nextcloud and gpoddernet servers. + # 6.1.5 * minor adjustments on FeedInfo page, especially for handling long feed title diff --git a/fastlane/metadata/android/en-US/changelogs/3020220.txt b/fastlane/metadata/android/en-US/changelogs/3020220.txt new file mode 100644 index 00000000..8e3a843f --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020220.txt @@ -0,0 +1,8 @@ + +Version 6.1.6 brings several changes: + +* enabled swipe actions in Queue bin view (same actions as in Queue) +* both icons of show bin and back to queue are changed to be more intuitive +* bin items are sorted based on the update time +* added a reconsile feature in Downloads view that verifies episodes' download status with media files in system and performs cleanup +* likely fixed syncing with nextcloud and gpoddernet servers. diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1_drawer.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/1_drawer.jpg index fa57ef74..e554563f 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1_drawer.jpg and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1_drawer.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2_setting1.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/2_setting1.jpg index 0797edc4..b3b781bf 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2_setting1.jpg and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2_setting1.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2_setting2.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/2_setting2.jpg new file mode 100644 index 00000000..d7161f52 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2_setting2.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2_setting3.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/2_setting3.jpg new file mode 100644 index 00000000..4a656bf5 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2_setting3.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3_subscriptions.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/3_subscriptions.jpg index 255d2e46..c16a867b 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3_subscriptions.jpg and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3_subscriptions.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3_subscriptions1.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/3_subscriptions1.jpg new file mode 100644 index 00000000..b01ab744 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3_subscriptions1.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3_subscriptions2.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/3_subscriptions2.jpg new file mode 100644 index 00000000..f427b6b4 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3_subscriptions2.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4_queue.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/4_queue.jpg index f6d35a44..4445c0a4 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/4_queue.jpg and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4_queue.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4_queue1.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/4_queue1.jpg new file mode 100644 index 00000000..a7487525 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4_queue1.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5_podcast_0.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/5_podcast_0.jpg index 5ac3726b..a92cc532 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/5_podcast_0.jpg and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5_podcast_0.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5_podcast_1.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/5_podcast_1.jpg index 7d4f5ac7..d93d1b09 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/5_podcast_1.jpg and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5_podcast_1.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5_podcast_setting1.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/5_podcast_setting1.jpg new file mode 100644 index 00000000..f60287ad Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5_podcast_setting1.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6_episode.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/6_episode.jpg index 900a7f5a..8975c9a2 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/6_episode.jpg and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6_episode.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6_player_details.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/6_player_details.jpg new file mode 100644 index 00000000..483b13bf Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6_player_details.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/7_speed.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/7_speed.jpg index ffb57fe7..18258de1 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/7_speed.jpg and b/fastlane/metadata/android/en-US/images/phoneScreenshots/7_speed.jpg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/9_online_feed_info.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/9_online_feed_info.jpg index 88cc8935..7331d541 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/9_online_feed_info.jpg and b/fastlane/metadata/android/en-US/images/phoneScreenshots/9_online_feed_info.jpg differ