6.1.6 commit
|
@ -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.
|
||||
|
||||
|
|
|
@ -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 = ""
|
||||
|
|
|
@ -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<GpodnetDevice>
|
||||
/**
|
||||
* 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<String>, removed: List<String>): 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<EpisodeAction>): 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
|
||||
}
|
||||
|
|
|
@ -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}")
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -24,10 +24,7 @@ class PlayQueue : RealmObject {
|
|||
@Ignore
|
||||
val episodes: MutableList<Episode> = mutableListOf()
|
||||
|
||||
var idsBin: RealmSet<Long> = realmSetOf()
|
||||
|
||||
// @Ignore
|
||||
// val episodesBin: MutableList<Episode> = mutableListOf()
|
||||
var idsBinList: RealmList<Long> = realmListOf()
|
||||
|
||||
fun isInQueue(episode: Episode): Boolean {
|
||||
return episodeIds.contains(episode.id)
|
||||
|
|
|
@ -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<String, Episode> = mutableMapOf()
|
||||
private val filesRemoved: MutableList<String> = 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<String> = HashSet()
|
||||
for (url in event.urls) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -17,10 +17,16 @@
|
|||
custom:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/refresh_item"
|
||||
android:title="@string/refresh_label"
|
||||
android:menuCategory="container"
|
||||
android:id="@+id/reconsile"
|
||||
android:title="@string/reconsile_label"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<!-- <item-->
|
||||
<!-- android:id="@+id/refresh_item"-->
|
||||
<!-- android:title="@string/refresh_label"-->
|
||||
<!-- android:menuCategory="container"-->
|
||||
<!-- app:showAsAction="never" />-->
|
||||
|
||||
<item
|
||||
android:id="@+id/downloads_sort"
|
||||
android:title="@string/sort" />
|
||||
|
|
|
@ -29,11 +29,11 @@
|
|||
android:title="@string/sort"
|
||||
custom:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/refresh_item"
|
||||
android:title="@string/refresh_label"
|
||||
android:menuCategory="container"
|
||||
custom:showAsAction="never" />
|
||||
<!-- <item-->
|
||||
<!-- android:id="@+id/refresh_item"-->
|
||||
<!-- android:title="@string/refresh_label"-->
|
||||
<!-- android:menuCategory="container"-->
|
||||
<!-- custom:showAsAction="never" />-->
|
||||
|
||||
<item
|
||||
android:id="@+id/switch_queue"
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<item
|
||||
android:id="@+id/show_bin"
|
||||
android:icon="@drawable/trash_can_arrow_up_solid"
|
||||
android:icon="@drawable/ic_history"
|
||||
custom:showAsAction="ifRoom"
|
||||
android:title="@string/show_bin_label"/>
|
||||
|
||||
|
@ -14,11 +14,11 @@
|
|||
custom:showAsAction="ifRoom"
|
||||
android:title="@string/search_label"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/refresh_item"
|
||||
android:title="@string/refresh_label"
|
||||
android:menuCategory="container"
|
||||
custom:showAsAction="never" />
|
||||
<!-- <item-->
|
||||
<!-- android:id="@+id/refresh_item"-->
|
||||
<!-- android:title="@string/refresh_label"-->
|
||||
<!-- android:menuCategory="container"-->
|
||||
<!-- custom:showAsAction="never" />-->
|
||||
|
||||
<item
|
||||
android:id="@+id/queue_lock"
|
||||
|
|
|
@ -119,6 +119,7 @@
|
|||
<string name="error_label">Error</string>
|
||||
<string name="error_msg_prefix">An error occurred:</string>
|
||||
<string name="refresh_label">Refresh</string>
|
||||
<string name="reconsile_label">Reconsile</string>
|
||||
<string name="chapters_label">Chapters</string>
|
||||
<string name="no_chapters_label">No chapters</string>
|
||||
<string name="chapter_duration">Duration: %1$s</string>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 102 KiB |
Before Width: | Height: | Size: 177 KiB After Width: | Height: | Size: 196 KiB |
After Width: | Height: | Size: 147 KiB |
After Width: | Height: | Size: 156 KiB |
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 275 KiB |
After Width: | Height: | Size: 169 KiB |
After Width: | Height: | Size: 218 KiB |
Before Width: | Height: | Size: 248 KiB After Width: | Height: | Size: 493 KiB |
After Width: | Height: | Size: 194 KiB |
Before Width: | Height: | Size: 286 KiB After Width: | Height: | Size: 474 KiB |
Before Width: | Height: | Size: 258 KiB After Width: | Height: | Size: 266 KiB |
After Width: | Height: | Size: 215 KiB |
Before Width: | Height: | Size: 287 KiB After Width: | Height: | Size: 328 KiB |
After Width: | Height: | Size: 256 KiB |
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 148 KiB |
Before Width: | Height: | Size: 338 KiB After Width: | Height: | Size: 272 KiB |