6.0.13 commit

This commit is contained in:
Xilin Jia 2024-07-15 18:55:00 +01:00
parent b4b850baba
commit 91423f9267
84 changed files with 832 additions and 817 deletions

View File

@ -126,8 +126,8 @@ android {
buildConfig true buildConfig true
} }
defaultConfig { defaultConfig {
versionCode 3020212 versionCode 3020213
versionName "6.0.12" versionName "6.0.13"
applicationId "ac.mdiq.podcini.R" applicationId "ac.mdiq.podcini.R"
def commit = "" def commit = ""

View File

@ -1,9 +1,9 @@
package de.test.podcini.storage package de.test.podcini.storage
import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileStreaming
import ac.mdiq.podcini.playback.PlaybackServiceStarter import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.preferences.UserPreferences.isAllowMobileStreaming import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isFollowQueue
import ac.mdiq.podcini.preferences.UserPreferences.isFollowQueue
import ac.mdiq.podcini.storage.algorithms.AutoDownloads import ac.mdiq.podcini.storage.algorithms.AutoDownloads
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.downloadAlgorithm import ac.mdiq.podcini.storage.algorithms.AutoDownloads.downloadAlgorithm
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode

View File

@ -11,6 +11,12 @@ import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import androidx.test.rule.ActivityTestRule import androidx.test.rule.ActivityTestRule
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.playback.service.PlaybackService
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isFollowQueue
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isPauseOnHeadsetDisconnect
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isPersistNotify
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isUnpauseOnBluetoothReconnect
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isUnpauseOnHeadsetReconnect
import ac.mdiq.podcini.ui.activity.PreferenceActivity import ac.mdiq.podcini.ui.activity.PreferenceActivity
import ac.mdiq.podcini.storage.algorithms.AutoCleanups.APCleanupAlgorithm import ac.mdiq.podcini.storage.algorithms.AutoCleanups.APCleanupAlgorithm
import ac.mdiq.podcini.storage.algorithms.AutoCleanups.APNullCleanupAlgorithm import ac.mdiq.podcini.storage.algorithms.AutoCleanups.APNullCleanupAlgorithm
@ -18,8 +24,6 @@ import ac.mdiq.podcini.storage.algorithms.AutoCleanups.APQueueCleanupAlgorithm
import ac.mdiq.podcini.storage.algorithms.AutoCleanups.build import ac.mdiq.podcini.storage.algorithms.AutoCleanups.build
import ac.mdiq.podcini.storage.algorithms.AutoCleanups.ExceptFavoriteCleanupAlgorithm import ac.mdiq.podcini.storage.algorithms.AutoCleanups.ExceptFavoriteCleanupAlgorithm
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.EnqueueLocation
import ac.mdiq.podcini.preferences.UserPreferences.enqueueLocation
import ac.mdiq.podcini.preferences.UserPreferences.episodeCacheSize import ac.mdiq.podcini.preferences.UserPreferences.episodeCacheSize
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
import ac.mdiq.podcini.preferences.UserPreferences.init import ac.mdiq.podcini.preferences.UserPreferences.init
@ -27,17 +31,14 @@ import ac.mdiq.podcini.preferences.UserPreferences.isAutoDelete
import ac.mdiq.podcini.preferences.UserPreferences.isAutoDeleteLocal import ac.mdiq.podcini.preferences.UserPreferences.isAutoDeleteLocal
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownloadOnBattery import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownloadOnBattery
import ac.mdiq.podcini.preferences.UserPreferences.isFollowQueue
import ac.mdiq.podcini.preferences.UserPreferences.isPauseOnHeadsetDisconnect
import ac.mdiq.podcini.preferences.UserPreferences.isPersistNotify
import ac.mdiq.podcini.preferences.UserPreferences.isUnpauseOnBluetoothReconnect
import ac.mdiq.podcini.preferences.UserPreferences.isUnpauseOnHeadsetReconnect
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue
import ac.mdiq.podcini.preferences.UserPreferences.shouldPauseForFocusLoss import ac.mdiq.podcini.preferences.UserPreferences.shouldPauseForFocusLoss
import ac.mdiq.podcini.preferences.UserPreferences.showNextChapterOnFullNotification import ac.mdiq.podcini.preferences.UserPreferences.showNextChapterOnFullNotification
import ac.mdiq.podcini.preferences.UserPreferences.showPlaybackSpeedOnFullNotification import ac.mdiq.podcini.preferences.UserPreferences.showPlaybackSpeedOnFullNotification
import ac.mdiq.podcini.preferences.UserPreferences.showSkipOnFullNotification import ac.mdiq.podcini.preferences.UserPreferences.showSkipOnFullNotification
import ac.mdiq.podcini.storage.database.Queues
import ac.mdiq.podcini.storage.database.Queues.enqueueLocation
import de.test.podcini.EspressoTestUtils import de.test.podcini.EspressoTestUtils
import org.awaitility.Awaitility import org.awaitility.Awaitility
import org.junit.Assert import org.junit.Assert
@ -104,13 +105,13 @@ class PreferencesTest {
@Test @Test
fun testEnqueueLocation() { fun testEnqueueLocation() {
EspressoTestUtils.clickPreference(R.string.playback_pref) EspressoTestUtils.clickPreference(R.string.playback_pref)
doTestEnqueueLocation(R.string.enqueue_location_after_current, EnqueueLocation.AFTER_CURRENTLY_PLAYING) doTestEnqueueLocation(R.string.enqueue_location_after_current, Queues.EnqueueLocation.AFTER_CURRENTLY_PLAYING)
doTestEnqueueLocation(R.string.enqueue_location_front, EnqueueLocation.FRONT) doTestEnqueueLocation(R.string.enqueue_location_front, Queues.EnqueueLocation.FRONT)
doTestEnqueueLocation(R.string.enqueue_location_back, EnqueueLocation.BACK) doTestEnqueueLocation(R.string.enqueue_location_back, Queues.EnqueueLocation.BACK)
doTestEnqueueLocation(R.string.enqueue_location_random, EnqueueLocation.RANDOM) doTestEnqueueLocation(R.string.enqueue_location_random, Queues.EnqueueLocation.RANDOM)
} }
private fun doTestEnqueueLocation(@StringRes optionResId: Int, expected: EnqueueLocation) { private fun doTestEnqueueLocation(@StringRes optionResId: Int, expected: Queues.EnqueueLocation) {
EspressoTestUtils.clickPreference(R.string.pref_enqueue_location_title) EspressoTestUtils.clickPreference(R.string.pref_enqueue_location_title)
Espresso.onView(ViewMatchers.withText(optionResId)).perform(ViewActions.click()) Espresso.onView(ViewMatchers.withText(optionResId)).perform(ViewActions.click())
Awaitility.await().atMost(1000, TimeUnit.MILLISECONDS) Awaitility.await().atMost(1000, TimeUnit.MILLISECONDS)
@ -138,7 +139,7 @@ class PreferencesTest {
Espresso.onView(ViewMatchers.withText(R.string.pref_pauseOnHeadsetDisconnect_title)) Espresso.onView(ViewMatchers.withText(R.string.pref_pauseOnHeadsetDisconnect_title))
.perform(ViewActions.click()) .perform(ViewActions.click())
Awaitility.await().atMost(1000, TimeUnit.MILLISECONDS) Awaitility.await().atMost(1000, TimeUnit.MILLISECONDS)
.until(UserPreferences::isPauseOnHeadsetDisconnect) .until(PlaybackService::isPauseOnHeadsetDisconnect)
} }
val unpauseOnHeadsetReconnect = isUnpauseOnHeadsetReconnect val unpauseOnHeadsetReconnect = isUnpauseOnHeadsetReconnect
Espresso.onView(ViewMatchers.withText(R.string.pref_unpauseOnHeadsetReconnect_title)) Espresso.onView(ViewMatchers.withText(R.string.pref_unpauseOnHeadsetReconnect_title))
@ -158,7 +159,7 @@ class PreferencesTest {
Espresso.onView(ViewMatchers.withText(R.string.pref_pauseOnHeadsetDisconnect_title)) Espresso.onView(ViewMatchers.withText(R.string.pref_pauseOnHeadsetDisconnect_title))
.perform(ViewActions.click()) .perform(ViewActions.click())
Awaitility.await().atMost(1000, TimeUnit.MILLISECONDS) Awaitility.await().atMost(1000, TimeUnit.MILLISECONDS)
.until(UserPreferences::isPauseOnHeadsetDisconnect) .until(PlaybackService::isPauseOnHeadsetDisconnect)
} }
val unpauseOnBluetoothReconnect = isUnpauseOnBluetoothReconnect val unpauseOnBluetoothReconnect = isUnpauseOnBluetoothReconnect
Espresso.onView(ViewMatchers.withText(R.string.pref_unpauseOnBluetoothReconnect_title)) Espresso.onView(ViewMatchers.withText(R.string.pref_unpauseOnBluetoothReconnect_title))

View File

@ -27,7 +27,6 @@ class PodciniApp : Application() {
ClientConfig.applicationCallbacks = ApplicationCallbacksImpl() ClientConfig.applicationCallbacks = ApplicationCallbacksImpl()
Thread.setDefaultUncaughtExceptionHandler(CrashReportWriter()) Thread.setDefaultUncaughtExceptionHandler(CrashReportWriter())
// RxJavaErrorHandlerSetup.setupRxJavaErrorHandler()
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
val builder: StrictMode.VmPolicy.Builder = StrictMode.VmPolicy.Builder() val builder: StrictMode.VmPolicy.Builder = StrictMode.VmPolicy.Builder()

View File

@ -1,8 +1,5 @@
package ac.mdiq.podcini.net.download package ac.mdiq.podcini.net.download
import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction
import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction.NEVER
/** Utility class for Download Errors. */ /** Utility class for Download Errors. */
/** Get machine-readable code. */ /** Get machine-readable code. */
enum class DownloadError(@JvmField val code: Int) { enum class DownloadError(@JvmField val code: Int) {

View File

@ -1,15 +1,14 @@
package ac.mdiq.podcini.net.download.service package ac.mdiq.podcini.net.download.service
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.utils.FileNameGenerator import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.utils.FilesUtils.feedfilePath
import ac.mdiq.podcini.storage.utils.FilesUtils.findUnusedFile
import ac.mdiq.podcini.storage.utils.FilesUtils.getFeedfileName
import ac.mdiq.podcini.storage.utils.FilesUtils.getMediafilePath
import ac.mdiq.podcini.storage.utils.FilesUtils.getMediafilename
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import android.webkit.URLUtil
import io.realm.kotlin.ext.isManaged
import org.apache.commons.io.FilenameUtils
import java.io.File import java.io.File
/** /**
@ -17,8 +16,6 @@ import java.io.File
*/ */
object DownloadRequestCreator { object DownloadRequestCreator {
private val TAG: String = DownloadRequestCreator::class.simpleName ?: "Anonymous" private val TAG: String = DownloadRequestCreator::class.simpleName ?: "Anonymous"
private const val FEED_DOWNLOADPATH = "cache/"
private const val MEDIA_DOWNLOADPATH = "media/"
@JvmStatic @JvmStatic
fun create(feed: Feed): DownloadRequest.Builder { fun create(feed: Feed): DownloadRequest.Builder {
@ -53,83 +50,4 @@ object DownloadRequestCreator {
return DownloadRequest.Builder(dest.toString(), media).withAuthentication(username, password) return DownloadRequest.Builder(dest.toString(), media).withAuthentication(username, password)
} }
private fun findUnusedFile(dest: File): File? {
// find different name
var newDest: File? = null
for (i in 1 until Int.MAX_VALUE) {
val newName = (FilenameUtils.getBaseName(dest.name) + "-" + i + FilenameUtils.EXTENSION_SEPARATOR + FilenameUtils.getExtension(dest.name))
Logd(TAG, "Testing filename $newName")
newDest = File(dest.parent, newName)
if (!newDest.exists()) {
Logd(TAG, "File doesn't exist yet. Using $newName")
break
}
}
return newDest
}
private val feedfilePath: String
get() = UserPreferences.getDataFolder(FEED_DOWNLOADPATH).toString() + "/"
private fun getFeedfileName(feed: Feed): String {
var filename = feed.downloadUrl
if (!feed.title.isNullOrEmpty()) filename = feed.title
if (filename == null) return ""
return "feed-" + FileNameGenerator.generateFileName(filename) + feed.id
}
private fun getMediafilePath(media: EpisodeMedia): String {
val item = media.episode ?: return ""
Logd(TAG, "item managed: ${item.isManaged()}")
val title = item.feed?.title?:return ""
val mediaPath = (MEDIA_DOWNLOADPATH + FileNameGenerator.generateFileName(title))
return UserPreferences.getDataFolder(mediaPath).toString() + "/"
}
private fun getMediafilename(media: EpisodeMedia): String {
var titleBaseFilename = ""
// Try to generate the filename by the item title
if (media.episode?.title != null) {
val title = media.episode!!.title!!
titleBaseFilename = FileNameGenerator.generateFileName(title)
}
val urlBaseFilename = URLUtil.guessFileName(media.downloadUrl, null, media.mimeType)
var baseFilename: String
baseFilename = if (titleBaseFilename != "") titleBaseFilename else urlBaseFilename
val filenameMaxLength = 220
if (baseFilename.length > filenameMaxLength) baseFilename = baseFilename.substring(0, filenameMaxLength)
return (baseFilename + FilenameUtils.EXTENSION_SEPARATOR + media.id + FilenameUtils.EXTENSION_SEPARATOR + FilenameUtils.getExtension(urlBaseFilename))
}
fun getMediafilePath(item: Episode): String {
val title = item.feed?.title?:return ""
val mediaPath = (MEDIA_DOWNLOADPATH + FileNameGenerator.generateFileName(title))
return UserPreferences.getDataFolder(mediaPath).toString() + "/"
}
fun getMediafilename(item: Episode): String {
var titleBaseFilename = ""
// Try to generate the filename by the item title
if (item.title != null) {
val title = item.title!!
titleBaseFilename = FileNameGenerator.generateFileName(title)
}
// val urlBaseFilename = URLUtil.guessFileName(media.download_url, null, media.mime_type)
var baseFilename: String
baseFilename = if (titleBaseFilename != "") titleBaseFilename else "NoTitle"
val filenameMaxLength = 220
if (baseFilename.length > filenameMaxLength) baseFilename = baseFilename.substring(0, filenameMaxLength)
return (baseFilename + FilenameUtils.EXTENSION_SEPARATOR + "noid" + FilenameUtils.EXTENSION_SEPARATOR + "wav")
}
} }

View File

@ -7,7 +7,10 @@ import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
import ac.mdiq.podcini.net.sync.model.EpisodeAction import ac.mdiq.podcini.net.sync.model.EpisodeAction
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink 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
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.Episodes
import ac.mdiq.podcini.storage.database.LogsAndStats import ac.mdiq.podcini.storage.database.LogsAndStats
import ac.mdiq.podcini.storage.database.Queues import ac.mdiq.podcini.storage.database.Queues
@ -102,29 +105,35 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
private val constraints: Constraints private val constraints: Constraints
get() { get() {
val constraints = Builder() val constraints = Builder()
if (UserPreferences.isAllowMobileEpisodeDownload) constraints.setRequiredNetworkType(NetworkType.CONNECTED) if (isAllowMobileEpisodeDownload) constraints.setRequiredNetworkType(NetworkType.CONNECTED)
else constraints.setRequiredNetworkType(NetworkType.UNMETERED) else constraints.setRequiredNetworkType(NetworkType.UNMETERED)
return constraints.build() return constraints.build()
} }
@OptIn(UnstableApi::class) private fun getRequest(item: Episode): OneTimeWorkRequest.Builder { @OptIn(UnstableApi::class)
private fun getRequest(item: Episode): OneTimeWorkRequest.Builder {
Logd(TAG, "starting getRequest") Logd(TAG, "starting getRequest")
val workRequest: OneTimeWorkRequest.Builder = OneTimeWorkRequest.Builder(EpisodeDownloadWorker::class.java) val workRequest: OneTimeWorkRequest.Builder = OneTimeWorkRequest.Builder(EpisodeDownloadWorker::class.java)
.setInitialDelay(0L, TimeUnit.MILLISECONDS) .setInitialDelay(0L, TimeUnit.MILLISECONDS)
.addTag(WORK_TAG) .addTag(WORK_TAG)
.addTag(WORK_TAG_EPISODE_URL + item.media!!.downloadUrl) .addTag(WORK_TAG_EPISODE_URL + item.media!!.downloadUrl)
if (UserPreferences.enqueueDownloadedEpisodes()) { if (enqueueDownloadedEpisodes()) {
runBlocking { Queues.addToQueueSync(false, item) } runBlocking { Queues.addToQueueSync(false, item) }
workRequest.addTag(WORK_DATA_WAS_QUEUED) workRequest.addTag(WORK_DATA_WAS_QUEUED)
} }
workRequest.setInputData(Data.Builder().putLong(WORK_DATA_MEDIA_ID, item.media!!.id).build()) workRequest.setInputData(Data.Builder().putLong(WORK_DATA_MEDIA_ID, item.media!!.id).build())
return workRequest return workRequest
} }
private fun enqueueDownloadedEpisodes(): Boolean {
return appPrefs.getBoolean(PREF_ENQUEUE_DOWNLOADED, true)
}
} }
class EpisodeDownloadWorker(context: Context, params: WorkerParameters) : Worker(context, params) { class EpisodeDownloadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
private var downloader: Downloader? = null private var downloader: Downloader? = null
private val isLastRunAttempt: Boolean
get() = runAttemptCount >= 2
@UnstableApi @UnstableApi
override fun doWork(): Result { override fun doWork(): Result {
@ -136,7 +145,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
Log.e(TAG, "media is null for mediaId: $mediaId") Log.e(TAG, "media is null for mediaId: $mediaId")
return Result.failure() return Result.failure()
} }
val request = create(media).build() val request = create(media).build()
val progressUpdaterThread: Thread = object : Thread() { val progressUpdaterThread: Thread = object : Thread() {
override fun run() { override fun run() {
@ -168,7 +176,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
} }
if (result == Result.failure() && downloader?.downloadRequest?.destination != null) if (result == Result.failure() && downloader?.downloadRequest?.destination != null)
FileUtils.deleteQuietly(File(downloader!!.downloadRequest.destination!!)) FileUtils.deleteQuietly(File(downloader!!.downloadRequest.destination!!))
progressUpdaterThread.interrupt() progressUpdaterThread.interrupt()
try { try {
progressUpdaterThread.join() progressUpdaterThread.join()
@ -185,17 +192,15 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
Logd(TAG, "Worker for " + media.downloadUrl + " returned.") Logd(TAG, "Worker for " + media.downloadUrl + " returned.")
return result return result
} }
override fun onStopped() { override fun onStopped() {
super.onStopped() super.onStopped()
downloader?.cancel() downloader?.cancel()
} }
override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> { override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> {
return Futures.immediateFuture(ForegroundInfo(R.id.notification_downloading, generateProgressNotification())) return Futures.immediateFuture(ForegroundInfo(R.id.notification_downloading, generateProgressNotification()))
} }
@OptIn(UnstableApi::class)
@OptIn(UnstableApi::class) private fun performDownload(media: EpisodeMedia, request: DownloadRequest): Result { private fun performDownload(media: EpisodeMedia, request: DownloadRequest): Result {
Logd(TAG, "starting performDownload") Logd(TAG, "starting performDownload")
val dest = File(request.destination) val dest = File(request.destination)
if (!dest.exists()) { if (!dest.exists()) {
@ -205,7 +210,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
Log.e(TAG, "performDownload Unable to create file") Log.e(TAG, "performDownload Unable to create file")
} }
} }
if (dest.exists()) { if (dest.exists()) {
try { try {
media.setfileUrlOrNull(request.destination) media.setfileUrlOrNull(request.destination)
@ -214,13 +218,11 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
Log.e(TAG, "performDownload Exception in writeFileUrl: " + e.message) Log.e(TAG, "performDownload Exception in writeFileUrl: " + e.message)
} }
} }
downloader = DefaultDownloaderFactory().create(request) downloader = DefaultDownloaderFactory().create(request)
if (downloader == null) { if (downloader == null) {
Log.e(TAG, "performDownload Unable to create downloader") Log.e(TAG, "performDownload Unable to create downloader")
return Result.failure() return Result.failure()
} }
try { try {
downloader!!.call() downloader!!.call()
} catch (e: Exception) { } catch (e: Exception) {
@ -229,10 +231,8 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
sendErrorNotification(request.title?:"") sendErrorNotification(request.title?:"")
return Result.failure() return Result.failure()
} }
// This also happens when the worker was preempted, not just when the user cancelled it // This also happens when the worker was preempted, not just when the user cancelled it
if (downloader!!.cancelled) return Result.success() if (downloader!!.cancelled) return Result.success()
val status = downloader!!.result val status = downloader!!.result
if (status.isSuccessful) { if (status.isSuccessful) {
val handler = MediaDownloadedHandler(applicationContext, downloader!!.result, request) val handler = MediaDownloadedHandler(applicationContext, downloader!!.result, request)
@ -240,14 +240,12 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
LogsAndStats.addDownloadStatus(handler.updatedStatus) LogsAndStats.addDownloadStatus(handler.updatedStatus)
return Result.success() return Result.success()
} }
if (status.reason == DownloadError.ERROR_HTTP_DATA_ERROR && status.reasonDetailed.toInt() == 416) { if (status.reason == DownloadError.ERROR_HTTP_DATA_ERROR && status.reasonDetailed.toInt() == 416) {
Logd(TAG, "Requested invalid range, restarting download from the beginning") Logd(TAG, "Requested invalid range, restarting download from the beginning")
if (downloader?.downloadRequest?.destination != null) FileUtils.deleteQuietly(File(downloader!!.downloadRequest.destination!!)) if (downloader?.downloadRequest?.destination != null) FileUtils.deleteQuietly(File(downloader!!.downloadRequest.destination!!))
sendMessage(request.title?:"", false) sendMessage(request.title?:"", false)
return retry3times() return retry3times()
} }
Log.e(TAG, "Download failed ${request.title} ${status.reason}") Log.e(TAG, "Download failed ${request.title} ${status.reason}")
LogsAndStats.addDownloadStatus(status) LogsAndStats.addDownloadStatus(status)
if (status.reason == DownloadError.ERROR_FORBIDDEN || status.reason == DownloadError.ERROR_NOT_FOUND if (status.reason == DownloadError.ERROR_FORBIDDEN || status.reason == DownloadError.ERROR_NOT_FOUND
@ -260,7 +258,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
sendMessage(request.title?:"", false) sendMessage(request.title?:"", false)
return retry3times() return retry3times()
} }
private fun retry3times(): Result { private fun retry3times(): Result {
if (isLastRunAttempt) { if (isLastRunAttempt) {
Log.e(TAG, "retry3times failure on isLastRunAttempt") Log.e(TAG, "retry3times failure on isLastRunAttempt")
@ -268,10 +265,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
return Result.failure() return Result.failure()
} else return Result.retry() } else return Result.retry()
} }
private val isLastRunAttempt: Boolean
get() = runAttemptCount >= 2
private fun sendMessage(episodeTitle: String, isImmediateFail: Boolean) { private fun sendMessage(episodeTitle: String, isImmediateFail: Boolean) {
var episodeTitle = episodeTitle var episodeTitle = episodeTitle
val retrying = !isLastRunAttempt && !isImmediateFail val retrying = !isLastRunAttempt && !isImmediateFail
@ -282,26 +275,22 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
episodeTitle), { ctx: Context -> MainActivityStarter(ctx).withDownloadLogsOpen().start() }, applicationContext.getString( episodeTitle), { ctx: Context -> MainActivityStarter(ctx).withDownloadLogsOpen().start() }, applicationContext.getString(
R.string.download_error_details))) R.string.download_error_details)))
} }
private fun getDownloadLogsIntent(context: Context): PendingIntent { private fun getDownloadLogsIntent(context: Context): PendingIntent {
val intent = MainActivityStarter(context).withDownloadLogsOpen().getIntent() val intent = MainActivityStarter(context).withDownloadLogsOpen().getIntent()
return PendingIntent.getActivity(context, R.id.pending_intent_download_service_report, intent, return PendingIntent.getActivity(context, R.id.pending_intent_download_service_report, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
} }
private fun getDownloadsIntent(context: Context): PendingIntent { private fun getDownloadsIntent(context: Context): PendingIntent {
val intent = MainActivityStarter(context).withFragmentLoaded("DownloadsFragment").getIntent() val intent = MainActivityStarter(context).withFragmentLoaded("DownloadsFragment").getIntent()
return PendingIntent.getActivity(context, R.id.pending_intent_download_service_notification, intent, return PendingIntent.getActivity(context, R.id.pending_intent_download_service_notification, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
} }
private fun sendErrorNotification(title: String) { private fun sendErrorNotification(title: String) {
// TODO: need to get number of subscribers in SharedFlow // TODO: need to get number of subscribers in SharedFlow
// if (EventBus.getDefault().hasSubscriberForEvent(FlowEvent.MessageEvent::class.java)) { // if (EventBus.getDefault().hasSubscriberForEvent(FlowEvent.MessageEvent::class.java)) {
// sendMessage(title, false) // sendMessage(title, false)
// return // return
// } // }
val builder = NotificationCompat.Builder(applicationContext, NotificationUtils.CHANNEL_ID_DOWNLOAD_ERROR) val builder = NotificationCompat.Builder(applicationContext, NotificationUtils.CHANNEL_ID_DOWNLOAD_ERROR)
builder.setTicker(applicationContext.getString(R.string.download_report_title)) builder.setTicker(applicationContext.getString(R.string.download_report_title))
.setContentTitle(applicationContext.getString(R.string.download_report_title)) .setContentTitle(applicationContext.getString(R.string.download_report_title))
@ -313,7 +302,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
val nm = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val nm = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
nm.notify(R.id.notification_download_report, builder.build()) nm.notify(R.id.notification_download_report, builder.build())
} }
private fun generateProgressNotification(): Notification { private fun generateProgressNotification(): Notification {
val bigTextB = StringBuilder() val bigTextB = StringBuilder()
var progressCopy: Map<String, Int> var progressCopy: Map<String, Int>
@ -326,7 +314,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
val bigText = bigTextB.toString().trim { it <= ' ' } val bigText = bigTextB.toString().trim { it <= ' ' }
val contentText = if (progressCopy.size == 1) bigText val contentText = if (progressCopy.size == 1) bigText
else applicationContext.resources.getQuantityString(R.plurals.downloads_left, progressCopy.size, progressCopy.size) 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)
builder.setTicker(applicationContext.getString(R.string.download_notification_title_episodes)) builder.setTicker(applicationContext.getString(R.string.download_notification_title_episodes))
.setContentTitle(applicationContext.getString(R.string.download_notification_title_episodes)) .setContentTitle(applicationContext.getString(R.string.download_notification_title_episodes))
@ -344,7 +331,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
} }
class MediaDownloadedHandler(private val context: Context, var updatedStatus: DownloadResult, private val request: DownloadRequest) : Runnable { class MediaDownloadedHandler(private val context: Context, var updatedStatus: DownloadResult, private val request: DownloadRequest) : Runnable {
@UnstableApi override fun run() { @UnstableApi override fun run() {
val media = Episodes.getEpisodeMedia(request.feedfileId) val media = Episodes.getEpisodeMedia(request.feedfileId)
if (media == null) { if (media == null) {
@ -358,16 +344,11 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
Logd(TAG, "media.episode.isNew: ${media.episode?.isNew} ${media.episode?.playState}") Logd(TAG, "media.episode.isNew: ${media.episode?.isNew} ${media.episode?.playState}")
media.setfileUrlOrNull(request.destination) media.setfileUrlOrNull(request.destination)
if (request.destination != null) media.size = File(request.destination).length() if (request.destination != null) media.size = File(request.destination).length()
media.checkEmbeddedPicture() // enforce check media.checkEmbeddedPicture() // enforce check
// check if file has chapters // check if file has chapters
if (media.episode != null && media.episode!!.chapters.isEmpty()) if (media.episode != null && media.episode!!.chapters.isEmpty()) media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context))
media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context))
if (media.episode?.podcastIndexChapterUrl != null) if (media.episode?.podcastIndexChapterUrl != null)
ChapterUtils.loadChaptersFromUrl(media.episode!!.podcastIndexChapterUrl!!, false) ChapterUtils.loadChaptersFromUrl(media.episode!!.podcastIndexChapterUrl!!, false)
// Get duration // Get duration
var durationStr: String? = null var durationStr: String? = null
try { try {
@ -385,7 +366,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
} }
val item = media.episode val item = media.episode
item?.media = media item?.media = media
try { try {
// we've received the media, we don't want to autodownload it again // we've received the media, we don't want to autodownload it again
if (item != null) { if (item != null) {
@ -404,7 +384,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
Log.e(TAG, "ExecutionException in MediaHandlerThread: " + e.message) Log.e(TAG, "ExecutionException in MediaHandlerThread: " + e.message)
updatedStatus = DownloadResult(media.id, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, e.message?:"") updatedStatus = DownloadResult(media.id, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, e.message?:"")
} }
if (item != null) { if (item != null) {
val action = EpisodeAction.Builder(item, EpisodeAction.DOWNLOAD) val action = EpisodeAction.Builder(item, EpisodeAction.DOWNLOAD)
.currentTimestamp() .currentTimestamp()

View File

@ -7,6 +7,7 @@ import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
import ac.mdiq.podcini.net.feed.parser.FeedHandler import ac.mdiq.podcini.net.feed.parser.FeedHandler
import ac.mdiq.podcini.net.feed.parser.FeedHandlerResult 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.preferences.UserPreferences
import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
@ -14,6 +15,8 @@ import ac.mdiq.podcini.net.utils.NetworkUtils.isFeedRefreshAllowed
import ac.mdiq.podcini.net.utils.NetworkUtils.isNetworkRestricted import ac.mdiq.podcini.net.utils.NetworkUtils.isNetworkRestricted
import ac.mdiq.podcini.net.utils.NetworkUtils.isVpnOverWifi import ac.mdiq.podcini.net.utils.NetworkUtils.isVpnOverWifi
import ac.mdiq.podcini.net.utils.NetworkUtils.networkAvailable import ac.mdiq.podcini.net.utils.NetworkUtils.networkAvailable
import ac.mdiq.podcini.preferences.UserPreferences.PREF_UPDATE_INTERVAL
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia
import ac.mdiq.podcini.storage.database.Feeds import ac.mdiq.podcini.storage.database.Feeds
import ac.mdiq.podcini.storage.database.LogsAndStats import ac.mdiq.podcini.storage.database.LogsAndStats
@ -53,13 +56,20 @@ import java.util.concurrent.TimeUnit
import javax.xml.parsers.ParserConfigurationException import javax.xml.parsers.ParserConfigurationException
object FeedUpdateManager { object FeedUpdateManager {
private val TAG: String = FeedUpdateManager::class.simpleName ?: "Anonymous"
const val WORK_TAG_FEED_UPDATE: String = "feedUpdate" const val WORK_TAG_FEED_UPDATE: String = "feedUpdate"
private const val WORK_ID_FEED_UPDATE = "ac.mdiq.podcini.service.download.FeedUpdateWorker" private const val WORK_ID_FEED_UPDATE = "ac.mdiq.podcini.service.download.FeedUpdateWorker"
private const val WORK_ID_FEED_UPDATE_MANUAL = "feedUpdateManual" private const val WORK_ID_FEED_UPDATE_MANUAL = "feedUpdateManual"
const val EXTRA_FEED_ID: String = "feed_id" const val EXTRA_FEED_ID: String = "feed_id"
const val EXTRA_NEXT_PAGE: String = "next_page" const val EXTRA_NEXT_PAGE: String = "next_page"
const val EXTRA_EVEN_ON_MOBILE: String = "even_on_mobile" const val EXTRA_EVEN_ON_MOBILE: String = "even_on_mobile"
private val TAG: String = FeedUpdateManager::class.simpleName ?: "Anonymous"
val updateInterval: Long
get() = appPrefs.getString(PREF_UPDATE_INTERVAL, "12")!!.toInt().toLong()
val isAutoUpdateDisabled: Boolean
get() = updateInterval == 0L
/** /**
* Start / restart periodic auto feed refresh * Start / restart periodic auto feed refresh
@ -67,12 +77,12 @@ object FeedUpdateManager {
*/ */
@JvmStatic @JvmStatic
fun restartUpdateAlarm(context: Context, replace: Boolean) { fun restartUpdateAlarm(context: Context, replace: Boolean) {
if (UserPreferences.isAutoUpdateDisabled) { if (isAutoUpdateDisabled) {
WorkManager.getInstance(context).cancelUniqueWork(WORK_ID_FEED_UPDATE) WorkManager.getInstance(context).cancelUniqueWork(WORK_ID_FEED_UPDATE)
} else { } else {
val workRequest: PeriodicWorkRequest = PeriodicWorkRequest.Builder(FeedUpdateWorker::class.java, UserPreferences.updateInterval, TimeUnit.HOURS) val workRequest: PeriodicWorkRequest = PeriodicWorkRequest.Builder(FeedUpdateWorker::class.java, updateInterval, TimeUnit.HOURS)
.setConstraints(Builder() .setConstraints(Builder()
.setRequiredNetworkType(if (UserPreferences.isAllowMobileFeedRefresh) NetworkType.CONNECTED else NetworkType.UNMETERED) .setRequiredNetworkType(if (isAllowMobileFeedRefresh) NetworkType.CONNECTED else NetworkType.UNMETERED)
.build()) .build())
.build() .build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(WORK_ID_FEED_UPDATE, WorkManager.getInstance(context).enqueueUniquePeriodicWork(WORK_ID_FEED_UPDATE,
@ -116,7 +126,7 @@ object FeedUpdateManager {
.setTitle(R.string.feed_refresh_title) .setTitle(R.string.feed_refresh_title)
.setPositiveButton(R.string.confirm_mobile_streaming_button_once) { _: DialogInterface?, _: Int -> runOnce(context, feed) } .setPositiveButton(R.string.confirm_mobile_streaming_button_once) { _: DialogInterface?, _: Int -> runOnce(context, feed) }
.setNeutralButton(R.string.confirm_mobile_streaming_button_always) { _: DialogInterface?, _: Int -> .setNeutralButton(R.string.confirm_mobile_streaming_button_always) { _: DialogInterface?, _: Int ->
UserPreferences.isAllowMobileFeedRefresh = true isAllowMobileFeedRefresh = true
runOnce(context, feed) runOnce(context, feed)
} }
.setNegativeButton(R.string.no, null) .setNegativeButton(R.string.no, null)

View File

@ -15,9 +15,11 @@ import ac.mdiq.podcini.net.sync.model.ISyncService
import ac.mdiq.podcini.net.sync.model.SyncServiceException import ac.mdiq.podcini.net.sync.model.SyncServiceException
import ac.mdiq.podcini.net.sync.nextcloud.NextcloudSyncService import ac.mdiq.podcini.net.sync.nextcloud.NextcloudSyncService
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueStorage 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.net.utils.UrlChecker.containsUrl
import ac.mdiq.podcini.preferences.UserPreferences.gpodnetNotificationsEnabled import ac.mdiq.podcini.preferences.UserPreferences.PREF_GPODNET_NOTIFICATIONS
import ac.mdiq.podcini.preferences.UserPreferences.isAllowMobileSync import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.database.Episodes.getEpisodeByGuidOrUrl import ac.mdiq.podcini.storage.database.Episodes.getEpisodeByGuidOrUrl
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
import ac.mdiq.podcini.storage.database.Episodes.persistEpisodes import ac.mdiq.podcini.storage.database.Episodes.persistEpisodes
@ -27,16 +29,17 @@ import ac.mdiq.podcini.storage.database.Feeds.getFeedListDownloadUrls
import ac.mdiq.podcini.storage.database.Feeds.updateFeed import ac.mdiq.podcini.storage.database.Feeds.updateFeed
import ac.mdiq.podcini.storage.database.Queues.removeFromQueue import ac.mdiq.podcini.storage.database.Queues.removeFromQueue
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.model.EpisodeFilter import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
import ac.mdiq.podcini.storage.model.EpisodeSortOrder import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
import ac.mdiq.podcini.ui.utils.NotificationUtils import ac.mdiq.podcini.ui.utils.NotificationUtils
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.* import ac.mdiq.podcini.util.event.*
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.os.Build
import android.util.Log import android.util.Log
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.collection.ArrayMap import androidx.collection.ArrayMap
@ -295,6 +298,11 @@ open class SyncService(context: Context, params: WorkerParameters) : Worker(cont
nm.cancel(R.id.notification_gpodnet_sync_autherror) nm.cancel(R.id.notification_gpodnet_sync_autherror)
} }
fun gpodnetNotificationsEnabled(): Boolean {
if (Build.VERSION.SDK_INT >= 26) return true // System handles notification preferences
return appPrefs.getBoolean(PREF_GPODNET_NOTIFICATIONS, true)
}
protected fun updateErrorNotification(exception: Exception) { protected fun updateErrorNotification(exception: Exception) {
Logd(TAG, "Posting sync error notification") Logd(TAG, "Posting sync error notification")
val description = ("${applicationContext.getString(R.string.gpodnetsync_error_descr)}${exception.message}") val description = ("${applicationContext.getString(R.string.gpodnetsync_error_descr)}${exception.message}")
@ -347,6 +355,11 @@ open class SyncService(context: Context, params: WorkerParameters) : Worker(cont
internal fun setCurrentlyActive(active: Boolean) { internal fun setCurrentlyActive(active: Boolean) {
isCurrentlyActive = active isCurrentlyActive = active
} }
var isAllowMobileSync: Boolean
get() = isAllowMobileFor("sync")
set(allow) {
setAllowMobileFor("sync", allow)
}
private fun getWorkRequest(): OneTimeWorkRequest.Builder { private fun getWorkRequest(): OneTimeWorkRequest.Builder {
val constraints = Builder() val constraints = Builder()

View File

@ -1,7 +1,8 @@
package ac.mdiq.podcini.net.sync package ac.mdiq.podcini.net.sync
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink.clearQueue import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink.clearQueue
import ac.mdiq.podcini.preferences.UserPreferences.setGpodnetNotificationsEnabled import ac.mdiq.podcini.preferences.UserPreferences.PREF_GPODNET_NOTIFICATIONS
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.util.config.ClientConfig import ac.mdiq.podcini.util.config.ClientConfig
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
@ -55,6 +56,10 @@ object SynchronizationCredentials {
preferences.edit().putInt(PREF_HOSTPORT, value).apply() preferences.edit().putInt(PREF_HOSTPORT, value).apply()
} }
fun setGpodnetNotificationsEnabled() {
appPrefs.edit().putBoolean(PREF_GPODNET_NOTIFICATIONS, true).apply()
}
@Synchronized @Synchronized
fun clear(context: Context) { fun clear(context: Context) {
username = null username = null

View File

@ -1,10 +1,14 @@
package ac.mdiq.podcini.net.utils 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.appPrefs
import android.content.Context import android.content.Context
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import ac.mdiq.podcini.preferences.UserPreferences import android.os.Build
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.BufferedReader import java.io.BufferedReader
@ -22,6 +26,39 @@ object NetworkUtils {
NetworkUtils.context = context NetworkUtils.context = context
} }
var isAllowMobileStreaming: Boolean
get() = isAllowMobileFor("streaming")
set(allow) {
setAllowMobileFor("streaming", allow)
}
var isAllowMobileAutoDownload: Boolean
get() = isAllowMobileFor("auto_download")
set(allow) {
setAllowMobileFor("auto_download", allow)
}
fun isAllowMobileFor(type: String): Boolean {
val defaultValue = HashSet<String>()
defaultValue.add("images")
val allowed = appPrefs.getStringSet(PREF_MOBILE_UPDATE, defaultValue)
return allowed!!.contains(type)
}
fun setAllowMobileFor(type: String, allow: Boolean) {
val defaultValue = HashSet<String>()
defaultValue.add("images")
val getValueStringSet = appPrefs.getStringSet(PREF_MOBILE_UPDATE, defaultValue)
val allowed: MutableSet<String> = HashSet(getValueStringSet!!)
if (allow) allowed.add(type)
else allowed.remove(type)
appPrefs.edit().putStringSet(PREF_MOBILE_UPDATE, allowed).apply()
}
val isEnableAutodownloadWifiFilter: Boolean
get() = Build.VERSION.SDK_INT < 29 && appPrefs.getBoolean(PREF_ENABLE_AUTODL_WIFI_FILTER, false)
@JvmStatic @JvmStatic
val isAutoDownloadAllowed: Boolean val isAutoDownloadAllowed: Boolean
get() { get() {
@ -29,11 +66,11 @@ object NetworkUtils {
val networkInfo = cm.activeNetworkInfo ?: return false val networkInfo = cm.activeNetworkInfo ?: return false
return when (networkInfo.type) { return when (networkInfo.type) {
ConnectivityManager.TYPE_WIFI -> { ConnectivityManager.TYPE_WIFI -> {
if (UserPreferences.isEnableAutodownloadWifiFilter) isInAllowedWifiNetwork if (isEnableAutodownloadWifiFilter) isInAllowedWifiNetwork
else !isNetworkMetered else !isNetworkMetered
} }
ConnectivityManager.TYPE_ETHERNET -> true ConnectivityManager.TYPE_ETHERNET -> true
else -> UserPreferences.isAllowMobileAutoDownload || !isNetworkRestricted else -> isAllowMobileAutoDownload || !isNetworkRestricted
} }
} }
@ -44,9 +81,21 @@ object NetworkUtils {
return info != null && info.isConnected return info != null && info.isConnected
} }
var isAllowMobileFeedRefresh: Boolean
get() = isAllowMobileFor("feed_refresh")
set(allow) {
setAllowMobileFor("feed_refresh", allow)
}
var isAllowMobileEpisodeDownload: Boolean
get() = isAllowMobileFor("episode_download")
set(allow) {
setAllowMobileFor("episode_download", allow)
}
@JvmStatic @JvmStatic
val isEpisodeDownloadAllowed: Boolean val isEpisodeDownloadAllowed: Boolean
get() = UserPreferences.isAllowMobileEpisodeDownload || !isNetworkRestricted get() = isAllowMobileEpisodeDownload || !isNetworkRestricted
@JvmStatic @JvmStatic
val isEpisodeHeadDownloadAllowed: Boolean val isEpisodeHeadDownloadAllowed: Boolean
@ -54,17 +103,23 @@ object NetworkUtils {
// that is probably not even considered a download by most users // that is probably not even considered a download by most users
get() = isImageAllowed get() = isImageAllowed
var isAllowMobileImages: Boolean
get() = isAllowMobileFor("images")
set(allow) {
setAllowMobileFor("images", allow)
}
@JvmStatic @JvmStatic
val isImageAllowed: Boolean val isImageAllowed: Boolean
get() = UserPreferences.isAllowMobileImages || !isNetworkRestricted get() = isAllowMobileImages || !isNetworkRestricted
@JvmStatic @JvmStatic
val isStreamingAllowed: Boolean val isStreamingAllowed: Boolean
get() = UserPreferences.isAllowMobileStreaming || !isNetworkRestricted get() = isAllowMobileStreaming || !isNetworkRestricted
@JvmStatic @JvmStatic
val isFeedRefreshAllowed: Boolean val isFeedRefreshAllowed: Boolean
get() = UserPreferences.isAllowMobileFeedRefresh || !isNetworkRestricted get() = isAllowMobileFeedRefresh || !isNetworkRestricted
@JvmStatic @JvmStatic
val isNetworkRestricted: Boolean val isNetworkRestricted: Boolean
@ -101,10 +156,16 @@ object NetworkUtils {
// } // }
} }
val autodownloadSelectedNetworks: Array<String>
get() {
val selectedNetWorks = appPrefs.getString(PREF_AUTODL_SELECTED_NETWORKS, "")
return selectedNetWorks?.split(",")?.toTypedArray() ?: arrayOf()
}
private val isInAllowedWifiNetwork: Boolean private val isInAllowedWifiNetwork: Boolean
get() { get() {
val wm = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager val wm = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
val selectedNetworks = listOf(*UserPreferences.autodownloadSelectedNetworks) val selectedNetworks = listOf(*autodownloadSelectedNetworks)
return selectedNetworks.contains(wm.connectionInfo.networkId.toString()) return selectedNetworks.contains(wm.connectionInfo.networkId.toString())
} }

View File

@ -3,6 +3,10 @@ package ac.mdiq.podcini.playback.base
import ac.mdiq.podcini.playback.base.InTheatre.curMedia import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.InTheatre.curState import ac.mdiq.podcini.playback.base.InTheatre.curState
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.PREF_PLAYBACK_SPEED
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.setPlaybackSpeed
import ac.mdiq.podcini.preferences.UserPreferences.videoPlaybackSpeed
import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.FeedPreferences import ac.mdiq.podcini.storage.model.FeedPreferences
import ac.mdiq.podcini.storage.model.Playable import ac.mdiq.podcini.storage.model.Playable
@ -301,6 +305,17 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
@JvmField @JvmField
val LONG_REWIND: Long = TimeUnit.SECONDS.toMillis(20) val LONG_REWIND: Long = TimeUnit.SECONDS.toMillis(20)
val audioPlaybackSpeed: Float
get() {
try {
return appPrefs.getString(PREF_PLAYBACK_SPEED, "1.00")!!.toFloat()
} catch (e: NumberFormatException) {
Log.e(TAG, Log.getStackTraceString(e))
setPlaybackSpeed(1.0f)
return 1.0f
}
}
/** /**
* @param currentPosition current position in a media file in ms * @param currentPosition current position in a media file in ms
* @param lastPlayedTime timestamp when was media paused * @param lastPlayedTime timestamp when was media paused
@ -334,8 +349,12 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
if (media.episode?.feed?.preferences != null) playbackSpeed = media.episode!!.feed!!.preferences!!.playSpeed if (media.episode?.feed?.preferences != null) playbackSpeed = media.episode!!.feed!!.preferences!!.playSpeed
} }
} }
if (mediaType != null && playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL) playbackSpeed = UserPreferences.getPlaybackSpeed(mediaType) if (mediaType != null && playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL) playbackSpeed = getPlaybackSpeed(mediaType)
return playbackSpeed return playbackSpeed
} }
fun getPlaybackSpeed(mediaType: MediaType): Float {
return if (mediaType == MediaType.VIDEO) videoPlaybackSpeed else audioPlaybackSpeed
}
} }
} }

View File

@ -2,6 +2,7 @@ package ac.mdiq.podcini.playback.service
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileStreaming
import ac.mdiq.podcini.net.utils.NetworkUtils.isStreamingAllowed import ac.mdiq.podcini.net.utils.NetworkUtils.isStreamingAllowed
import ac.mdiq.podcini.playback.PlaybackServiceStarter import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.playback.base.InTheatre import ac.mdiq.podcini.playback.base.InTheatre
@ -22,43 +23,38 @@ import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableFrom
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableTo import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableTo
import ac.mdiq.podcini.preferences.SleepTimerPreferences.isInTimeRange import ac.mdiq.podcini.preferences.SleepTimerPreferences.isInTimeRange
import ac.mdiq.podcini.preferences.SleepTimerPreferences.timerMillis 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.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
import ac.mdiq.podcini.preferences.UserPreferences.hardwareForwardButton
import ac.mdiq.podcini.preferences.UserPreferences.hardwarePreviousButton
import ac.mdiq.podcini.preferences.UserPreferences.isAllowMobileStreaming
import ac.mdiq.podcini.preferences.UserPreferences.isFollowQueue
import ac.mdiq.podcini.preferences.UserPreferences.isPauseOnHeadsetDisconnect
import ac.mdiq.podcini.preferences.UserPreferences.isPersistNotify
import ac.mdiq.podcini.preferences.UserPreferences.isUnpauseOnBluetoothReconnect
import ac.mdiq.podcini.preferences.UserPreferences.isUnpauseOnHeadsetReconnect
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
import ac.mdiq.podcini.preferences.UserPreferences.shouldAutoDeleteItem import ac.mdiq.podcini.preferences.UserPreferences.shouldAutoDeleteItem
import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue
import ac.mdiq.podcini.preferences.UserPreferences.shouldFavoriteKeepEpisode
import ac.mdiq.podcini.preferences.UserPreferences.shouldSkipKeepEpisode
import ac.mdiq.podcini.receiver.MediaButtonReceiver import ac.mdiq.podcini.receiver.MediaButtonReceiver
import ac.mdiq.podcini.storage.database.Episodes.addToHistory import ac.mdiq.podcini.storage.database.Episodes.addToHistory
import ac.mdiq.podcini.storage.database.Episodes.deleteMediaSync import ac.mdiq.podcini.storage.database.Episodes.deleteMediaSync
import ac.mdiq.podcini.storage.database.Episodes.getEpisodeByGuidOrUrl import ac.mdiq.podcini.storage.database.Episodes.getEpisodeByGuidOrUrl
import ac.mdiq.podcini.storage.database.Episodes.setPlayStateSync
import ac.mdiq.podcini.storage.database.Episodes.persistEpisode import ac.mdiq.podcini.storage.database.Episodes.persistEpisode
import ac.mdiq.podcini.storage.database.Episodes.setPlayStateSync
import ac.mdiq.podcini.storage.database.Queues.addToQueue import ac.mdiq.podcini.storage.database.Queues.addToQueue
import ac.mdiq.podcini.storage.database.Queues.removeFromQueueSync import ac.mdiq.podcini.storage.database.Queues.removeFromQueueSync
import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.model.CurrentState.Companion.NO_MEDIA_PLAYING import ac.mdiq.podcini.storage.model.CurrentState.Companion.NO_MEDIA_PLAYING
import ac.mdiq.podcini.storage.model.CurrentState.Companion.PLAYER_STATUS_OTHER import ac.mdiq.podcini.storage.model.CurrentState.Companion.PLAYER_STATUS_OTHER
import ac.mdiq.podcini.storage.model.CurrentState.Companion.PLAYER_STATUS_PAUSED import ac.mdiq.podcini.storage.model.CurrentState.Companion.PLAYER_STATUS_PAUSED
import ac.mdiq.podcini.storage.model.CurrentState.Companion.PLAYER_STATUS_PLAYING import ac.mdiq.podcini.storage.model.CurrentState.Companion.PLAYER_STATUS_PLAYING
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.FeedPreferences
import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction
import ac.mdiq.podcini.storage.model.Playable
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.storage.model.VolumeAdaptionSetting
import ac.mdiq.podcini.ui.utils.NotificationUtils import ac.mdiq.podcini.ui.utils.NotificationUtils
import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState
import ac.mdiq.podcini.util.IntentUtils.sendLocalBroadcast import ac.mdiq.podcini.util.IntentUtils.sendLocalBroadcast
@ -82,6 +78,7 @@ import android.view.KeyEvent
import android.view.ViewConfiguration import android.view.ViewConfiguration
import android.webkit.URLUtil import android.webkit.URLUtil
import android.widget.Toast import android.widget.Toast
import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.media3.common.MediaMetadata import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player.STATE_ENDED import androidx.media3.common.Player.STATE_ENDED
@ -331,6 +328,14 @@ class PlaybackService : MediaSessionService() {
} }
} }
fun shouldSkipKeepEpisode(): Boolean {
return appPrefs.getBoolean(PREF_SKIP_KEEPS_EPISODE, true)
}
fun shouldFavoriteKeepEpisode(): Boolean {
return appPrefs.getBoolean(PREF_FAVORITE_KEEPS_EPISODE, true)
}
override fun onPlaybackStart(playable: Playable, position: Int) { override fun onPlaybackStart(playable: Playable, position: Int) {
Logd(TAG, "onPlaybackStart position: $position") Logd(TAG, "onPlaybackStart position: $position")
taskManager.startWidgetUpdater() taskManager.startWidgetUpdater()
@ -1178,6 +1183,37 @@ class PlaybackService : MediaSessionService() {
var currentMediaType: MediaType? = MediaType.UNKNOWN var currentMediaType: MediaType? = MediaType.UNKNOWN
private set private set
/**
* @return `true` if notifications are persistent, `false` otherwise
*/
val isPersistNotify: Boolean
get() = appPrefs.getBoolean(PREF_PERSISTENT_NOTIFICATION, true)
val isPauseOnHeadsetDisconnect: Boolean
get() = appPrefs.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true)
val isUnpauseOnHeadsetReconnect: Boolean
get() = appPrefs.getBoolean(PREF_UNPAUSE_ON_HEADSET_RECONNECT, true)
val isUnpauseOnBluetoothReconnect: Boolean
get() = appPrefs.getBoolean(PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT, false)
val hardwareForwardButton: Int
get() = appPrefs.getString(PREF_HARDWARE_FORWARD_BUTTON, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD.toString())!!.toInt()
val hardwarePreviousButton: Int
get() = appPrefs.getString(PREF_HARDWARE_PREVIOUS_BUTTON, 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)
set(value) {
appPrefs.edit().putBoolean(PREF_FOLLOW_QUEUE, value).apply()
}
fun updateVolumeIfNecessary(mediaPlayer: MediaPlayerBase, feedId: Long, volumeAdaptionSetting: VolumeAdaptionSetting) { fun updateVolumeIfNecessary(mediaPlayer: MediaPlayerBase, feedId: Long, volumeAdaptionSetting: VolumeAdaptionSetting) {
val playable = curMedia val playable = curMedia
if (playable is EpisodeMedia) { if (playable is EpisodeMedia) {

View File

@ -1,12 +1,13 @@
package ac.mdiq.podcini.preferences package ac.mdiq.podcini.preferences
import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce
import ac.mdiq.podcini.preferences.UserPreferences.isAutoBackupOPML 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.getFeedList
import ac.mdiq.podcini.storage.database.Feeds.updateFeed import ac.mdiq.podcini.storage.database.Feeds.updateFeed
import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlReader
import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlWriter
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import android.app.backup.BackupAgentHelper import android.app.backup.BackupAgentHelper
import android.app.backup.BackupDataInputStream import android.app.backup.BackupDataInputStream
@ -29,6 +30,9 @@ import java.security.NoSuchAlgorithmException
class OpmlBackupAgent : BackupAgentHelper() { class OpmlBackupAgent : BackupAgentHelper() {
val isAutoBackupOPML: Boolean
get() = appPrefs.getBoolean(PREF_OPML_BACKUP, true)
override fun onCreate() { override fun onCreate() {
if (isAutoBackupOPML) { if (isAutoBackupOPML) {
Logd(TAG, "Backup enabled in preferences") Logd(TAG, "Backup enabled in preferences")

View File

@ -2,24 +2,16 @@ package ac.mdiq.podcini.preferences
import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.model.ProxyConfig import ac.mdiq.podcini.storage.model.ProxyConfig
import ac.mdiq.podcini.storage.model.EpisodeSortOrder import ac.mdiq.podcini.storage.utils.FilesUtils
import ac.mdiq.podcini.storage.model.MediaType import ac.mdiq.podcini.storage.utils.FilesUtils.createNoMediaFile
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import android.view.KeyEvent
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import org.json.JSONArray
import org.json.JSONException
import java.io.File
import java.io.IOException
import java.net.Proxy import java.net.Proxy
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.*
/** /**
* Provides access to preferences set by the user in the settings screen. A * Provides access to preferences set by the user in the settings screen. A
@ -30,7 +22,7 @@ import java.util.*
object UserPreferences { object UserPreferences {
private val TAG: String = UserPreferences::class.simpleName ?: "Anonymous" private val TAG: String = UserPreferences::class.simpleName ?: "Anonymous"
private const val PREF_OPML_BACKUP = "prefOPMLBackup" const val PREF_OPML_BACKUP = "prefOPMLBackup"
// User Interface // User Interface
const val PREF_THEME: String = "prefTheme" const val PREF_THEME: String = "prefTheme"
@ -40,39 +32,39 @@ object UserPreferences {
const val PREF_DRAWER_FEED_ORDER: String = "prefDrawerFeedOrder" const val PREF_DRAWER_FEED_ORDER: String = "prefDrawerFeedOrder"
const val PREF_DRAWER_FEED_ORDER_DIRECTION: String = "prefDrawerFeedOrderDir" const val PREF_DRAWER_FEED_ORDER_DIRECTION: String = "prefDrawerFeedOrderDir"
const val PREF_FEED_GRID_LAYOUT: String = "prefFeedGridLayout" const val PREF_FEED_GRID_LAYOUT: String = "prefFeedGridLayout"
const val PREF_DRAWER_FEED_COUNTER: String = "prefDrawerFeedIndicator" // const val PREF_DRAWER_FEED_COUNTER: String = "prefDrawerFeedIndicator"
const val PREF_EXPANDED_NOTIFICATION: String = "prefExpandNotify" const val PREF_EXPANDED_NOTIFICATION: String = "prefExpandNotify"
private const val PREF_USE_EPISODE_COVER: String = "prefEpisodeCover" const val PREF_USE_EPISODE_COVER: String = "prefEpisodeCover"
const val PREF_SHOW_TIME_LEFT: String = "showTimeLeft" const val PREF_SHOW_TIME_LEFT: String = "showTimeLeft"
private const val PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify" const val PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify"
const val PREF_FULL_NOTIFICATION_BUTTONS: String = "prefFullNotificationButtons" const val PREF_FULL_NOTIFICATION_BUTTONS: String = "prefFullNotificationButtons"
private const val PREF_SHOW_DOWNLOAD_REPORT = "prefShowDownloadReport" const val PREF_SHOW_DOWNLOAD_REPORT = "prefShowDownloadReport"
const val PREF_DEFAULT_PAGE: String = "prefDefaultPage" const val PREF_DEFAULT_PAGE: String = "prefDefaultPage"
private const val PREF_BACK_OPENS_DRAWER: String = "prefBackButtonOpensDrawer" private const val PREF_BACK_OPENS_DRAWER: String = "prefBackButtonOpensDrawer"
private const val PREF_QUEUE_KEEP_SORTED: String = "prefQueueKeepSorted" const val PREF_QUEUE_KEEP_SORTED: String = "prefQueueKeepSorted"
private const val PREF_QUEUE_KEEP_SORTED_ORDER: String = "prefQueueKeepSortedOrder" const val PREF_QUEUE_KEEP_SORTED_ORDER: String = "prefQueueKeepSortedOrder"
private const val PREF_DOWNLOADS_SORTED_ORDER = "prefDownloadSortedOrder" const val PREF_DOWNLOADS_SORTED_ORDER = "prefDownloadSortedOrder"
private const val PREF_HISTORY_SORTED_ORDER = "prefHistorySortedOrder" // private const val PREF_HISTORY_SORTED_ORDER = "prefHistorySortedOrder"
// Episodes // Episodes
private const val PREF_SORT_ALL_EPISODES: String = "prefEpisodesSort" const val PREF_SORT_ALL_EPISODES: String = "prefEpisodesSort"
private const val PREF_FILTER_ALL_EPISODES: String = "prefEpisodesFilter" const val PREF_FILTER_ALL_EPISODES: String = "prefEpisodesFilter"
// Playback // Playback
private const val PREF_PAUSE_ON_HEADSET_DISCONNECT: String = "prefPauseOnHeadsetDisconnect" const val PREF_PAUSE_ON_HEADSET_DISCONNECT: String = "prefPauseOnHeadsetDisconnect"
const val PREF_UNPAUSE_ON_HEADSET_RECONNECT: String = "prefUnpauseOnHeadsetReconnect" const val PREF_UNPAUSE_ON_HEADSET_RECONNECT: String = "prefUnpauseOnHeadsetReconnect"
const val PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT: String = "prefUnpauseOnBluetoothReconnect" const val PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT: String = "prefUnpauseOnBluetoothReconnect"
private const val PREF_HARDWARE_FORWARD_BUTTON: String = "prefHardwareForwardButton" const val PREF_HARDWARE_FORWARD_BUTTON: String = "prefHardwareForwardButton"
private const val PREF_HARDWARE_PREVIOUS_BUTTON: String = "prefHardwarePreviousButton" const val PREF_HARDWARE_PREVIOUS_BUTTON: String = "prefHardwarePreviousButton"
const val PREF_FOLLOW_QUEUE: String = "prefFollowQueue" const val PREF_FOLLOW_QUEUE: String = "prefFollowQueue"
const val PREF_SKIP_KEEPS_EPISODE: String = "prefSkipKeepsEpisode" const val PREF_SKIP_KEEPS_EPISODE: String = "prefSkipKeepsEpisode"
const val PREF_REMOVDE_FROM_QUEUE_MARKED_PLAYED: String = "prefRemoveFromQueueMarkedPlayed" const val PREF_REMOVDE_FROM_QUEUE_MARKED_PLAYED: String = "prefRemoveFromQueueMarkedPlayed"
private const val PREF_FAVORITE_KEEPS_EPISODE = "prefFavoriteKeepsEpisode" const val PREF_FAVORITE_KEEPS_EPISODE = "prefFavoriteKeepsEpisode"
private const val PREF_AUTO_DELETE = "prefAutoDelete" private const val PREF_AUTO_DELETE = "prefAutoDelete"
private const val PREF_AUTO_DELETE_LOCAL = "prefAutoDeleteLocal" private const val PREF_AUTO_DELETE_LOCAL = "prefAutoDeleteLocal"
const val PREF_SMART_MARK_AS_PLAYED_SECS: String = "prefSmartMarkAsPlayedSecs" const val PREF_SMART_MARK_AS_PLAYED_SECS: String = "prefSmartMarkAsPlayedSecs"
private const val PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray" const val PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray"
private const val PREF_FALLBACK_SPEED = "prefFallbackSpeed" private const val PREF_FALLBACK_SPEED = "prefFallbackSpeed"
private const val PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS: String = "prefPauseForFocusLoss" private const val PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS: String = "prefPauseForFocusLoss"
private const val PREF_TIME_RESPECTS_SPEED = "prefPlaybackTimeRespectsSpeed" private const val PREF_TIME_RESPECTS_SPEED = "prefPlaybackTimeRespectsSpeed"
@ -80,16 +72,16 @@ object UserPreferences {
private const val PREF_SPEEDFORWRD_SPEED = "prefSpeedforwardSpeed" private const val PREF_SPEEDFORWRD_SPEED = "prefSpeedforwardSpeed"
// Network // Network
private const val PREF_ENQUEUE_DOWNLOADED = "prefEnqueueDownloaded" const val PREF_ENQUEUE_DOWNLOADED = "prefEnqueueDownloaded"
const val PREF_ENQUEUE_LOCATION: String = "prefEnqueueLocation" const val PREF_ENQUEUE_LOCATION: String = "prefEnqueueLocation"
const val PREF_UPDATE_INTERVAL: String = "prefAutoUpdateIntervall" const val PREF_UPDATE_INTERVAL: String = "prefAutoUpdateIntervall"
private const val PREF_MOBILE_UPDATE = "prefMobileUpdateTypes" const val PREF_MOBILE_UPDATE = "prefMobileUpdateTypes"
const val PREF_EPISODE_CLEANUP: String = "prefEpisodeCleanup" const val PREF_EPISODE_CLEANUP: String = "prefEpisodeCleanup"
const val PREF_EPISODE_CACHE_SIZE: String = "prefEpisodeCacheSize" const val PREF_EPISODE_CACHE_SIZE: String = "prefEpisodeCacheSize"
const val PREF_ENABLE_AUTODL: String = "prefEnableAutoDl" const val PREF_ENABLE_AUTODL: String = "prefEnableAutoDl"
const val PREF_ENABLE_AUTODL_ON_BATTERY: String = "prefEnableAutoDownloadOnBattery" const val PREF_ENABLE_AUTODL_ON_BATTERY: String = "prefEnableAutoDownloadOnBattery"
const val PREF_ENABLE_AUTODL_WIFI_FILTER: String = "prefEnableAutoDownloadWifiFilter" const val PREF_ENABLE_AUTODL_WIFI_FILTER: String = "prefEnableAutoDownloadWifiFilter"
private const val PREF_AUTODL_SELECTED_NETWORKS = "prefAutodownloadSelectedNetworks" const val PREF_AUTODL_SELECTED_NETWORKS = "prefAutodownloadSelectedNetworks"
private const val PREF_PROXY_TYPE = "prefProxyType" private const val PREF_PROXY_TYPE = "prefProxyType"
private const val PREF_PROXY_HOST = "prefProxyHost" private const val PREF_PROXY_HOST = "prefProxyHost"
private const val PREF_PROXY_PORT = "prefProxyPort" private const val PREF_PROXY_PORT = "prefProxyPort"
@ -97,19 +89,19 @@ object UserPreferences {
private const val PREF_PROXY_PASSWORD = "prefProxyPassword" private const val PREF_PROXY_PASSWORD = "prefProxyPassword"
// Services // Services
private const val PREF_GPODNET_NOTIFICATIONS = "pref_gpodnet_notifications" const val PREF_GPODNET_NOTIFICATIONS = "pref_gpodnet_notifications"
// Other // Other
private const val PREF_DATA_FOLDER = "prefDataFolder" // const val PREF_DATA_FOLDER = "prefDataFolder"
const val PREF_DELETE_REMOVES_FROM_QUEUE: String = "prefDeleteRemovesFromQueue" const val PREF_DELETE_REMOVES_FROM_QUEUE: String = "prefDeleteRemovesFromQueue"
// Mediaplayer // Mediaplayer
private const val PREF_PLAYBACK_SPEED = "prefPlaybackSpeed" const val PREF_PLAYBACK_SPEED = "prefPlaybackSpeed"
private const val PREF_VIDEO_PLAYBACK_SPEED = "prefVideoPlaybackSpeed" private const val PREF_VIDEO_PLAYBACK_SPEED = "prefVideoPlaybackSpeed"
private const val PREF_PLAYBACK_SKIP_SILENCE: String = "prefSkipSilence" private const val PREF_PLAYBACK_SKIP_SILENCE: String = "prefSkipSilence"
private const val PREF_FAST_FORWARD_SECS = "prefFastForwardSecs" private const val PREF_FAST_FORWARD_SECS = "prefFastForwardSecs"
private const val PREF_REWIND_SECS = "prefRewindSecs" private const val PREF_REWIND_SECS = "prefRewindSecs"
private const val PREF_QUEUE_LOCKED = "prefQueueLocked" const val PREF_QUEUE_LOCKED = "prefQueueLocked"
private const val PREF_VIDEO_MODE = "prefVideoPlaybackMode" private const val PREF_VIDEO_MODE = "prefVideoPlaybackMode"
// Experimental // Experimental
@ -141,7 +133,6 @@ object UserPreferences {
private lateinit var context: Context private lateinit var context: Context
lateinit var appPrefs: SharedPreferences lateinit var appPrefs: SharedPreferences
var theme: ThemePreference var theme: ThemePreference
get() = when (appPrefs.getString(PREF_THEME, "system")) { get() = when (appPrefs.getString(PREF_THEME, "system")) {
"0" -> ThemePreference.LIGHT "0" -> ThemePreference.LIGHT
@ -190,103 +181,12 @@ object UserPreferences {
.apply() .apply()
} }
val isAutoBackupOPML: Boolean
get() = appPrefs.getBoolean(PREF_OPML_BACKUP, true)
val feedOrderBy: Int
get() {
val value = appPrefs.getString(PREF_DRAWER_FEED_ORDER, "" + FEED_ORDER_UNPLAYED)
return value!!.toInt()
}
val feedOrderDir: Int
get() {
val value = appPrefs.getInt(PREF_DRAWER_FEED_ORDER_DIRECTION, 0)
return value
}
val useGridLayout: Boolean
get() = appPrefs.getBoolean(PREF_FEED_GRID_LAYOUT, false)
/**
* @return `true` if episodes should use their own cover, `false` otherwise
*/
val useEpisodeCoverSetting: Boolean
get() = appPrefs.getBoolean(PREF_USE_EPISODE_COVER, true)
/**
* @return `true` if notifications are persistent, `false` otherwise
*/
val isPersistNotify: Boolean
get() = appPrefs.getBoolean(PREF_PERSISTENT_NOTIFICATION, true)
/**
* Used for migration of the preference to system notification channels.
*/
val showDownloadReportRaw: Boolean
get() = appPrefs.getBoolean(PREF_SHOW_DOWNLOAD_REPORT, true)
var enqueueLocation: EnqueueLocation
get() {
val valStr = appPrefs.getString(PREF_ENQUEUE_LOCATION, EnqueueLocation.BACK.name)
try {
return EnqueueLocation.valueOf(valStr!!)
} catch (t: Throwable) {
// should never happen but just in case
Log.e(TAG, "getEnqueueLocation: invalid value '$valStr' Use default.", t)
return EnqueueLocation.BACK
}
}
set(location) {
appPrefs.edit().putString(PREF_ENQUEUE_LOCATION, location.name).apply()
}
val isPauseOnHeadsetDisconnect: Boolean
get() = appPrefs.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true)
val isUnpauseOnHeadsetReconnect: Boolean
get() = appPrefs.getBoolean(PREF_UNPAUSE_ON_HEADSET_RECONNECT, true)
val isUnpauseOnBluetoothReconnect: Boolean
get() = appPrefs.getBoolean(PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT, false)
val hardwareForwardButton: Int
get() = appPrefs.getString(PREF_HARDWARE_FORWARD_BUTTON, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD.toString())!!.toInt()
val hardwarePreviousButton: Int
get() = appPrefs.getString(PREF_HARDWARE_PREVIOUS_BUTTON, 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)
set(value) {
appPrefs.edit().putBoolean(PREF_FOLLOW_QUEUE, value).apply()
}
val isAutoDelete: Boolean val isAutoDelete: Boolean
get() = appPrefs.getBoolean(PREF_AUTO_DELETE, false) get() = appPrefs.getBoolean(PREF_AUTO_DELETE, false)
val isAutoDeleteLocal: Boolean val isAutoDeleteLocal: Boolean
get() = appPrefs.getBoolean(PREF_AUTO_DELETE_LOCAL, false) get() = appPrefs.getBoolean(PREF_AUTO_DELETE_LOCAL, false)
val smartMarkAsPlayedSecs: Int
get() = appPrefs.getString(PREF_SMART_MARK_AS_PLAYED_SECS, "30")!!.toInt()
private val audioPlaybackSpeed: Float
get() {
try {
return appPrefs.getString(PREF_PLAYBACK_SPEED, "1.00")!!.toFloat()
} catch (e: NumberFormatException) {
Log.e(TAG, Log.getStackTraceString(e))
setPlaybackSpeed(1.0f)
return 1.0f
}
}
val videoPlayMode: Int val videoPlayMode: Int
get() { get() {
try { try {
@ -320,61 +220,6 @@ object UserPreferences {
appPrefs.edit().putBoolean(PREF_PLAYBACK_SKIP_SILENCE, skipSilence).apply() appPrefs.edit().putBoolean(PREF_PLAYBACK_SKIP_SILENCE, skipSilence).apply()
} }
var playbackSpeedArray: List<Float>
get() = readPlaybackSpeedArray(appPrefs.getString(PREF_PLAYBACK_SPEED_ARRAY, null))
set(speeds) {
val format = DecimalFormatSymbols(Locale.US)
format.decimalSeparator = '.'
val speedFormat = DecimalFormat("0.00", format)
val jsonArray = JSONArray()
for (speed in speeds) {
jsonArray.put(speedFormat.format(speed.toDouble()))
}
appPrefs.edit().putString(PREF_PLAYBACK_SPEED_ARRAY, jsonArray.toString()).apply()
}
val updateInterval: Long
get() = appPrefs.getString(PREF_UPDATE_INTERVAL, "12")!!.toInt().toLong()
val isAutoUpdateDisabled: Boolean
get() = updateInterval == 0L
var isAllowMobileFeedRefresh: Boolean
get() = isAllowMobileFor("feed_refresh")
set(allow) {
setAllowMobileFor("feed_refresh", allow)
}
var isAllowMobileSync: Boolean
get() = isAllowMobileFor("sync")
set(allow) {
setAllowMobileFor("sync", allow)
}
var isAllowMobileEpisodeDownload: Boolean
get() = isAllowMobileFor("episode_download")
set(allow) {
setAllowMobileFor("episode_download", allow)
}
var isAllowMobileAutoDownload: Boolean
get() = isAllowMobileFor("auto_download")
set(allow) {
setAllowMobileFor("auto_download", allow)
}
var isAllowMobileStreaming: Boolean
get() = isAllowMobileFor("streaming")
set(allow) {
setAllowMobileFor("streaming", allow)
}
var isAllowMobileImages: Boolean
get() = isAllowMobileFor("images")
set(allow) {
setAllowMobileFor("images", allow)
}
/** /**
* Returns the capacity of the episode cache. This method will return the * Returns the capacity of the episode cache. This method will return the
* negative integer EPISODE_CACHE_SIZE_UNLIMITED if the cache size is set to * negative integer EPISODE_CACHE_SIZE_UNLIMITED if the cache size is set to
@ -393,9 +238,6 @@ object UserPreferences {
val isEnableAutodownloadOnBattery: Boolean val isEnableAutodownloadOnBattery: Boolean
get() = appPrefs.getBoolean(PREF_ENABLE_AUTODL_ON_BATTERY, true) get() = appPrefs.getBoolean(PREF_ENABLE_AUTODL_ON_BATTERY, true)
val isEnableAutodownloadWifiFilter: Boolean
get() = Build.VERSION.SDK_INT < 29 && appPrefs.getBoolean(PREF_ENABLE_AUTODL_WIFI_FILTER, false)
var speedforwardSpeed: Float var speedforwardSpeed: Float
get() { get() {
try { try {
@ -436,12 +278,6 @@ object UserPreferences {
appPrefs.edit().putInt(PREF_REWIND_SECS, secs).apply() appPrefs.edit().putInt(PREF_REWIND_SECS, secs).apply()
} }
val autodownloadSelectedNetworks: Array<String>
get() {
val selectedNetWorks = appPrefs.getString(PREF_AUTODL_SELECTED_NETWORKS, "")
return selectedNetWorks?.split(",")?.toTypedArray() ?: arrayOf()
}
var proxyConfig: ProxyConfig var proxyConfig: ProxyConfig
get() { get() {
val type = Proxy.Type.valueOf(appPrefs.getString(PREF_PROXY_TYPE, Proxy.Type.DIRECT.name)!!) val type = Proxy.Type.valueOf(appPrefs.getString(PREF_PROXY_TYPE, Proxy.Type.DIRECT.name)!!)
@ -469,24 +305,6 @@ object UserPreferences {
editor.apply() editor.apply()
} }
var isQueueLocked: Boolean
get() = appPrefs.getBoolean(PREF_QUEUE_LOCKED, false)
set(locked) {
appPrefs.edit().putBoolean(PREF_QUEUE_LOCKED, locked).apply()
}
/**
* Used for migration of the preference to system notification channels.
*/
val gpodnetNotificationsEnabledRaw: Boolean
get() = appPrefs.getBoolean(PREF_GPODNET_NOTIFICATIONS, true)
var episodeCleanupValue: Int
get() = appPrefs.getString(PREF_EPISODE_CLEANUP, "" + EPISODE_CLEANUP_NULL)!!.toInt()
set(episodeCleanupValue) {
appPrefs.edit().putString(PREF_EPISODE_CLEANUP, episodeCleanupValue.toString()).apply()
}
var defaultPage: String? var defaultPage: String?
get() = appPrefs.getString(PREF_DEFAULT_PAGE, "SubscriptionsFragment") get() = appPrefs.getString(PREF_DEFAULT_PAGE, "SubscriptionsFragment")
set(defaultPage) { set(defaultPage) {
@ -499,61 +317,6 @@ object UserPreferences {
appPrefs.edit().putBoolean(PREF_STREAM_OVER_DOWNLOAD, stream).apply() appPrefs.edit().putBoolean(PREF_STREAM_OVER_DOWNLOAD, stream).apply()
} }
var isQueueKeepSorted: Boolean
/**
* Returns if the queue is in keep sorted mode.
* @see .queueKeepSortedOrder
*/
get() = appPrefs.getBoolean(PREF_QUEUE_KEEP_SORTED, false)
/**
* Enables/disables the keep sorted mode of the queue.
* @see .queueKeepSortedOrder
*/
set(keepSorted) {
appPrefs.edit().putBoolean(PREF_QUEUE_KEEP_SORTED, keepSorted).apply()
}
var queueKeepSortedOrder: EpisodeSortOrder?
/**
* Returns the sort order for the queue keep sorted mode.
* Note: This value is stored independently from the keep sorted state.
* @see .isQueueKeepSorted
*/
get() {
val sortOrderStr = appPrefs.getString(PREF_QUEUE_KEEP_SORTED_ORDER, "use-default")
return EpisodeSortOrder.parseWithDefault(sortOrderStr, EpisodeSortOrder.DATE_NEW_OLD)
}
/**
* Sets the sort order for the queue keep sorted mode.
* @see .setQueueKeepSorted
*/
set(sortOrder) {
if (sortOrder == null) return
appPrefs.edit().putString(PREF_QUEUE_KEEP_SORTED_ORDER, sortOrder.name).apply()
}
// the sort order for the downloads.
var downloadsSortedOrder: EpisodeSortOrder?
get() {
val sortOrderStr = appPrefs.getString(PREF_DOWNLOADS_SORTED_ORDER, "" + EpisodeSortOrder.DATE_NEW_OLD.code)
return EpisodeSortOrder.fromCodeString(sortOrderStr)
}
set(sortOrder) {
appPrefs.edit().putString(PREF_DOWNLOADS_SORTED_ORDER, "" + sortOrder!!.code).apply()
}
var allEpisodesSortOrder: EpisodeSortOrder?
get() = EpisodeSortOrder.fromCodeString(appPrefs.getString(PREF_SORT_ALL_EPISODES, "" + EpisodeSortOrder.DATE_NEW_OLD.code))
set(s) {
appPrefs.edit().putString(PREF_SORT_ALL_EPISODES, "" + s!!.code).apply()
}
var prefFilterAllEpisodes: String
get() = appPrefs.getString(PREF_FILTER_ALL_EPISODES, "")?:""
set(filter) {
appPrefs.edit().putString(PREF_FILTER_ALL_EPISODES, filter).apply()
}
/** /**
* Sets up the UserPreferences class. * Sets up the UserPreferences class.
* @throws IllegalArgumentException if context is null * @throws IllegalArgumentException if context is null
@ -561,6 +324,7 @@ object UserPreferences {
fun init(context: Context) { fun init(context: Context) {
Logd(TAG, "Creating new instance of UserPreferences") Logd(TAG, "Creating new instance of UserPreferences")
UserPreferences.context = context.applicationContext UserPreferences.context = context.applicationContext
FilesUtils.context = context.applicationContext
appPrefs = PreferenceManager.getDefaultSharedPreferences(context) appPrefs = PreferenceManager.getDefaultSharedPreferences(context)
createNoMediaFile() createNoMediaFile()
@ -606,19 +370,6 @@ object UserPreferences {
return appPrefs.getBoolean(PREF_SHOW_TIME_LEFT, false) return appPrefs.getBoolean(PREF_SHOW_TIME_LEFT, false)
} }
fun setFeedOrder(selected: String, dir: Int) {
appPrefs.edit()
.putString(PREF_DRAWER_FEED_ORDER, selected)
.apply()
appPrefs.edit()
.putInt(PREF_DRAWER_FEED_ORDER_DIRECTION, dir)
.apply()
}
fun enqueueDownloadedEpisodes(): Boolean {
return appPrefs.getBoolean(PREF_ENQUEUE_DOWNLOADED, true)
}
/** /**
* Sets the preference for whether we show the remain time, if not show the duration. This will * Sets the preference for whether we show the remain time, if not show the duration. This will
* send out events so the current playing screen, queue and the episode list would refresh * send out events so the current playing screen, queue and the episode list would refresh
@ -628,49 +379,15 @@ object UserPreferences {
appPrefs.edit().putBoolean(PREF_SHOW_TIME_LEFT, showRemain!!).apply() appPrefs.edit().putBoolean(PREF_SHOW_TIME_LEFT, showRemain!!).apply()
} }
fun shouldSkipKeepEpisode(): Boolean {
return appPrefs.getBoolean(PREF_SKIP_KEEPS_EPISODE, true)
}
fun shouldRemoveFromQueuesMarkPlayed(): Boolean {
return appPrefs.getBoolean(PREF_REMOVDE_FROM_QUEUE_MARKED_PLAYED, true)
}
fun shouldFavoriteKeepEpisode(): Boolean {
return appPrefs.getBoolean(PREF_FAVORITE_KEEPS_EPISODE, true)
}
fun shouldDeleteRemoveFromQueue(): Boolean { fun shouldDeleteRemoveFromQueue(): Boolean {
return appPrefs.getBoolean(PREF_DELETE_REMOVES_FROM_QUEUE, false) return appPrefs.getBoolean(PREF_DELETE_REMOVES_FROM_QUEUE, false)
} }
fun getPlaybackSpeed(mediaType: MediaType): Float {
return if (mediaType == MediaType.VIDEO) videoPlaybackSpeed else audioPlaybackSpeed
}
// only used in test // only used in test
fun shouldPauseForFocusLoss(): Boolean { fun shouldPauseForFocusLoss(): Boolean {
return appPrefs.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, true) return appPrefs.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, true)
} }
private fun isAllowMobileFor(type: String): Boolean {
val defaultValue = HashSet<String>()
defaultValue.add("images")
val allowed = appPrefs.getStringSet(PREF_MOBILE_UPDATE, defaultValue)
return allowed!!.contains(type)
}
private fun setAllowMobileFor(type: String, allow: Boolean) {
val defaultValue = HashSet<String>()
defaultValue.add("images")
val getValueStringSet = appPrefs.getStringSet(PREF_MOBILE_UPDATE, defaultValue)
val allowed: MutableSet<String> = HashSet(getValueStringSet!!)
if (allow) allowed.add(type)
else allowed.remove(type)
appPrefs.edit().putStringSet(PREF_MOBILE_UPDATE, allowed).apply()
}
fun backButtonOpensDrawer(): Boolean { fun backButtonOpensDrawer(): Boolean {
return appPrefs.getBoolean(PREF_BACK_OPENS_DRAWER, false) return appPrefs.getBoolean(PREF_BACK_OPENS_DRAWER, false)
} }
@ -687,101 +404,7 @@ object UserPreferences {
appPrefs.edit().putString(PREF_VIDEO_MODE, mode.toString()).apply() appPrefs.edit().putString(PREF_VIDEO_MODE, mode.toString()).apply()
} }
fun setAutodownloadSelectedNetworks(value: Array<String?>?) {
appPrefs.edit().putString(PREF_AUTODL_SELECTED_NETWORKS, value!!.joinToString()).apply()
}
fun gpodnetNotificationsEnabled(): Boolean {
if (Build.VERSION.SDK_INT >= 26) return true // System handles notification preferences
return appPrefs.getBoolean(PREF_GPODNET_NOTIFICATIONS, true)
}
fun setGpodnetNotificationsEnabled() {
appPrefs.edit().putBoolean(PREF_GPODNET_NOTIFICATIONS, true).apply()
}
private fun readPlaybackSpeedArray(valueFromPrefs: String?): List<Float> {
if (valueFromPrefs != null) {
try {
val jsonArray = JSONArray(valueFromPrefs)
val selectedSpeeds: MutableList<Float> = ArrayList()
for (i in 0 until jsonArray.length()) {
selectedSpeeds.add(jsonArray.getDouble(i).toFloat())
}
return selectedSpeeds
} catch (e: JSONException) {
Log.e(TAG, "Got JSON error when trying to get speeds from JSONArray")
e.printStackTrace()
}
}
// If this preference hasn't been set yet, return the default options
return mutableListOf(1.0f, 1.25f, 1.5f)
}
/**
* Return the folder where the app stores all of its data. This method will
* return the standard data folder if none has been set by the user.
* @param type The name of the folder inside the data folder. May be null
* when accessing the root of the data folder.
* @return The data folder that has been requested or null if the folder could not be created.
*/
fun getDataFolder(type: String?): File? {
var dataFolder = getTypeDir(appPrefs.getString(PREF_DATA_FOLDER, null), type)
if (dataFolder == null || !dataFolder.canWrite()) {
Logd(TAG, "User data folder not writable or not set. Trying default.")
dataFolder = context.getExternalFilesDir(type)
}
if (dataFolder == null || !dataFolder.canWrite()) {
Logd(TAG, "Default data folder not available or not writable. Falling back to internal memory.")
dataFolder = getTypeDir(context.filesDir.absolutePath, type)
}
return dataFolder
}
private fun getTypeDir(baseDirPath: String?, type: String?): File? {
if (baseDirPath == null) return null
val baseDir = File(baseDirPath)
val typeDir = if (type == null) baseDir else File(baseDir, type)
if (!typeDir.exists()) {
if (!baseDir.canWrite()) {
Log.e(TAG, "Base dir is not writable " + baseDir.absolutePath)
return null
}
if (!typeDir.mkdirs()) {
Log.e(TAG, "Could not create type dir " + typeDir.absolutePath)
return null
}
}
return typeDir
}
fun setDataFolder(dir: String) {
Logd(TAG, "setDataFolder(dir: $dir)")
appPrefs.edit().putString(PREF_DATA_FOLDER, dir).apply()
}
/**
* Create a .nomedia file to prevent scanning by the media scanner.
*/
private fun createNoMediaFile() {
val f = File(context.getExternalFilesDir(null), ".nomedia")
if (!f.exists()) {
try {
f.createNewFile()
} catch (e: IOException) {
Log.e(TAG, "Could not create .nomedia file")
e.printStackTrace()
}
Logd(TAG, ".nomedia file created")
}
}
enum class ThemePreference { enum class ThemePreference {
LIGHT, DARK, BLACK, SYSTEM LIGHT, DARK, BLACK, SYSTEM
} }
enum class EnqueueLocation {
BACK, FRONT, AFTER_CURRENTLY_PLAYING, RANDOM
}
} }

View File

@ -13,12 +13,13 @@ import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import ac.mdiq.podcini.R 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.ui.activity.PreferenceActivity
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.autodownloadSelectedNetworks 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.preferences.UserPreferences.isEnableAutodownload
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownloadWifiFilter
import ac.mdiq.podcini.preferences.UserPreferences.setAutodownloadSelectedNetworks
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import java.util.* import java.util.*
@ -126,6 +127,10 @@ class AutoDownloadPreferencesFragment : PreferenceFragmentCompat() {
} }
} }
fun setAutodownloadSelectedNetworks(value: Array<String?>?) {
appPrefs.edit().putString(PREF_AUTODL_SELECTED_NETWORKS, value!!.joinToString()).apply()
}
private fun clearAutodownloadSelectedNetworsPreference() { private fun clearAutodownloadSelectedNetworsPreference() {
if (selectedNetworks != null) { if (selectedNetworks != null) {
val prefScreen = preferenceScreen val prefScreen = preferenceScreen

View File

@ -4,16 +4,15 @@ import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.ChooseDataFolderDialogBinding import ac.mdiq.podcini.databinding.ChooseDataFolderDialogBinding
import ac.mdiq.podcini.databinding.ChooseDataFolderDialogEntryBinding import ac.mdiq.podcini.databinding.ChooseDataFolderDialogEntryBinding
import ac.mdiq.podcini.databinding.ProxySettingsBinding import ac.mdiq.podcini.databinding.ProxySettingsBinding
import ac.mdiq.podcini.net.feed.FeedUpdateManager.restartUpdateAlarm
import ac.mdiq.podcini.net.download.service.PodciniHttpClient import ac.mdiq.podcini.net.download.service.PodciniHttpClient
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.newBuilder import ac.mdiq.podcini.net.download.service.PodciniHttpClient.newBuilder
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.reinit import ac.mdiq.podcini.net.download.service.PodciniHttpClient.reinit
import ac.mdiq.podcini.net.feed.FeedUpdateManager.restartUpdateAlarm
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.getDataFolder
import ac.mdiq.podcini.preferences.UserPreferences.proxyConfig import ac.mdiq.podcini.preferences.UserPreferences.proxyConfig
import ac.mdiq.podcini.preferences.UserPreferences.setDataFolder
import ac.mdiq.podcini.storage.model.ProxyConfig import ac.mdiq.podcini.storage.model.ProxyConfig
import ac.mdiq.podcini.storage.utils.FilesUtils.getDataFolder
import ac.mdiq.podcini.storage.utils.StorageUtils.getFreeSpaceAvailable import ac.mdiq.podcini.storage.utils.StorageUtils.getFreeSpaceAvailable
import ac.mdiq.podcini.storage.utils.StorageUtils.getTotalSpaceAvailable import ac.mdiq.podcini.storage.utils.StorageUtils.getTotalSpaceAvailable
import ac.mdiq.podcini.ui.activity.PreferenceActivity import ac.mdiq.podcini.ui.activity.PreferenceActivity
@ -79,7 +78,7 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
setDataFolderText() // setDataFolderText()
} }
private fun setupNetworkScreen() { private fun setupNetworkScreen() {
@ -93,13 +92,13 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere
dialog.show() dialog.show()
true true
} }
findPreference<Preference>(PREF_CHOOSE_DATA_DIR)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { // findPreference<Preference>(PREF_CHOOSE_DATA_DIR)?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
ChooseDataFolderDialog.showDialog(requireContext()) { path: String? -> // ChooseDataFolderDialog.showDialog(requireContext()) { path: String? ->
setDataFolder(path!!) // setDataFolder(path!!)
setDataFolderText() //// setDataFolderText()
} // }
true // true
} // }
findPreference<Preference>(PREF_AUTO_DELETE_LOCAL)!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> findPreference<Preference>(PREF_AUTO_DELETE_LOCAL)!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
if (blockAutoDeleteLocal && newValue as Boolean) { if (blockAutoDeleteLocal && newValue as Boolean) {
showAutoDeleteEnableDialog() showAutoDeleteEnableDialog()
@ -108,10 +107,10 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere
} }
} }
private fun setDataFolderText() { // private fun setDataFolderText() {
val f = getDataFolder(null) // val f = getDataFolder(null)
if (f != null) findPreference<Preference>(PREF_CHOOSE_DATA_DIR)!!.summary = f.absolutePath // if (f != null) findPreference<Preference>(PREF_CHOOSE_DATA_DIR)!!.summary = f.absolutePath
} // }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
if (UserPreferences.PREF_UPDATE_INTERVAL == key) restartUpdateAlarm(requireContext(), true) if (UserPreferences.PREF_UPDATE_INTERVAL == key) restartUpdateAlarm(requireContext(), true)
@ -444,6 +443,6 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere
private const val PREF_SCREEN_AUTODL = "prefAutoDownloadSettings" private const val PREF_SCREEN_AUTODL = "prefAutoDownloadSettings"
private const val PREF_AUTO_DELETE_LOCAL = "prefAutoDeleteLocal" private const val PREF_AUTO_DELETE_LOCAL = "prefAutoDeleteLocal"
private const val PREF_PROXY = "prefProxy" private const val PREF_PROXY = "prefProxy"
private const val PREF_CHOOSE_DATA_DIR = "prefChooseDataDir" // private const val PREF_CHOOSE_DATA_DIR = "prefChooseDataDir"
} }
} }

View File

@ -9,7 +9,6 @@ import ac.mdiq.podcini.net.sync.model.EpisodeAction.Companion.readFromJsonObject
import ac.mdiq.podcini.net.sync.model.SyncServiceException import ac.mdiq.podcini.net.sync.model.SyncServiceException
import ac.mdiq.podcini.preferences.ExportWriter import ac.mdiq.podcini.preferences.ExportWriter
import ac.mdiq.podcini.preferences.OpmlTransporter.* import ac.mdiq.podcini.preferences.OpmlTransporter.*
import ac.mdiq.podcini.preferences.UserPreferences.getDataFolder
import ac.mdiq.podcini.storage.database.Episodes.getEpisodeByGuidOrUrl import ac.mdiq.podcini.storage.database.Episodes.getEpisodeByGuidOrUrl
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
import ac.mdiq.podcini.storage.database.Feeds.getFeedList import ac.mdiq.podcini.storage.database.Feeds.getFeedList
@ -21,6 +20,7 @@ import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
import ac.mdiq.podcini.storage.utils.FileNameGenerator.generateFileName import ac.mdiq.podcini.storage.utils.FileNameGenerator.generateFileName
import ac.mdiq.podcini.storage.utils.FilesUtils.getDataFolder
import ac.mdiq.podcini.ui.activity.OpmlImportActivity import ac.mdiq.podcini.ui.activity.OpmlImportActivity
import ac.mdiq.podcini.ui.activity.PreferenceActivity import ac.mdiq.podcini.ui.activity.PreferenceActivity
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
@ -102,10 +102,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
result: ActivityResult -> this.restoreMediaFilesResult(result) } result: ActivityResult -> this.restoreMediaFilesResult(result) }
private val backupMediaFilesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { private val backupMediaFilesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) { result: ActivityResult -> this.exportMediaFilesResult(result)
val data: Uri? = it.data?.data
if (data != null) MediaFilesTransporter.exportToDocument(data, requireContext())
}
} }
private var progressDialog: ProgressDialog? = null private var progressDialog: ProgressDialog? = null
@ -194,7 +191,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
showExportSuccessSnackbar(fileUri, exportType.contentType) showExportSuccessSnackbar(fileUri, exportType.contentType)
} }
} catch (e: Exception) { } catch (e: Exception) {
showExportErrorDialog(e) showTransportErrorDialog(e)
} finally { } finally {
progressDialog!!.dismiss() progressDialog!!.dismiss()
} }
@ -208,7 +205,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
showExportSuccessSnackbar(output.uri, exportType.contentType) showExportSuccessSnackbar(output.uri, exportType.contentType)
} }
} catch (e: Exception) { } catch (e: Exception) {
showExportErrorDialog(e) showTransportErrorDialog(e)
} finally { } finally {
progressDialog!!.dismiss() progressDialog!!.dismiss()
} }
@ -290,7 +287,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
builder.show() builder.show()
} }
private fun showDatabaseImportSuccessDialog() { private fun showImportSuccessDialog() {
val builder = MaterialAlertDialogBuilder(requireContext()) val builder = MaterialAlertDialogBuilder(requireContext())
builder.setTitle(R.string.successful_import_label) builder.setTitle(R.string.successful_import_label)
builder.setMessage(R.string.import_ok) builder.setMessage(R.string.import_ok)
@ -305,11 +302,11 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
.show() .show()
} }
private fun showExportErrorDialog(error: Throwable) { private fun showTransportErrorDialog(error: Throwable) {
progressDialog!!.dismiss() progressDialog!!.dismiss()
val alert = MaterialAlertDialogBuilder(requireContext()) val alert = MaterialAlertDialogBuilder(requireContext())
alert.setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int -> dialog.dismiss() } alert.setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int -> dialog.dismiss() }
alert.setTitle(R.string.export_error_label) alert.setTitle(R.string.import_export_error_label)
alert.setMessage(error.message) alert.setMessage(error.message)
alert.show() alert.show()
} }
@ -371,17 +368,17 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
reader.close() reader.close()
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
showDatabaseImportSuccessDialog() showImportSuccessDialog()
progressDialog!!.dismiss() progressDialog!!.dismiss()
} }
} catch (e: Throwable) { } catch (e: Throwable) {
showExportErrorDialog(e) showTransportErrorDialog(e)
} }
} }
} else { } else {
val context = requireContext() val context = requireContext()
val message = context.getString(R.string.import_file_type_toast) + ".json" val message = context.getString(R.string.import_file_type_toast) + ".json"
showExportErrorDialog(Throwable(message)) showTransportErrorDialog(Throwable(message))
} }
} }
} }
@ -403,17 +400,17 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
DatabaseTransporter.importBackup(uri, requireContext()) DatabaseTransporter.importBackup(uri, requireContext())
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
showDatabaseImportSuccessDialog() showImportSuccessDialog()
progressDialog!!.dismiss() progressDialog!!.dismiss()
} }
} catch (e: Throwable) { } catch (e: Throwable) {
showExportErrorDialog(e) showTransportErrorDialog(e)
} }
} }
} else { } else {
val context = requireContext() val context = requireContext()
val message = context.getString(R.string.import_file_type_toast) + ".realm" val message = context.getString(R.string.import_file_type_toast) + ".realm"
showExportErrorDialog(Throwable(message)) showTransportErrorDialog(Throwable(message))
} }
} }
} }
@ -444,17 +441,17 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
PreferencesTransporter.importBackup(uri, requireContext()) PreferencesTransporter.importBackup(uri, requireContext())
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
showDatabaseImportSuccessDialog() showImportSuccessDialog()
progressDialog!!.dismiss() progressDialog!!.dismiss()
} }
} catch (e: Throwable) { } catch (e: Throwable) {
showExportErrorDialog(e) showTransportErrorDialog(e)
} }
} }
} else { } else {
val context = requireContext() val context = requireContext()
val message = context.getString(R.string.import_directory_toast) + "Podcini-Prefs" val message = context.getString(R.string.import_directory_toast) + "Podcini-Prefs"
showExportErrorDialog(Throwable(message)) showTransportErrorDialog(Throwable(message))
} }
} }
@ -469,20 +466,38 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
MediaFilesTransporter.importBackup(uri, requireContext()) MediaFilesTransporter.importBackup(uri, requireContext())
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
showDatabaseImportSuccessDialog() showImportSuccessDialog()
progressDialog!!.dismiss() progressDialog!!.dismiss()
} }
} catch (e: Throwable) { } catch (e: Throwable) {
showExportErrorDialog(e) showTransportErrorDialog(e)
} }
} }
} else { } else {
val context = requireContext() val context = requireContext()
val message = context.getString(R.string.import_directory_toast) + "Podcini-MediaFiles" val message = context.getString(R.string.import_directory_toast) + "Podcini-MediaFiles"
showExportErrorDialog(Throwable(message)) showTransportErrorDialog(Throwable(message))
} }
} }
private fun exportMediaFilesResult(result: ActivityResult) {
if (result.resultCode != RESULT_OK || result.data?.data == null) return
val uri = result.data!!.data!!
progressDialog!!.show()
lifecycleScope.launch {
try {
withContext(Dispatchers.IO) {
MediaFilesTransporter.exportToDocument(uri, requireContext())
}
withContext(Dispatchers.Main) {
showExportSuccessSnackbar(uri, null)
progressDialog!!.dismiss()
}
} catch (e: Throwable) {
showTransportErrorDialog(e)
}
}
}
private fun backupDatabaseResult(uri: Uri?) { private fun backupDatabaseResult(uri: Uri?) {
if (uri == null) return if (uri == null) return
@ -497,7 +512,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
progressDialog!!.dismiss() progressDialog!!.dismiss()
} }
} catch (e: Throwable) { } catch (e: Throwable) {
showExportErrorDialog(e) showTransportErrorDialog(e)
} }
} }
} }

View File

@ -1,8 +1,10 @@
package ac.mdiq.podcini.storage.algorithms package ac.mdiq.podcini.storage.algorithms
import ac.mdiq.podcini.preferences.UserPreferences 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.episodeCacheSize
import ac.mdiq.podcini.preferences.UserPreferences.episodeCleanupValue
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
import ac.mdiq.podcini.storage.database.Episodes.deleteMediaOfEpisode import ac.mdiq.podcini.storage.database.Episodes.deleteMediaOfEpisode
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
@ -22,6 +24,12 @@ import java.util.concurrent.ExecutionException
object AutoCleanups { object AutoCleanups {
var episodeCleanupValue: Int
get() = appPrefs.getString(PREF_EPISODE_CLEANUP, "" + EPISODE_CLEANUP_NULL)!!.toInt()
set(episodeCleanupValue) {
appPrefs.edit().putString(PREF_EPISODE_CLEANUP, episodeCleanupValue.toString()).apply()
}
/** /**
* Removed downloaded episodes outside of the queue if the episode cache is full. Episodes with a smaller * Removed downloaded episodes outside of the queue if the episode cache is full. Episodes with a smaller
* 'playbackCompletionDate'-value will be deleted first. * 'playbackCompletionDate'-value will be deleted first.

View File

@ -9,8 +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.curState
import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.ACTION_SHUTDOWN_PLAYBACK_SERVICE 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.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue
import ac.mdiq.podcini.preferences.UserPreferences.shouldRemoveFromQueuesMarkPlayed
import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueuesSync import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueuesSync
import ac.mdiq.podcini.storage.database.Queues.removeFromQueueSync import ac.mdiq.podcini.storage.database.Queues.removeFromQueueSync
import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.realm
@ -84,12 +85,6 @@ object Episodes {
return if (episode != null) realm.copyFromRealm(episode) else null return if (episode != null) realm.copyFromRealm(episode) else null
} }
fun getEpisodeByTitle(title: String): Episode? {
Logd(TAG, "getEpisodeByTitle called $title ")
val episode = realm.query(Episode::class).query("title == $0", title).first().find()
return if (episode != null) realm.copyFromRealm(episode) else null
}
fun getEpisodeMedia(mediaId: Long): EpisodeMedia? { fun getEpisodeMedia(mediaId: Long): EpisodeMedia? {
Logd(TAG, "getEpisodeMedia called $mediaId") Logd(TAG, "getEpisodeMedia called $mediaId")
val media = realm.query(EpisodeMedia::class).query("id == $0", mediaId).first().find() val media = realm.query(EpisodeMedia::class).query("id == $0", mediaId).first().find()
@ -290,4 +285,8 @@ object Episodes {
EventFlow.postEvent(FlowEvent.EpisodePlayedEvent(result)) EventFlow.postEvent(FlowEvent.EpisodePlayedEvent(result))
return result return result
} }
private fun shouldRemoveFromQueuesMarkPlayed(): Boolean {
return appPrefs.getBoolean(PREF_REMOVDE_FROM_QUEUE_MARKED_PLAYED, true)
}
} }

View File

@ -3,21 +3,18 @@ package ac.mdiq.podcini.storage.database
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
import ac.mdiq.podcini.playback.base.InTheatre.curMedia import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.preferences.UserPreferences.EnqueueLocation import ac.mdiq.podcini.preferences.UserPreferences.PREF_ENQUEUE_LOCATION
import ac.mdiq.podcini.preferences.UserPreferences.enqueueLocation import ac.mdiq.podcini.preferences.UserPreferences.PREF_QUEUE_KEEP_SORTED
import ac.mdiq.podcini.preferences.UserPreferences.isQueueKeepSorted import ac.mdiq.podcini.preferences.UserPreferences.PREF_QUEUE_KEEP_SORTED_ORDER
import ac.mdiq.podcini.preferences.UserPreferences.queueKeepSortedOrder import ac.mdiq.podcini.preferences.UserPreferences.PREF_QUEUE_LOCKED
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia
import ac.mdiq.podcini.storage.database.Episodes.setPlayState import ac.mdiq.podcini.storage.database.Episodes.setPlayState
import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsert import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.PlayQueue
import ac.mdiq.podcini.storage.model.Playable
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent import ac.mdiq.podcini.util.event.FlowEvent
@ -32,6 +29,10 @@ import java.util.*
object Queues { object Queues {
private val TAG: String = Queues::class.simpleName ?: "Anonymous" private val TAG: String = Queues::class.simpleName ?: "Anonymous"
enum class EnqueueLocation {
BACK, FRONT, AFTER_CURRENTLY_PLAYING, RANDOM
}
fun getInQueueEpisodeIds(): Set<Long> { fun getInQueueEpisodeIds(): Set<Long> {
Logd(TAG, "getQueueIDList() called") Logd(TAG, "getQueueIDList() called")
val queues = realm.query(PlayQueue::class).find() val queues = realm.query(PlayQueue::class).find()
@ -273,6 +274,60 @@ object Queues {
upsertBlk(curQueue) {} upsertBlk(curQueue) {}
} }
var isQueueLocked: Boolean
get() = appPrefs.getBoolean(PREF_QUEUE_LOCKED, false)
set(locked) {
appPrefs.edit().putBoolean(PREF_QUEUE_LOCKED, locked).apply()
}
var isQueueKeepSorted: Boolean
/**
* Returns if the queue is in keep sorted mode.
* @see .queueKeepSortedOrder
*/
get() = appPrefs.getBoolean(PREF_QUEUE_KEEP_SORTED, false)
/**
* Enables/disables the keep sorted mode of the queue.
* @see .queueKeepSortedOrder
*/
set(keepSorted) {
appPrefs.edit().putBoolean(PREF_QUEUE_KEEP_SORTED, keepSorted).apply()
}
var queueKeepSortedOrder: EpisodeSortOrder?
/**
* Returns the sort order for the queue keep sorted mode.
* Note: This value is stored independently from the keep sorted state.
* @see .isQueueKeepSorted
*/
get() {
val sortOrderStr = appPrefs.getString(PREF_QUEUE_KEEP_SORTED_ORDER, "use-default")
return EpisodeSortOrder.parseWithDefault(sortOrderStr, EpisodeSortOrder.DATE_NEW_OLD)
}
/**
* Sets the sort order for the queue keep sorted mode.
* @see .setQueueKeepSorted
*/
set(sortOrder) {
if (sortOrder == null) return
appPrefs.edit().putString(PREF_QUEUE_KEEP_SORTED_ORDER, sortOrder.name).apply()
}
var enqueueLocation: EnqueueLocation
get() {
val valStr = appPrefs.getString(PREF_ENQUEUE_LOCATION, EnqueueLocation.BACK.name)
try {
return EnqueueLocation.valueOf(valStr!!)
} catch (t: Throwable) {
// should never happen but just in case
Log.e(TAG, "getEnqueueLocation: invalid value '$valStr' Use default.", t)
return EnqueueLocation.BACK
}
}
set(location) {
appPrefs.edit().putString(PREF_ENQUEUE_LOCATION, location.name).apply()
}
class EnqueuePositionCalculator(private val enqueueLocation: EnqueueLocation) { class EnqueuePositionCalculator(private val enqueueLocation: EnqueueLocation) {
/** /**
* Determine the position (0-based) that the item(s) should be inserted to the named queue. * Determine the position (0-based) that the item(s) should be inserted to the named queue.

View File

@ -1,12 +1,14 @@
package ac.mdiq.podcini.storage.utils package ac.mdiq.podcini.storage.utils
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.PREF_SMART_MARK_AS_PLAYED_SECS
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.Playable import ac.mdiq.podcini.storage.model.Playable
import org.apache.commons.lang3.StringUtils
object EpisodeUtil { object EpisodeUtil {
private val TAG: String = EpisodeUtil::class.simpleName ?: "Anonymous" private val TAG: String = EpisodeUtil::class.simpleName ?: "Anonymous"
val smartMarkAsPlayedSecs: Int
get() = appPrefs.getString(PREF_SMART_MARK_AS_PLAYED_SECS, "30")!!.toInt()
@JvmStatic @JvmStatic
fun indexOfItemWithId(episodes: List<Episode?>, id: Long): Int { fun indexOfItemWithId(episodes: List<Episode?>, id: Long): Int {
@ -43,7 +45,6 @@ object EpisodeUtil {
@JvmStatic @JvmStatic
fun hasAlmostEnded(media: Playable): Boolean { fun hasAlmostEnded(media: Playable): Boolean {
val smartMarkAsPlayedSecs = UserPreferences.smartMarkAsPlayedSecs
return media.getDuration() > 0 && media.getPosition() >= media.getDuration() - smartMarkAsPlayedSecs * 1000 return media.getDuration() > 0 && media.getPosition() >= media.getDuration() - smartMarkAsPlayedSecs * 1000
} }
} }

View File

@ -13,10 +13,7 @@ object FileNameGenerator {
const val MAX_FILENAME_LENGTH: Int = 242 // limited by CircleCI const val MAX_FILENAME_LENGTH: Int = 242 // limited by CircleCI
private const val MD5_HEX_LENGTH = 32 private const val MD5_HEX_LENGTH = 32
private val validChars = ("abcdefghijklmnopqrstuvwxyz" private val validChars = ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 _-").toCharArray()
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "0123456789"
+ " _-").toCharArray()
/** /**
* This method will return a new string that doesn't contain any illegal * This method will return a new string that doesn't contain any illegal

View File

@ -0,0 +1,153 @@
package ac.mdiq.podcini.storage.utils
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.Logd
import android.content.Context
import android.util.Log
import android.webkit.URLUtil
import io.realm.kotlin.ext.isManaged
import org.apache.commons.io.FilenameUtils
import java.io.File
import java.io.IOException
object FilesUtils {
private val TAG: String = FilesUtils::class.simpleName ?: "Anonymous"
private const val FEED_DOWNLOADPATH = "cache/"
private const val MEDIA_DOWNLOADPATH = "media/"
lateinit var context: Context
fun findUnusedFile(dest: File): File? {
// find different name
var newDest: File? = null
for (i in 1 until Int.MAX_VALUE) {
val newName = (FilenameUtils.getBaseName(dest.name) + "-" + i + FilenameUtils.EXTENSION_SEPARATOR + FilenameUtils.getExtension(dest.name))
Logd(TAG, "Testing filename $newName")
newDest = File(dest.parent, newName)
if (!newDest.exists()) {
Logd(TAG, "File doesn't exist yet. Using $newName")
break
}
}
return newDest
}
val feedfilePath: String
get() = getDataFolder(FEED_DOWNLOADPATH).toString() + "/"
fun getFeedfileName(feed: Feed): String {
var filename = feed.downloadUrl
if (!feed.title.isNullOrEmpty()) filename = feed.title
if (filename == null) return ""
return "feed-" + FileNameGenerator.generateFileName(filename) + feed.id
}
fun getMediafilePath(media: EpisodeMedia): String {
val item = media.episode ?: return ""
Logd(TAG, "item managed: ${item.isManaged()}")
val title = item.feed?.title?:return ""
val mediaPath = (MEDIA_DOWNLOADPATH + FileNameGenerator.generateFileName(title))
return getDataFolder(mediaPath).toString() + "/"
}
fun getMediafilename(media: EpisodeMedia): String {
var titleBaseFilename = ""
// Try to generate the filename by the item title
if (media.episode?.title != null) {
val title = media.episode!!.title!!
titleBaseFilename = FileNameGenerator.generateFileName(title)
}
val urlBaseFilename = URLUtil.guessFileName(media.downloadUrl, null, media.mimeType)
var baseFilename: String
baseFilename = if (titleBaseFilename != "") titleBaseFilename else urlBaseFilename
val filenameMaxLength = 220
if (baseFilename.length > filenameMaxLength) baseFilename = baseFilename.substring(0, filenameMaxLength)
return (baseFilename + FilenameUtils.EXTENSION_SEPARATOR + media.id + FilenameUtils.EXTENSION_SEPARATOR + FilenameUtils.getExtension(urlBaseFilename))
}
fun getMediafilePath(item: Episode): String {
val title = item.feed?.title?:return ""
val mediaPath = (MEDIA_DOWNLOADPATH + FileNameGenerator.generateFileName(title))
return getDataFolder(mediaPath).toString() + "/"
}
fun getMediafilename(item: Episode): String {
var titleBaseFilename = ""
// Try to generate the filename by the item title
if (item.title != null) {
val title = item.title!!
titleBaseFilename = FileNameGenerator.generateFileName(title)
}
var baseFilename: String
baseFilename = if (titleBaseFilename != "") titleBaseFilename else "NoTitle"
val filenameMaxLength = 220
if (baseFilename.length > filenameMaxLength) baseFilename = baseFilename.substring(0, filenameMaxLength)
return (baseFilename + FilenameUtils.EXTENSION_SEPARATOR + "noid" + FilenameUtils.EXTENSION_SEPARATOR + "wav")
}
fun getTypeDir(baseDirPath: String?, type: String?): File? {
if (baseDirPath == null) return null
val baseDir = File(baseDirPath)
val typeDir = if (type == null) baseDir else File(baseDir, type)
if (!typeDir.exists()) {
if (!baseDir.canWrite()) {
Log.e(TAG, "Base dir is not writable " + baseDir.absolutePath)
return null
}
if (!typeDir.mkdirs()) {
Log.e(TAG, "Could not create type dir " + typeDir.absolutePath)
return null
}
}
return typeDir
}
/**
* Return the folder where the app stores all of its data. This method will
* return the standard data folder if none has been set by the user.
* @param type The name of the folder inside the data folder. May be null
* when accessing the root of the data folder.
* @return The data folder that has been requested or null if the folder could not be created.
*/
fun getDataFolder(type: String?): File? {
var dataFolder = getTypeDir(null, type)
if (dataFolder == null || !dataFolder.canWrite()) {
Logd(TAG, "User data folder not writable or not set. Trying default.")
dataFolder = context.getExternalFilesDir(type)
}
if (dataFolder == null || !dataFolder.canWrite()) {
Logd(TAG, "Default data folder not available or not writable. Falling back to internal memory.")
dataFolder = getTypeDir(context.filesDir.absolutePath, type)
}
return dataFolder
}
/**
* Create a .nomedia file to prevent scanning by the media scanner.
*/
fun createNoMediaFile() {
val f = File(context.getExternalFilesDir(null), ".nomedia")
if (!f.exists()) {
try {
f.createNewFile()
} catch (e: IOException) {
Log.e(TAG, "Could not create .nomedia file")
e.printStackTrace()
}
Logd(TAG, ".nomedia file created")
}
}
}

View File

@ -4,17 +4,25 @@ import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.Playable import ac.mdiq.podcini.storage.model.Playable
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.PREF_USE_EPISODE_COVER
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
/** /**
* Utility class to use the appropriate image resource based on [UserPreferences]. * Utility class to use the appropriate image resource based on [UserPreferences].
*/ */
object ImageResourceUtils { object ImageResourceUtils {
/**
* @return `true` if episodes should use their own cover, `false` otherwise
*/
val useEpisodeCoverSetting: Boolean
get() = appPrefs.getBoolean(PREF_USE_EPISODE_COVER, true)
/** /**
* returns the image location, does prefer the episode cover if available and enabled in settings. * returns the image location, does prefer the episode cover if available and enabled in settings.
*/ */
@JvmStatic @JvmStatic
fun getEpisodeListImageLocation(playable: Playable): String? { fun getEpisodeListImageLocation(playable: Playable): String? {
return if (UserPreferences.useEpisodeCoverSetting) playable.getImageLocation() return if (useEpisodeCoverSetting) playable.getImageLocation()
else getFallbackImageLocation(playable) else getFallbackImageLocation(playable)
} }
@ -23,7 +31,7 @@ object ImageResourceUtils {
*/ */
@JvmStatic @JvmStatic
fun getEpisodeListImageLocation(episode: Episode): String? { fun getEpisodeListImageLocation(episode: Episode): String? {
return if (UserPreferences.useEpisodeCoverSetting) episode.imageLocation return if (useEpisodeCoverSetting) episode.imageLocation
else getFallbackImageLocation(episode) else getFallbackImageLocation(episode)
} }

View File

@ -2,6 +2,7 @@ package ac.mdiq.podcini.storage.utils
import android.os.StatFs import android.os.StatFs
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.utils.FilesUtils.getDataFolder
/** /**
* Utility functions for handling storage errors * Utility functions for handling storage errors
@ -13,7 +14,7 @@ object StorageUtils {
@JvmStatic @JvmStatic
val freeSpaceAvailable: Long val freeSpaceAvailable: Long
get() { get() {
val dataFolder = UserPreferences.getDataFolder(null) val dataFolder = getDataFolder(null)
return if (dataFolder != null) getFreeSpaceAvailable(dataFolder.absolutePath) else 0 return if (dataFolder != null) getFreeSpaceAvailable(dataFolder.absolutePath) else 0
} }

View File

@ -8,6 +8,12 @@ import android.view.View
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
class DeleteActionButton(item: Episode) : EpisodeActionButton(item) { class DeleteActionButton(item: Episode) : EpisodeActionButton(item) {
override val visibility: Int
get() {
if (item.media != null && (item.media!!.downloaded || item.feed?.isLocalFeed == true)) return View.VISIBLE
return View.INVISIBLE
}
override fun getLabel(): Int { override fun getLabel(): Int {
return R.string.delete_label return R.string.delete_label
} }
@ -17,10 +23,4 @@ class DeleteActionButton(item: Episode) : EpisodeActionButton(item) {
@UnstableApi override fun onClick(context: Context) { @UnstableApi override fun onClick(context: Context) {
deleteEpisodesWarnLocal(context, listOf(item)) deleteEpisodesWarnLocal(context, listOf(item))
} }
override val visibility: Int
get() {
if (item.media != null && (item.media!!.downloaded || item.feed?.isLocalFeed == true)) return View.VISIBLE
return View.INVISIBLE
}
} }

View File

@ -15,14 +15,16 @@ import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
class DownloadActionButton(item: Episode) : EpisodeActionButton(item) { class DownloadActionButton(item: Episode) : EpisodeActionButton(item) {
override val visibility: Int
get() = if (item.feed?.isLocalFeed == true) View.INVISIBLE else View.VISIBLE
override fun getLabel(): Int { override fun getLabel(): Int {
return R.string.download_label return R.string.download_label
} }
override fun getDrawable(): Int { override fun getDrawable(): Int {
return R.drawable.ic_download return R.drawable.ic_download
} }
override val visibility: Int
get() = if (item.feed?.isLocalFeed == true) View.INVISIBLE else View.VISIBLE
override fun onClick(context: Context) { override fun onClick(context: Context) {
val media = item.media val media = item.media

View File

@ -13,17 +13,17 @@ import androidx.media3.common.util.UnstableApi
abstract class EpisodeActionButton internal constructor(@JvmField var item: Episode) { abstract class EpisodeActionButton internal constructor(@JvmField var item: Episode) {
val TAG = this::class.simpleName ?: "ItemActionButton" val TAG = this::class.simpleName ?: "ItemActionButton"
open val visibility: Int
get() = View.VISIBLE
var processing: Float = -1f
abstract fun getLabel(): Int abstract fun getLabel(): Int
abstract fun getDrawable(): Int abstract fun getDrawable(): Int
abstract fun onClick(context: Context) abstract fun onClick(context: Context)
open val visibility: Int
get() = View.VISIBLE
var processing: Float = -1f
fun configure(button: View, icon: ImageView, context: Context) { fun configure(button: View, icon: ImageView, context: Context) {
button.visibility = visibility button.visibility = visibility
button.contentDescription = context.getString(getLabel()) button.contentDescription = context.getString(getLabel())

View File

@ -8,16 +8,19 @@ import ac.mdiq.podcini.storage.model.Episode
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
class MarkAsPlayedActionButton(item: Episode) : EpisodeActionButton(item) { class MarkAsPlayedActionButton(item: Episode) : EpisodeActionButton(item) {
override val visibility: Int
get() = if (item.isPlayed()) View.INVISIBLE else View.VISIBLE
override fun getLabel(): Int { override fun getLabel(): Int {
return (if (item.media != null) R.string.mark_read_label else R.string.mark_read_no_media_label) return (if (item.media != null) R.string.mark_read_label else R.string.mark_read_no_media_label)
} }
override fun getDrawable(): Int { override fun getDrawable(): Int {
return R.drawable.ic_check return R.drawable.ic_check
} }
@UnstableApi override fun onClick(context: Context) { @UnstableApi override fun onClick(context: Context) {
if (!item.isPlayed()) setPlayState(Episode.PLAYED, true, item) if (!item.isPlayed()) setPlayState(Episode.PLAYED, true, item)
} }
override val visibility: Int
get() = if (item.isPlayed()) View.INVISIBLE else View.VISIBLE
} }

View File

@ -1,10 +1,10 @@
package ac.mdiq.podcini.ui.actions.actionbutton package ac.mdiq.podcini.ui.actions.actionbutton
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileStreaming
import ac.mdiq.podcini.playback.PlaybackServiceStarter import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.preferences.UsageStatistics import ac.mdiq.podcini.preferences.UsageStatistics
import ac.mdiq.podcini.preferences.UsageStatistics.logAction import ac.mdiq.podcini.preferences.UsageStatistics.logAction
import ac.mdiq.podcini.preferences.UserPreferences.isAllowMobileStreaming
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.MediaType import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.storage.model.Playable import ac.mdiq.podcini.storage.model.Playable

View File

@ -1,18 +1,18 @@
package ac.mdiq.podcini.ui.actions.actionbutton package ac.mdiq.podcini.ui.actions.actionbutton
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.getMediafilePath import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.getMediafilename import ac.mdiq.podcini.storage.database.Episodes.persistEpisode
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.utils.FilesUtils.getMediafilePath
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.tts
import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment.Companion.ttsReady import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment.Companion.ttsReady
import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment.Companion.ttsWorking import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment.Companion.ttsWorking
import ac.mdiq.podcini.util.AudioMediaOperation.mergeAudios import ac.mdiq.podcini.util.AudioMediaOperation.mergeAudios
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource
import ac.mdiq.podcini.storage.database.Episodes.persistEpisode
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent import ac.mdiq.podcini.util.event.FlowEvent
import android.content.Context import android.content.Context
@ -25,7 +25,10 @@ import android.widget.Toast
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import net.dankito.readability4j.Readability4J import net.dankito.readability4j.Readability4J
import java.io.File import java.io.File
import java.util.* import java.util.*
@ -35,7 +38,9 @@ import kotlin.math.min
class TTSActionButton(item: Episode) : EpisodeActionButton(item) { class TTSActionButton(item: Episode) : EpisodeActionButton(item) {
private var readerText: String? = null private var readerText: String? = null
// private val ioScope = CoroutineScope(Dispatchers.IO)
override val visibility: Int
get() = if (item.link.isNullOrEmpty()) View.INVISIBLE else View.VISIBLE
override fun getLabel(): Int { override fun getLabel(): Int {
return R.string.TTS_label return R.string.TTS_label
@ -151,7 +156,4 @@ class TTSActionButton(item: Episode) : EpisodeActionButton(item) {
EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item)) EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item))
} }
} }
override val visibility: Int
get() = if (item.link.isNullOrEmpty()) View.INVISIBLE else View.VISIBLE
} }

View File

@ -7,16 +7,18 @@ import ac.mdiq.podcini.util.IntentUtils.openInBrowser
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
class VisitWebsiteActionButton(item: Episode) : EpisodeActionButton(item) { class VisitWebsiteActionButton(item: Episode) : EpisodeActionButton(item) {
override val visibility: Int
get() = if (item.link.isNullOrEmpty()) View.INVISIBLE else View.VISIBLE
override fun getLabel(): Int { override fun getLabel(): Int {
return R.string.visit_website_label return R.string.visit_website_label
} }
override fun getDrawable(): Int { override fun getDrawable(): Int {
return R.drawable.ic_web return R.drawable.ic_web
} }
override fun onClick(context: Context) { override fun onClick(context: Context) {
if (!item.link.isNullOrEmpty()) openInBrowser(context, item.link!!) if (!item.link.isNullOrEmpty()) openInBrowser(context, item.link!!)
} }
override val visibility: Int
get() = if (item.link.isNullOrEmpty()) View.INVISIBLE else View.VISIBLE
} }

View File

@ -3,7 +3,7 @@ package ac.mdiq.podcini.ui.activity
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.BugReportBinding import ac.mdiq.podcini.databinding.BugReportBinding
import ac.mdiq.podcini.preferences.ThemeSwitcher.getTheme import ac.mdiq.podcini.preferences.ThemeSwitcher.getTheme
import ac.mdiq.podcini.preferences.UserPreferences.getDataFolder import ac.mdiq.podcini.storage.utils.FilesUtils.getDataFolder
import ac.mdiq.podcini.util.IntentUtils.openInBrowser import ac.mdiq.podcini.util.IntentUtils.openInBrowser
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.error.CrashReportWriter import ac.mdiq.podcini.util.error.CrashReportWriter

View File

@ -4,13 +4,13 @@ import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.SortDialogBinding import ac.mdiq.podcini.databinding.SortDialogBinding
import ac.mdiq.podcini.databinding.SortDialogItemActiveBinding import ac.mdiq.podcini.databinding.SortDialogItemActiveBinding
import ac.mdiq.podcini.databinding.SortDialogItemBinding import ac.mdiq.podcini.databinding.SortDialogItemBinding
import ac.mdiq.podcini.preferences.UserPreferences.feedOrderBy import ac.mdiq.podcini.preferences.UserPreferences.PREF_DRAWER_FEED_ORDER
import ac.mdiq.podcini.preferences.UserPreferences.feedOrderDir import ac.mdiq.podcini.preferences.UserPreferences.PREF_DRAWER_FEED_ORDER_DIRECTION
import ac.mdiq.podcini.preferences.UserPreferences.setFeedOrder import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.model.FeedSortOrder import ac.mdiq.podcini.storage.model.FeedSortOrder
import ac.mdiq.podcini.storage.model.FeedSortOrder.Companion.getSortOrder import ac.mdiq.podcini.storage.model.FeedSortOrder.Companion.getSortOrder
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion.feedOrderBy
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion.feedOrderDir
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent import ac.mdiq.podcini.util.event.FlowEvent
@ -134,4 +134,13 @@ open class FeedSortDialogNew : BottomSheetDialogFragment() {
behavior.state = BottomSheetBehavior.STATE_EXPANDED behavior.state = BottomSheetBehavior.STATE_EXPANDED
} }
} }
private fun setFeedOrder(selected: String, dir: Int) {
appPrefs.edit()
.putString(PREF_DRAWER_FEED_ORDER, selected)
.apply()
appPrefs.edit()
.putInt(PREF_DRAWER_FEED_ORDER_DIRECTION, dir)
.apply()
}
} }

View File

@ -9,15 +9,14 @@ import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.InTheatre.curState import ac.mdiq.podcini.playback.base.InTheatre.curState
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.currentMediaType import ac.mdiq.podcini.playback.service.PlaybackService.Companion.currentMediaType
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.PREF_PLAYBACK_SPEED_ARRAY
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
import ac.mdiq.podcini.preferences.UserPreferences.playbackSpeedArray
import ac.mdiq.podcini.preferences.UserPreferences.videoPlaybackSpeed import ac.mdiq.podcini.preferences.UserPreferences.videoPlaybackSpeed
import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.MediaType import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion
import ac.mdiq.podcini.ui.utils.ItemOffsetDecoration import ac.mdiq.podcini.ui.utils.ItemOffsetDecoration
import ac.mdiq.podcini.ui.view.PlaybackSpeedSeekBar import ac.mdiq.podcini.ui.view.PlaybackSpeedSeekBar
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
@ -42,6 +41,9 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.json.JSONArray
import org.json.JSONException
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols import java.text.DecimalFormatSymbols
import java.util.* import java.util.*
@ -177,6 +179,37 @@ import java.util.*
} }
} }
var playbackSpeedArray: List<Float>
get() = readPlaybackSpeedArray(appPrefs.getString(PREF_PLAYBACK_SPEED_ARRAY, null))
set(speeds) {
val format = DecimalFormatSymbols(Locale.US)
format.decimalSeparator = '.'
val speedFormat = DecimalFormat("0.00", format)
val jsonArray = JSONArray()
for (speed in speeds) {
jsonArray.put(speedFormat.format(speed.toDouble()))
}
appPrefs.edit().putString(PREF_PLAYBACK_SPEED_ARRAY, jsonArray.toString()).apply()
}
private fun readPlaybackSpeedArray(valueFromPrefs: String?): List<Float> {
if (valueFromPrefs != null) {
try {
val jsonArray = JSONArray(valueFromPrefs)
val selectedSpeeds: MutableList<Float> = ArrayList()
for (i in 0 until jsonArray.length()) {
selectedSpeeds.add(jsonArray.getDouble(i).toFloat())
}
return selectedSpeeds
} catch (e: JSONException) {
Log.e(TAG, "Got JSON error when trying to get speeds from JSONArray")
e.printStackTrace()
}
}
// If this preference hasn't been set yet, return the default options
return mutableListOf(1.0f, 1.25f, 1.5f)
}
inner class SpeedSelectionAdapter : RecyclerView.Adapter<SpeedSelectionAdapter.ViewHolder>() { inner class SpeedSelectionAdapter : RecyclerView.Adapter<SpeedSelectionAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val chip = Chip(context) val chip = Chip(context)

View File

@ -1,8 +1,9 @@
package ac.mdiq.podcini.ui.fragment package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.preferences.UserPreferences.allEpisodesSortOrder import ac.mdiq.podcini.preferences.UserPreferences.PREF_FILTER_ALL_EPISODES
import ac.mdiq.podcini.preferences.UserPreferences.prefFilterAllEpisodes import ac.mdiq.podcini.preferences.UserPreferences.PREF_SORT_ALL_EPISODES
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
import ac.mdiq.podcini.storage.database.Episodes.getEpisodesCount import ac.mdiq.podcini.storage.database.Episodes.getEpisodesCount
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
@ -12,7 +13,6 @@ import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.dialog.EpisodeFilterDialog import ac.mdiq.podcini.ui.dialog.EpisodeFilterDialog
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
import ac.mdiq.podcini.ui.dialog.SwitchQueueDialog import ac.mdiq.podcini.ui.dialog.SwitchQueueDialog
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent import ac.mdiq.podcini.util.event.FlowEvent
@ -35,7 +35,7 @@ import kotlin.math.min
*/ */
@UnstableApi class AllEpisodesFragment : BaseEpisodesFragment() { @UnstableApi class AllEpisodesFragment : BaseEpisodesFragment() {
var allEpisodes: List<Episode> = listOf() private var allEpisodes: List<Episode> = listOf()
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { @UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val root = super.onCreateView(inflater, container, savedInstanceState) val root = super.onCreateView(inflater, container, savedInstanceState)
@ -181,8 +181,19 @@ import kotlin.math.min
} }
} }
} }
companion object { companion object {
val TAG = AllEpisodesFragment::class.simpleName ?: "Anonymous" val TAG = AllEpisodesFragment::class.simpleName ?: "Anonymous"
const val PREF_NAME: String = "PrefAllEpisodesFragment" const val PREF_NAME: String = "PrefAllEpisodesFragment"
var allEpisodesSortOrder: EpisodeSortOrder?
get() = EpisodeSortOrder.fromCodeString(appPrefs.getString(PREF_SORT_ALL_EPISODES, "" + EpisodeSortOrder.DATE_NEW_OLD.code))
set(s) {
appPrefs.edit().putString(PREF_SORT_ALL_EPISODES, "" + s!!.code).apply()
}
var prefFilterAllEpisodes: String
get() = appPrefs.getString(PREF_FILTER_ALL_EPISODES, "")?:""
set(filter) {
appPrefs.edit().putString(PREF_FILTER_ALL_EPISODES, filter).apply()
}
} }
} }

View File

@ -6,14 +6,15 @@ import ac.mdiq.podcini.databinding.SimpleListFragmentBinding
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
import ac.mdiq.podcini.net.feed.FeedUpdateManager import ac.mdiq.podcini.net.feed.FeedUpdateManager
import ac.mdiq.podcini.playback.base.InTheatre.isCurMedia import ac.mdiq.podcini.playback.base.InTheatre.isCurMedia
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.PREF_DOWNLOADS_SORTED_ORDER
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.EpisodeFilter import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.utils.EpisodeUtil import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.EpisodeSortOrder import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectHandler import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectHandler
import ac.mdiq.podcini.ui.actions.actionbutton.DeleteActionButton import ac.mdiq.podcini.ui.actions.actionbutton.DeleteActionButton
import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler
@ -24,7 +25,6 @@ import ac.mdiq.podcini.ui.adapter.EpisodesAdapter
import ac.mdiq.podcini.ui.adapter.SelectableAdapter import ac.mdiq.podcini.ui.adapter.SelectableAdapter
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
import ac.mdiq.podcini.ui.dialog.SwitchQueueDialog import ac.mdiq.podcini.ui.dialog.SwitchQueueDialog
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion
import ac.mdiq.podcini.ui.utils.EmptyViewHandler import ac.mdiq.podcini.ui.utils.EmptyViewHandler
import ac.mdiq.podcini.ui.utils.LiftOnScrollListener import ac.mdiq.podcini.ui.utils.LiftOnScrollListener
import ac.mdiq.podcini.ui.view.EpisodesRecyclerView import ac.mdiq.podcini.ui.view.EpisodesRecyclerView
@ -327,7 +327,7 @@ import java.util.*
lifecycleScope.launch { lifecycleScope.launch {
try { try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val sortOrder: EpisodeSortOrder? = UserPreferences.downloadsSortedOrder val sortOrder: EpisodeSortOrder? = downloadsSortedOrder
val downloadedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.DOWNLOADED), sortOrder) val downloadedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.DOWNLOADED), sortOrder)
if (runningDownloads.isEmpty()) episodes = downloadedItems.toMutableList() if (runningDownloads.isEmpty()) episodes = downloadedItems.toMutableList()
else { else {
@ -412,7 +412,7 @@ import java.util.*
class DownloadsSortDialog : EpisodeSortDialog() { class DownloadsSortDialog : EpisodeSortDialog() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
sortOrder = UserPreferences.downloadsSortedOrder sortOrder = downloadsSortedOrder
} }
override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) { override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) {
@ -426,7 +426,7 @@ import java.util.*
override fun onSelectionChanged() { override fun onSelectionChanged() {
super.onSelectionChanged() super.onSelectionChanged()
UserPreferences.downloadsSortedOrder = sortOrder downloadsSortedOrder = sortOrder
EventFlow.postEvent(FlowEvent.DownloadLogEvent()) EventFlow.postEvent(FlowEvent.DownloadLogEvent())
} }
} }
@ -436,5 +436,15 @@ import java.util.*
const val ARG_SHOW_LOGS: String = "show_logs" const val ARG_SHOW_LOGS: String = "show_logs"
private const val KEY_UP_ARROW = "up_arrow" private const val KEY_UP_ARROW = "up_arrow"
// the sort order for the downloads.
var downloadsSortedOrder: EpisodeSortOrder?
get() {
val sortOrderStr = appPrefs.getString(PREF_DOWNLOADS_SORTED_ORDER, "" + EpisodeSortOrder.DATE_NEW_OLD.code)
return EpisodeSortOrder.fromCodeString(sortOrderStr)
}
set(sortOrder) {
appPrefs.edit().putString(PREF_DOWNLOADS_SORTED_ORDER, "" + sortOrder!!.code).apply()
}
} }
} }

View File

@ -10,16 +10,19 @@ import ac.mdiq.podcini.playback.base.InTheatre.isCurMedia
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.database.Queues.clearQueue import ac.mdiq.podcini.storage.database.Queues.clearQueue
import ac.mdiq.podcini.storage.database.Queues.isQueueKeepSorted
import ac.mdiq.podcini.storage.database.Queues.isQueueLocked
import ac.mdiq.podcini.storage.database.Queues.moveInQueue import ac.mdiq.podcini.storage.database.Queues.moveInQueue
import ac.mdiq.podcini.storage.database.Queues.moveInQueueSync import ac.mdiq.podcini.storage.database.Queues.moveInQueueSync
import ac.mdiq.podcini.storage.database.Queues.queueKeepSortedOrder
import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsert import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.EpisodeFilter import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.utils.EpisodeUtil import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.EpisodeSortOrder import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectHandler import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectHandler
import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler
import ac.mdiq.podcini.ui.actions.menuhandler.MenuItemUtils import ac.mdiq.podcini.ui.actions.menuhandler.MenuItemUtils
@ -30,7 +33,6 @@ import ac.mdiq.podcini.ui.adapter.SelectableAdapter
import ac.mdiq.podcini.ui.dialog.ConfirmationDialog import ac.mdiq.podcini.ui.dialog.ConfirmationDialog
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
import ac.mdiq.podcini.ui.dialog.SwitchQueueDialog import ac.mdiq.podcini.ui.dialog.SwitchQueueDialog
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion
import ac.mdiq.podcini.ui.utils.EmptyViewHandler import ac.mdiq.podcini.ui.utils.EmptyViewHandler
import ac.mdiq.podcini.ui.utils.LiftOnScrollListener import ac.mdiq.podcini.ui.utils.LiftOnScrollListener
import ac.mdiq.podcini.ui.view.EpisodesRecyclerView import ac.mdiq.podcini.ui.view.EpisodesRecyclerView
@ -393,8 +395,8 @@ import java.util.*
} }
private fun refreshToolbarState() { private fun refreshToolbarState() {
val keepSorted: Boolean = UserPreferences.isQueueKeepSorted val keepSorted: Boolean = isQueueKeepSorted
toolbar.menu?.findItem(R.id.queue_lock)?.setChecked(UserPreferences.isQueueLocked) toolbar.menu?.findItem(R.id.queue_lock)?.setChecked(isQueueLocked)
toolbar.menu?.findItem(R.id.queue_lock)?.setVisible(!keepSorted) toolbar.menu?.findItem(R.id.queue_lock)?.setVisible(!keepSorted)
} }
@ -422,7 +424,7 @@ import java.util.*
} }
@UnstableApi private fun toggleQueueLock() { @UnstableApi private fun toggleQueueLock() {
val isLocked: Boolean = UserPreferences.isQueueLocked val isLocked: Boolean = isQueueLocked
if (isLocked) setQueueLocked(false) if (isLocked) setQueueLocked(false)
else { else {
val shouldShowLockWarning: Boolean = prefs!!.getBoolean(PREF_SHOW_LOCK_WARNING, true) val shouldShowLockWarning: Boolean = prefs!!.getBoolean(PREF_SHOW_LOCK_WARNING, true)
@ -448,7 +450,7 @@ import java.util.*
} }
@UnstableApi private fun setQueueLocked(locked: Boolean) { @UnstableApi private fun setQueueLocked(locked: Boolean) {
UserPreferences.isQueueLocked = locked isQueueLocked = locked
refreshToolbarState() refreshToolbarState()
adapter?.updateDragDropEnabled() adapter?.updateDragDropEnabled()
@ -568,12 +570,12 @@ import java.util.*
class QueueSortDialog : EpisodeSortDialog() { class QueueSortDialog : EpisodeSortDialog() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
if (UserPreferences.isQueueKeepSorted) sortOrder = UserPreferences.queueKeepSortedOrder if (isQueueKeepSorted) sortOrder = queueKeepSortedOrder
val view: View = super.onCreateView(inflater, container, savedInstanceState)!! val view: View = super.onCreateView(inflater, container, savedInstanceState)!!
binding.keepSortedCheckbox.visibility = View.VISIBLE binding.keepSortedCheckbox.visibility = View.VISIBLE
binding.keepSortedCheckbox.setChecked(UserPreferences.isQueueKeepSorted) binding.keepSortedCheckbox.setChecked(isQueueKeepSorted)
// Disable until something gets selected // Disable until something gets selected
binding.keepSortedCheckbox.setEnabled(UserPreferences.isQueueKeepSorted) binding.keepSortedCheckbox.setEnabled(isQueueKeepSorted)
return view return view
} }
override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) { override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) {
@ -584,8 +586,8 @@ import java.util.*
super.onSelectionChanged() super.onSelectionChanged()
binding.keepSortedCheckbox.setEnabled(sortOrder != EpisodeSortOrder.RANDOM) binding.keepSortedCheckbox.setEnabled(sortOrder != EpisodeSortOrder.RANDOM)
if (sortOrder == EpisodeSortOrder.RANDOM) binding.keepSortedCheckbox.setChecked(false) if (sortOrder == EpisodeSortOrder.RANDOM) binding.keepSortedCheckbox.setChecked(false)
UserPreferences.isQueueKeepSorted = binding.keepSortedCheckbox.isChecked isQueueKeepSorted = binding.keepSortedCheckbox.isChecked
UserPreferences.queueKeepSortedOrder = sortOrder queueKeepSortedOrder = sortOrder
reorderQueue(sortOrder, true) reorderQueue(sortOrder, true)
} }
/** /**
@ -659,10 +661,10 @@ import java.util.*
private var dragDropEnabled: Boolean private var dragDropEnabled: Boolean
init { init {
dragDropEnabled = !(UserPreferences.isQueueKeepSorted || UserPreferences.isQueueLocked) dragDropEnabled = !(isQueueKeepSorted || isQueueLocked)
} }
fun updateDragDropEnabled() { fun updateDragDropEnabled() {
dragDropEnabled = !(UserPreferences.isQueueKeepSorted || UserPreferences.isQueueLocked) dragDropEnabled = !(isQueueKeepSorted || isQueueLocked)
notifyDataSetChanged() notifyDataSetChanged()
} }
@UnstableApi @UnstableApi
@ -701,7 +703,7 @@ import java.util.*
if (!inActionMode()) { if (!inActionMode()) {
// menu.findItem(R.id.multi_select).setVisible(true) // menu.findItem(R.id.multi_select).setVisible(true)
val keepSorted: Boolean = UserPreferences.isQueueKeepSorted val keepSorted: Boolean = isQueueKeepSorted
if (getItem(0)?.id === longPressedItem?.id || keepSorted) menu.findItem(R.id.move_to_top_item).setVisible(false) if (getItem(0)?.id === longPressedItem?.id || keepSorted) menu.findItem(R.id.move_to_top_item).setVisible(false)
if (getItem(itemCount - 1)?.id === longPressedItem?.id || keepSorted) menu.findItem(R.id.move_to_bottom_item).setVisible(false) if (getItem(itemCount - 1)?.id === longPressedItem?.id || keepSorted) menu.findItem(R.id.move_to_bottom_item).setVisible(false)
} else { } else {

View File

@ -19,12 +19,10 @@ import ac.mdiq.podcini.ui.actions.menuhandler.MenuItemUtils
import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.adapter.EpisodesAdapter import ac.mdiq.podcini.ui.adapter.EpisodesAdapter
import ac.mdiq.podcini.ui.adapter.SelectableAdapter import ac.mdiq.podcini.ui.adapter.SelectableAdapter
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion
import ac.mdiq.podcini.ui.utils.EmptyViewHandler import ac.mdiq.podcini.ui.utils.EmptyViewHandler
import ac.mdiq.podcini.ui.utils.LiftOnScrollListener import ac.mdiq.podcini.ui.utils.LiftOnScrollListener
import ac.mdiq.podcini.ui.view.EpisodesRecyclerView import ac.mdiq.podcini.ui.view.EpisodesRecyclerView
import ac.mdiq.podcini.ui.view.SquareImageView import ac.mdiq.podcini.ui.view.SquareImageView
import ac.mdiq.podcini.ui.view.viewholder.EpisodeViewHolder
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent import ac.mdiq.podcini.util.event.FlowEvent
@ -74,7 +72,7 @@ import java.lang.ref.WeakReference
private lateinit var emptyViewHandler: EmptyViewHandler private lateinit var emptyViewHandler: EmptyViewHandler
private lateinit var recyclerView: EpisodesRecyclerView private lateinit var recyclerView: EpisodesRecyclerView
private lateinit var searchView: SearchView private lateinit var searchView: SearchView
private lateinit var speedDialBinding: MultiSelectSpeedDialBinding private lateinit var sdBinding: MultiSelectSpeedDialBinding
private lateinit var chip: Chip private lateinit var chip: Chip
private lateinit var automaticSearchDebouncer: Handler private lateinit var automaticSearchDebouncer: Handler
@ -95,7 +93,7 @@ import java.lang.ref.WeakReference
Logd(TAG, "fragment onCreateView") Logd(TAG, "fragment onCreateView")
setupToolbar(binding.toolbar) setupToolbar(binding.toolbar)
speedDialBinding = MultiSelectSpeedDialBinding.bind(binding.root) sdBinding = MultiSelectSpeedDialBinding.bind(binding.root)
progressBar = binding.progressBar progressBar = binding.progressBar
recyclerView = binding.recyclerView recyclerView = binding.recyclerView
recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool) recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool)
@ -104,7 +102,6 @@ import java.lang.ref.WeakReference
override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) { override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
super.onCreateContextMenu(menu, v, menuInfo) super.onCreateContextMenu(menu, v, menuInfo)
if (!inActionMode()) menu.findItem(R.id.multi_select).setVisible(true) if (!inActionMode()) menu.findItem(R.id.multi_select).setVisible(true)
MenuItemUtils.setOnClickListeners(menu) { item: MenuItem -> this@SearchFragment.onContextItemSelected(item) } MenuItemUtils.setOnClickListeners(menu) { item: MenuItem -> this@SearchFragment.onContextItemSelected(item) }
} }
} }
@ -151,21 +148,20 @@ import java.lang.ref.WeakReference
} }
} }
}) })
speedDialBinding.fabSD.overlayLayout = speedDialBinding.fabSDOverlay sdBinding.fabSD.overlayLayout = sdBinding.fabSDOverlay
speedDialBinding.fabSD.inflate(R.menu.episodes_apply_action_speeddial) sdBinding.fabSD.inflate(R.menu.episodes_apply_action_speeddial)
speedDialBinding.fabSD.setOnChangeListener(object : SpeedDialView.OnChangeListener { sdBinding.fabSD.setOnChangeListener(object : SpeedDialView.OnChangeListener {
override fun onMainActionSelected(): Boolean { override fun onMainActionSelected(): Boolean {
return false return false
} }
override fun onToggleChanged(open: Boolean) { override fun onToggleChanged(open: Boolean) {
if (open && adapter.selectedCount == 0) { if (open && adapter.selectedCount == 0) {
(activity as MainActivity).showSnackbarAbovePlayer(R.string.no_items_selected, Snackbar.LENGTH_SHORT) (activity as MainActivity).showSnackbarAbovePlayer(R.string.no_items_selected, Snackbar.LENGTH_SHORT)
speedDialBinding.fabSD.close() sdBinding.fabSD.close()
} }
} }
}) })
speedDialBinding.fabSD.setOnActionSelectedListener { actionItem: SpeedDialActionItem -> sdBinding.fabSD.setOnActionSelectedListener { actionItem: SpeedDialActionItem ->
EpisodeMultiSelectHandler(activity as MainActivity, actionItem.id) EpisodeMultiSelectHandler(activity as MainActivity, actionItem.id)
.handleAction(adapter.selectedItems.filterIsInstance<Episode>()) .handleAction(adapter.selectedItems.filterIsInstance<Episode>())
adapter.endSelectMode() adapter.endSelectMode()
@ -210,7 +206,6 @@ import java.lang.ref.WeakReference
searchWithProgressBar() searchWithProgressBar()
return true return true
} }
@UnstableApi override fun onQueryTextChange(s: String): Boolean { @UnstableApi override fun onQueryTextChange(s: String): Boolean {
automaticSearchDebouncer.removeCallbacksAndMessages(null) automaticSearchDebouncer.removeCallbacksAndMessages(null)
if (s.isEmpty() || s.endsWith(" ") || (lastQueryChange != 0L && System.currentTimeMillis() > lastQueryChange + SEARCH_DEBOUNCE_INTERVAL)) { if (s.isEmpty() || s.endsWith(" ") || (lastQueryChange != 0L && System.currentTimeMillis() > lastQueryChange + SEARCH_DEBOUNCE_INTERVAL)) {
@ -227,7 +222,6 @@ import java.lang.ref.WeakReference
override fun onMenuItemActionExpand(item: MenuItem): Boolean { override fun onMenuItemActionExpand(item: MenuItem): Boolean {
return true return true
} }
override fun onMenuItemActionCollapse(item: MenuItem): Boolean { override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
parentFragmentManager.popBackStack() parentFragmentManager.popBackStack()
return true return true
@ -337,7 +331,6 @@ import java.lang.ref.WeakReference
if (requireArguments().getLong(ARG_FEED, 0) == 0L) { if (requireArguments().getLong(ARG_FEED, 0) == 0L) {
if (results.second != null) adapterFeeds.updateData(results.second!!) if (results.second != null) adapterFeeds.updateData(results.second!!)
} else adapterFeeds.updateData(emptyList()) } else adapterFeeds.updateData(emptyList())
if (searchView.query.toString().isEmpty()) emptyViewHandler.setMessage(R.string.type_to_search) if (searchView.query.toString().isEmpty()) emptyViewHandler.setMessage(R.string.type_to_search)
else emptyViewHandler.setMessage(getString(R.string.no_results_for_query) + searchView.query) else emptyViewHandler.setMessage(getString(R.string.no_results_for_query) + searchView.query)
} }
@ -351,9 +344,10 @@ import java.lang.ref.WeakReference
val query = searchView.query.toString() val query = searchView.query.toString()
if (query.isEmpty()) return Pair<List<Episode>, List<Feed>>(emptyList(), emptyList()) if (query.isEmpty()) return Pair<List<Episode>, List<Feed>>(emptyList(), emptyList())
val feed = requireArguments().getLong(ARG_FEED) val feedID = requireArguments().getLong(ARG_FEED)
val items: List<Episode> = searchEpisodes(feed, query) val items: List<Episode> = searchEpisodes(feedID, query)
val feeds: List<Feed> = searchFeeds(query) val feeds: List<Feed> = searchFeeds(query)
Logd(TAG, "performSearch items: ${items.size} feeds: ${feeds.size}")
return Pair<List<Episode>, List<Feed>>(items, feeds) return Pair<List<Episode>, List<Feed>>(items, feeds)
} }
@ -362,23 +356,23 @@ import java.lang.ref.WeakReference
val sb = StringBuilder() val sb = StringBuilder()
for (i in queryWords.indices) { for (i in queryWords.indices) {
sb.append("(") sb.append("(")
.append("feedTitle TEXT ${queryWords[i]}") .append("eigenTitle TEXT '${queryWords[i]}'")
.append(" OR ") .append(" OR ")
.append("customTitle TEXT ${queryWords[i]}") .append("customTitle TEXT '${queryWords[i]}'")
.append(" OR ") .append(" OR ")
.append("author TEXT ${queryWords[i]}") .append("author TEXT '${queryWords[i]}'")
.append(" OR ") .append(" OR ")
.append("description TEXT ${queryWords[i]}") .append("description TEXT '${queryWords[i]}'")
.append(") ") .append(") ")
if (i != queryWords.size - 1) sb.append("AND ") if (i != queryWords.size - 1) sb.append("AND ")
} }
return sb.toString() return sb.toString()
} }
private fun searchFeeds(query: String): List<Feed> { private fun searchFeeds(query: String): List<Feed> {
Logd(TAG, "searchFeeds called") Logd(TAG, "searchFeeds called")
val queryString = prepareFeedQueryString(query) val queryString = prepareFeedQueryString(query)
Logd(TAG, "searchFeeds queryString: $queryString")
return realm.query(Feed::class).query(queryString).find() return realm.query(Feed::class).query(queryString).find()
} }
@ -387,9 +381,9 @@ import java.lang.ref.WeakReference
val sb = StringBuilder() val sb = StringBuilder()
for (i in queryWords.indices) { for (i in queryWords.indices) {
sb.append("(") sb.append("(")
.append("description TEXT ${queryWords[i]}") .append("description TEXT '${queryWords[i]}'")
.append(" OR ") .append(" OR ")
.append("title TEXT ${queryWords[i]}" ) .append("title TEXT '${queryWords[i]}'" )
.append(") ") .append(") ")
if (i != queryWords.size - 1) sb.append("AND ") if (i != queryWords.size - 1) sb.append("AND ")
} }
@ -405,9 +399,10 @@ import java.lang.ref.WeakReference
*/ */
private fun searchEpisodes(feedID: Long, query: String): List<Episode> { private fun searchEpisodes(feedID: Long, query: String): List<Episode> {
Logd(TAG, "searchEpisodes called") Logd(TAG, "searchEpisodes called")
val queryString = prepareEpisodeQueryString(query) var queryString = prepareEpisodeQueryString(query)
val idString = if (feedID != 0L) "(id = $feedID)" else "" if (feedID != 0L) queryString = "(feedId == $feedID) AND $queryString"
return realm.query(Episode::class).query("$idString AND $queryString").find() Logd(TAG, "searchEpisodes queryString: $queryString")
return realm.query(Episode::class).query(queryString).find()
} }
private fun showInputMethod(view: View) { private fun showInputMethod(view: View) {
@ -431,14 +426,14 @@ import java.lang.ref.WeakReference
override fun onStartSelectMode() { override fun onStartSelectMode() {
searchViewFocusOff() searchViewFocusOff()
// speedDialBinding.fabSD.removeActionItemById(R.id.remove_from_inbox_batch) // speedDialBinding.fabSD.removeActionItemById(R.id.remove_from_inbox_batch)
speedDialBinding.fabSD.removeActionItemById(R.id.remove_from_queue_batch) sdBinding.fabSD.removeActionItemById(R.id.remove_from_queue_batch)
speedDialBinding.fabSD.removeActionItemById(R.id.delete_batch) sdBinding.fabSD.removeActionItemById(R.id.delete_batch)
speedDialBinding.fabSD.visibility = View.VISIBLE sdBinding.fabSD.visibility = View.VISIBLE
} }
override fun onEndSelectMode() { override fun onEndSelectMode() {
speedDialBinding.fabSD.close() sdBinding.fabSD.close()
speedDialBinding.fabSD.visibility = View.GONE sdBinding.fabSD.visibility = View.GONE
searchViewFocusOn() searchViewFocusOn()
} }
@ -452,14 +447,13 @@ import java.lang.ref.WeakReference
searchView.requestFocus() searchView.requestFocus()
} }
open class HorizontalFeedListAdapter(mainActivity: MainActivity) : open class HorizontalFeedListAdapter(mainActivity: MainActivity)
RecyclerView.Adapter<HorizontalFeedListAdapter.Holder>(), View.OnCreateContextMenuListener { : RecyclerView.Adapter<HorizontalFeedListAdapter.Holder>(), View.OnCreateContextMenuListener {
private val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity) private val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity)
private val data: MutableList<Feed> = ArrayList() private val data: MutableList<Feed> = ArrayList()
private var dummyViews = 0 private var dummyViews = 0
var longPressedItem: Feed? = null var longPressedItem: Feed? = null
@StringRes @StringRes
private var endButtonText = 0 private var endButtonText = 0
private var endButtonAction: Runnable? = null private var endButtonAction: Runnable? = null
@ -467,18 +461,15 @@ import java.lang.ref.WeakReference
fun setDummyViews(dummyViews: Int) { fun setDummyViews(dummyViews: Int) {
this.dummyViews = dummyViews this.dummyViews = dummyViews
} }
fun updateData(newData: List<Feed>?) { fun updateData(newData: List<Feed>?) {
data.clear() data.clear()
data.addAll(newData!!) data.addAll(newData!!)
notifyDataSetChanged() notifyDataSetChanged()
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
val convertView = View.inflate(mainActivityRef.get(), R.layout.horizontal_feed_item, null) val convertView = View.inflate(mainActivityRef.get(), R.layout.horizontal_feed_item, null)
return Holder(convertView) return Holder(convertView)
} }
@UnstableApi override fun onBindViewHolder(holder: Holder, position: Int) { @UnstableApi override fun onBindViewHolder(holder: Holder, position: Int) {
if (position == itemCount - 1 && endButtonAction != null) { if (position == itemCount - 1 && endButtonAction != null) {
holder.cardView.visibility = View.GONE holder.cardView.visibility = View.GONE
@ -497,49 +488,41 @@ import java.lang.ref.WeakReference
holder.imageView.setImageResource(R.color.medium_gray) holder.imageView.setImageResource(R.color.medium_gray)
return return
} }
holder.itemView.alpha = 1.0f holder.itemView.alpha = 1.0f
val podcast: Feed = data[position] val podcast: Feed = data[position]
holder.imageView.setContentDescription(podcast.title) holder.imageView.setContentDescription(podcast.title)
holder.imageView.setOnClickListener { holder.imageView.setOnClickListener {
mainActivityRef.get()?.loadChildFragment(FeedEpisodesFragment.newInstance(podcast.id)) mainActivityRef.get()?.loadChildFragment(FeedEpisodesFragment.newInstance(podcast.id))
} }
holder.imageView.setOnCreateContextMenuListener(this) holder.imageView.setOnCreateContextMenuListener(this)
holder.imageView.setOnLongClickListener { holder.imageView.setOnLongClickListener {
val currentItemPosition = holder.bindingAdapterPosition val currentItemPosition = holder.bindingAdapterPosition
longPressedItem = data[currentItemPosition] longPressedItem = data[currentItemPosition]
false false
} }
holder.imageView.load(podcast.imageUrl) { holder.imageView.load(podcast.imageUrl) {
placeholder(R.color.light_gray) placeholder(R.color.light_gray)
error(R.mipmap.ic_launcher) error(R.mipmap.ic_launcher)
} }
} }
override fun getItemId(position: Int): Long { override fun getItemId(position: Int): Long {
if (position >= data.size) return RecyclerView.NO_ID // Dummy views if (position >= data.size) return RecyclerView.NO_ID // Dummy views
return data[position].id return data[position].id
} }
override fun getItemCount(): Int { override fun getItemCount(): Int {
return dummyViews + data.size + (if ((endButtonAction == null)) 0 else 1) return dummyViews + data.size + (if ((endButtonAction == null)) 0 else 1)
} }
override fun onCreateContextMenu(contextMenu: ContextMenu, view: View, contextMenuInfo: ContextMenu.ContextMenuInfo?) { override fun onCreateContextMenu(contextMenu: ContextMenu, view: View, contextMenuInfo: ContextMenu.ContextMenuInfo?) {
val inflater: MenuInflater = mainActivityRef.get()!!.menuInflater val inflater: MenuInflater = mainActivityRef.get()!!.menuInflater
if (longPressedItem == null) return if (longPressedItem == null) return
inflater.inflate(R.menu.nav_feed_context, contextMenu) inflater.inflate(R.menu.nav_feed_context, contextMenu)
contextMenu.setHeaderTitle(longPressedItem!!.title) contextMenu.setHeaderTitle(longPressedItem!!.title)
} }
fun setEndButton(@StringRes text: Int, action: Runnable?) { fun setEndButton(@StringRes text: Int, action: Runnable?) {
endButtonAction = action endButtonAction = action
endButtonText = text endButtonText = text
notifyDataSetChanged() notifyDataSetChanged()
} }
class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) { class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val binding = HorizontalFeedItemBinding.bind(itemView) val binding = HorizontalFeedItemBinding.bind(itemView)
var imageView: SquareImageView = binding.discoveryCover var imageView: SquareImageView = binding.discoveryCover
@ -584,9 +567,9 @@ import java.lang.ref.WeakReference
/** /**
* Create a new SearchFragment that searches one specific feed. * Create a new SearchFragment that searches one specific feed.
*/ */
fun newInstance(feed: Long, feedTitle: String?): SearchFragment { fun newInstance(feedId: Long, feedTitle: String?): SearchFragment {
val fragment = newInstance() val fragment = newInstance()
fragment.requireArguments().putLong(ARG_FEED, feed) fragment.requireArguments().putLong(ARG_FEED, feedId)
fragment.requireArguments().putString(ARG_FEED_NAME, feedTitle) fragment.requireArguments().putString(ARG_FEED_NAME, feedTitle)
return fragment return fragment
} }

View File

@ -4,9 +4,11 @@ import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.* import ac.mdiq.podcini.databinding.*
import ac.mdiq.podcini.net.feed.FeedUpdateManager import ac.mdiq.podcini.net.feed.FeedUpdateManager
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.feedOrderBy import ac.mdiq.podcini.preferences.UserPreferences.FEED_ORDER_UNPLAYED
import ac.mdiq.podcini.preferences.UserPreferences.feedOrderDir import ac.mdiq.podcini.preferences.UserPreferences.PREF_DRAWER_FEED_ORDER
import ac.mdiq.podcini.preferences.UserPreferences.useGridLayout 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.getFeedList
import ac.mdiq.podcini.storage.database.Feeds.getTags import ac.mdiq.podcini.storage.database.Feeds.getTags
import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences
@ -88,6 +90,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
private val tags: MutableList<String> = mutableListOf() private val tags: MutableList<String> = mutableListOf()
private var useGrid: Boolean? = null private var useGrid: Boolean? = null
val useGridLayout: Boolean
get() = appPrefs.getBoolean(PREF_FEED_GRID_LAYOUT, false)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -878,6 +882,18 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
private var prevFeedUpdatingEvent: FlowEvent.FeedUpdatingEvent? = null private var prevFeedUpdatingEvent: FlowEvent.FeedUpdatingEvent? = null
val feedOrderBy: Int
get() {
val value = appPrefs.getString(PREF_DRAWER_FEED_ORDER, "" + FEED_ORDER_UNPLAYED)
return value!!.toInt()
}
val feedOrderDir: Int
get() {
val value = appPrefs.getInt(PREF_DRAWER_FEED_ORDER_DIRECTION, 0)
return value
}
fun newInstance(folderTitle: String?): SubscriptionsFragment { fun newInstance(folderTitle: String?): SubscriptionsFragment {
val fragment = SubscriptionsFragment() val fragment = SubscriptionsFragment()
val args = Bundle() val args = Bundle()

View File

@ -1,12 +1,13 @@
package ac.mdiq.podcini.ui.utils package ac.mdiq.podcini.ui.utils
import ac.mdiq.podcini.R 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.appPrefs
import android.content.Context import android.content.Context
import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationChannelGroupCompat import androidx.core.app.NotificationChannelGroupCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import ac.mdiq.podcini.preferences.UserPreferences.gpodnetNotificationsEnabledRaw
import ac.mdiq.podcini.preferences.UserPreferences.showDownloadReportRaw
object NotificationUtils { object NotificationUtils {
const val CHANNEL_ID_USER_ACTION: String = "user_action" const val CHANNEL_ID_USER_ACTION: String = "user_action"
@ -19,6 +20,18 @@ object NotificationUtils {
const val GROUP_ID_ERRORS: String = "group_errors" const val GROUP_ID_ERRORS: String = "group_errors"
const val GROUP_ID_NEWS: String = "group_news" const val GROUP_ID_NEWS: String = "group_news"
/**
* Used for migration of the preference to system notification channels.
*/
val showDownloadReportRaw: Boolean
get() = appPrefs.getBoolean(PREF_SHOW_DOWNLOAD_REPORT, true)
/**
* Used for migration of the preference to system notification channels.
*/
val gpodnetNotificationsEnabledRaw: Boolean
get() = appPrefs.getBoolean(PREF_GPODNET_NOTIFICATIONS, true)
fun createChannels(context: Context) { fun createChannels(context: Context) {
val mNotificationManager = NotificationManagerCompat.from(context) val mNotificationManager = NotificationManagerCompat.from(context)

View File

@ -1,7 +1,7 @@
package ac.mdiq.podcini.util.error package ac.mdiq.podcini.util.error
import ac.mdiq.podcini.BuildConfig import ac.mdiq.podcini.BuildConfig
import ac.mdiq.podcini.preferences.UserPreferences.getDataFolder import ac.mdiq.podcini.storage.utils.FilesUtils.getDataFolder
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils

View File

@ -578,7 +578,7 @@
<string name="database_import_label">استيراد قاعدة البيانات</string> <string name="database_import_label">استيراد قاعدة البيانات</string>
<string name="database_import_warning">استيراد قاعدة بيانات سيمسح كل اشتراكاتك الحالية وسجل الاستماع. الأفضل أن تصدر قاعدة البيانات الحالية لأرشيف. هل تريد تبديل البيانات؟</string> <string name="database_import_warning">استيراد قاعدة بيانات سيمسح كل اشتراكاتك الحالية وسجل الاستماع. الأفضل أن تصدر قاعدة البيانات الحالية لأرشيف. هل تريد تبديل البيانات؟</string>
<string name="please_wait">يرجى الانتظار…</string> <string name="please_wait">يرجى الانتظار…</string>
<string name="export_error_label">حدث خطأ أثناء التصدير</string> <string name="import_export_error_label">حدث خطأ أثناء التصدير</string>
<string name="export_success_title">تم التصدير بنجاح</string> <string name="export_success_title">تم التصدير بنجاح</string>
<string name="opml_import_ask_read_permission">الوصول الى مساحة التخزين الخارجية مطلوب لقراءة ملف الـ OPML</string> <string name="opml_import_ask_read_permission">الوصول الى مساحة التخزين الخارجية مطلوب لقراءة ملف الـ OPML</string>
<string name="successful_import_label">استيراد ناجح</string> <string name="successful_import_label">استيراد ناجح</string>

View File

@ -459,7 +459,7 @@
<string name="database_import_label">Enporzhiañ ar stlennvon</string> <string name="database_import_label">Enporzhiañ ar stlennvon</string>
<string name="database_import_warning">Enporzhiañ ur stlennvon a amsavo ho holl goumanantoù ha roll istor lenn. Erbediñ a reomp ezporzhiañ ho stlennvon en a-raok. Fellout a ra deoc\'h enporzhiañ?</string> <string name="database_import_warning">Enporzhiañ ur stlennvon a amsavo ho holl goumanantoù ha roll istor lenn. Erbediñ a reomp ezporzhiañ ho stlennvon en a-raok. Fellout a ra deoc\'h enporzhiañ?</string>
<string name="please_wait">Gortozit un tamm…</string> <string name="please_wait">Gortozit un tamm…</string>
<string name="export_error_label">Fazi ezporzhiañ</string> <string name="import_export_error_label">Fazi ezporzhiañ</string>
<string name="export_success_title">Ezporzhiet gant berzh</string> <string name="export_success_title">Ezporzhiet gant berzh</string>
<string name="opml_import_ask_read_permission">Ret eo aotren haeziñ ar c\'hadaviñ diavaez evit lenn ar restr OPML</string> <string name="opml_import_ask_read_permission">Ret eo aotren haeziñ ar c\'hadaviñ diavaez evit lenn ar restr OPML</string>
<string name="successful_import_label">Enporzhiet gant berzh</string> <string name="successful_import_label">Enporzhiet gant berzh</string>

View File

@ -521,7 +521,7 @@
<string name="database_import_label">Importa base de dades</string> <string name="database_import_label">Importa base de dades</string>
<string name="database_import_warning">Importar una base de dades reemplaçarà totes les teues subscripcions i històric de reproducció. Deuries exportar la teua base de dades com a còpia de seguretat. Vols reemplaçar\?</string> <string name="database_import_warning">Importar una base de dades reemplaçarà totes les teues subscripcions i històric de reproducció. Deuries exportar la teua base de dades com a còpia de seguretat. Vols reemplaçar\?</string>
<string name="please_wait">Per favor, espera…</string> <string name="please_wait">Per favor, espera…</string>
<string name="export_error_label">Error d\'exportació</string> <string name="import_export_error_label">Error d\'exportació</string>
<string name="export_success_title">Exportació correcta</string> <string name="export_success_title">Exportació correcta</string>
<string name="opml_import_ask_read_permission">Per llegir arxius OPML és necessari accés a la memòria externa</string> <string name="opml_import_ask_read_permission">Per llegir arxius OPML és necessari accés a la memòria externa</string>
<string name="successful_import_label">Importació correcta</string> <string name="successful_import_label">Importació correcta</string>

View File

@ -573,7 +573,7 @@
<string name="database_import_label">Import databáze</string> <string name="database_import_label">Import databáze</string>
<string name="database_import_warning">Importem databáze nahradíte všechny své odběry a historii poslechu. Doporučujeme nejdříve exportovat současnou databázi pro případnou obnovu. Opravdu chcete databázi nahradit\?</string> <string name="database_import_warning">Importem databáze nahradíte všechny své odběry a historii poslechu. Doporučujeme nejdříve exportovat současnou databázi pro případnou obnovu. Opravdu chcete databázi nahradit\?</string>
<string name="please_wait">Čekejte prosím…</string> <string name="please_wait">Čekejte prosím…</string>
<string name="export_error_label">Chyba exportu</string> <string name="import_export_error_label">Chyba exportu</string>
<string name="export_success_title">Export úspěšný</string> <string name="export_success_title">Export úspěšný</string>
<string name="opml_import_ask_read_permission">Pro přečtení OPML souboru je vyžadován přístup k externímu úložišti</string> <string name="opml_import_ask_read_permission">Pro přečtení OPML souboru je vyžadován přístup k externímu úložišti</string>
<string name="successful_import_label">Import úspěšný</string> <string name="successful_import_label">Import úspěšný</string>

View File

@ -545,7 +545,7 @@
<string name="database_import_label">Importér database</string> <string name="database_import_label">Importér database</string>
<string name="database_import_warning">Import af database vil overskrive alle dine nuværende abonnementer og din afspilningshistorik. Du bør eksportere din nuværende database som en sikkerhedskopi først. Vil du overskrive\?</string> <string name="database_import_warning">Import af database vil overskrive alle dine nuværende abonnementer og din afspilningshistorik. Du bør eksportere din nuværende database som en sikkerhedskopi først. Vil du overskrive\?</string>
<string name="please_wait">Vent…</string> <string name="please_wait">Vent…</string>
<string name="export_error_label">Eksportfejl</string> <string name="import_export_error_label">Eksportfejl</string>
<string name="export_success_title">Eksport lykkedes</string> <string name="export_success_title">Eksport lykkedes</string>
<string name="opml_import_ask_read_permission">Adgang til eksternt lager er påkrævet for at læse OPML-filen</string> <string name="opml_import_ask_read_permission">Adgang til eksternt lager er påkrævet for at læse OPML-filen</string>
<string name="successful_import_label">Importeret</string> <string name="successful_import_label">Importeret</string>

View File

@ -549,7 +549,7 @@
<string name="database_import_label">Datenbank importieren</string> <string name="database_import_label">Datenbank importieren</string>
<string name="database_import_warning">Das Importieren einer Datenbank ersetzt alle Abonnements und abgespielte Episoden. Ein Export als Backup wird empfohlen. Möchtest du die Datenbank wirklich ersetzen\?</string> <string name="database_import_warning">Das Importieren einer Datenbank ersetzt alle Abonnements und abgespielte Episoden. Ein Export als Backup wird empfohlen. Möchtest du die Datenbank wirklich ersetzen\?</string>
<string name="please_wait">Bitte warten…</string> <string name="please_wait">Bitte warten…</string>
<string name="export_error_label">Exportfehler</string> <string name="import_export_error_label">Exportfehler</string>
<string name="export_success_title">Export erfolgreich</string> <string name="export_success_title">Export erfolgreich</string>
<string name="opml_import_ask_read_permission">Zugriff auf externen Speicher wird benötigt, um die OPML-Datei zu lesen</string> <string name="opml_import_ask_read_permission">Zugriff auf externen Speicher wird benötigt, um die OPML-Datei zu lesen</string>
<string name="successful_import_label">Import erfolgreich</string> <string name="successful_import_label">Import erfolgreich</string>

View File

@ -562,7 +562,7 @@
<string name="database_import_label">Importar base de datos</string> <string name="database_import_label">Importar base de datos</string>
<string name="database_import_warning">La importación de una base de datos reemplazará todas las suscripciones actuales y el historial de reproducción. Debes exportar la base de datos actual como una copia de seguridad. ¿Quieres reemplazarla\?</string> <string name="database_import_warning">La importación de una base de datos reemplazará todas las suscripciones actuales y el historial de reproducción. Debes exportar la base de datos actual como una copia de seguridad. ¿Quieres reemplazarla\?</string>
<string name="please_wait">Por favor espera…</string> <string name="please_wait">Por favor espera…</string>
<string name="export_error_label">Error en la exportación</string> <string name="import_export_error_label">Error en la exportación</string>
<string name="export_success_title">Exportación exitosa</string> <string name="export_success_title">Exportación exitosa</string>
<string name="opml_import_ask_read_permission">Necesita acceso al almacenamiento externo para leer archivos OPML</string> <string name="opml_import_ask_read_permission">Necesita acceso al almacenamiento externo para leer archivos OPML</string>
<string name="successful_import_label">Importación exitosa</string> <string name="successful_import_label">Importación exitosa</string>

View File

@ -403,7 +403,7 @@
<string name="database_import_label">Andmebaasi import</string> <string name="database_import_label">Andmebaasi import</string>
<string name="database_import_warning">Andmebaasi importimine asendab kõik sinu praegused tellimused ja kuulamiste ajaloo. Peaksid oma praeguse andmebaasi varundamise eesmärgil enne eksportima. Kas soovid andmed asendada\?</string> <string name="database_import_warning">Andmebaasi importimine asendab kõik sinu praegused tellimused ja kuulamiste ajaloo. Peaksid oma praeguse andmebaasi varundamise eesmärgil enne eksportima. Kas soovid andmed asendada\?</string>
<string name="please_wait">Palun oota…</string> <string name="please_wait">Palun oota…</string>
<string name="export_error_label">Viga eksportimisel</string> <string name="import_export_error_label">Viga eksportimisel</string>
<string name="export_success_title">Eksportimine edukas</string> <string name="export_success_title">Eksportimine edukas</string>
<string name="opml_import_ask_read_permission">OPML faili lugemiseks on vajalik ligipääs välisele salvestusruumile</string> <string name="opml_import_ask_read_permission">OPML faili lugemiseks on vajalik ligipääs välisele salvestusruumile</string>
<string name="successful_import_label">Importimine oli edukas</string> <string name="successful_import_label">Importimine oli edukas</string>

View File

@ -488,7 +488,7 @@
<string name="database_import_label">Ekarri datu basea</string> <string name="database_import_label">Ekarri datu basea</string>
<string name="database_import_warning">Datu-base bat inportatzeak egungo harpidetza guztiak eta erreprodukzio-historia ordezkatuko ditu. Egungo datu-basea esportatu behar duzu segurtasun-kopia gisa. Ordezkatu nahi duzu?</string> <string name="database_import_warning">Datu-base bat inportatzeak egungo harpidetza guztiak eta erreprodukzio-historia ordezkatuko ditu. Egungo datu-basea esportatu behar duzu segurtasun-kopia gisa. Ordezkatu nahi duzu?</string>
<string name="please_wait">Itxaron mesedez…</string> <string name="please_wait">Itxaron mesedez…</string>
<string name="export_error_label">Errorea esportatzean</string> <string name="import_export_error_label">Errorea esportatzean</string>
<string name="export_success_title">Esportatzea ongi burutu da</string> <string name="export_success_title">Esportatzea ongi burutu da</string>
<string name="opml_import_ask_read_permission">Kanpo biltegiratzerako sarbidea behar duzu OPML fitxategia irakurtzeko</string> <string name="opml_import_ask_read_permission">Kanpo biltegiratzerako sarbidea behar duzu OPML fitxategia irakurtzeko</string>
<string name="successful_import_label">Inportate arrakastatsua</string> <string name="successful_import_label">Inportate arrakastatsua</string>

View File

@ -524,7 +524,7 @@
<string name="database_import_label">وارد کردن پایگاه داده</string> <string name="database_import_label">وارد کردن پایگاه داده</string>
<string name="database_import_warning">وارد کردن یک پایگاه داده باعث جایگزین تمام اشتراکهای فعلی و سابقه پخش شما خواهد شد. شما باید پایگاه داده فعلی خود را به عنوان پشتیبان خارج کنید. آیا شما میخواهید جایگزین کنید؟</string> <string name="database_import_warning">وارد کردن یک پایگاه داده باعث جایگزین تمام اشتراکهای فعلی و سابقه پخش شما خواهد شد. شما باید پایگاه داده فعلی خود را به عنوان پشتیبان خارج کنید. آیا شما میخواهید جایگزین کنید؟</string>
<string name="please_wait">صبر کنید لطفا …</string> <string name="please_wait">صبر کنید لطفا …</string>
<string name="export_error_label">خطا در خارج کردن</string> <string name="import_export_error_label">خطا در خارج کردن</string>
<string name="export_success_title">خارج کردن موفقیت آمیز</string> <string name="export_success_title">خارج کردن موفقیت آمیز</string>
<string name="opml_import_ask_read_permission">برای خواندن پروندهٔ OPML نباز به دسترسی حافظهٔ خارجی است</string> <string name="opml_import_ask_read_permission">برای خواندن پروندهٔ OPML نباز به دسترسی حافظهٔ خارجی است</string>
<string name="successful_import_label">وارد کردن موفقیت آمیز</string> <string name="successful_import_label">وارد کردن موفقیت آمیز</string>

View File

@ -506,7 +506,7 @@
<string name="database_import_label">Tietokannan tuonti</string> <string name="database_import_label">Tietokannan tuonti</string>
<string name="database_import_warning">Tietokannan tuonti korvaa kaikki nykyiset tilauksesi ja toistohistoriasi. Vie nykyinen tietokanta halutessasi varmuuskopioksi ennen jatkamista. Haluatko korvata\?</string> <string name="database_import_warning">Tietokannan tuonti korvaa kaikki nykyiset tilauksesi ja toistohistoriasi. Vie nykyinen tietokanta halutessasi varmuuskopioksi ennen jatkamista. Haluatko korvata\?</string>
<string name="please_wait">Ole hyvä ja odota…</string> <string name="please_wait">Ole hyvä ja odota…</string>
<string name="export_error_label">Vientivirhe</string> <string name="import_export_error_label">Vientivirhe</string>
<string name="export_success_title">Vienti onnistunut</string> <string name="export_success_title">Vienti onnistunut</string>
<string name="opml_import_ask_read_permission">Pääsy ulkoiseen solvellukseen tarvitaan OPML tiedoston lukemiseen</string> <string name="opml_import_ask_read_permission">Pääsy ulkoiseen solvellukseen tarvitaan OPML tiedoston lukemiseen</string>
<string name="successful_import_label">Tuonti onnistui</string> <string name="successful_import_label">Tuonti onnistui</string>

View File

@ -562,7 +562,7 @@
<string name="database_import_label">Importer la base de données</string> <string name="database_import_label">Importer la base de données</string>
<string name="database_import_warning">Importer une base de données remplacera tout vos abonnements et votre historique de lecture. Il est conseillé d\'exporter votre base de données actuelle pour avoir une sauvegarde. Confirmez-vous l\'import \?</string> <string name="database_import_warning">Importer une base de données remplacera tout vos abonnements et votre historique de lecture. Il est conseillé d\'exporter votre base de données actuelle pour avoir une sauvegarde. Confirmez-vous l\'import \?</string>
<string name="please_wait">Merci de patienter…</string> <string name="please_wait">Merci de patienter…</string>
<string name="export_error_label">Erreur d\'exportation</string> <string name="import_export_error_label">Erreur d\'exportation</string>
<string name="export_success_title">Export réussi</string> <string name="export_success_title">Export réussi</string>
<string name="opml_import_ask_read_permission">L\'accès au stockage externe est requis pour lire le fichier OPML</string> <string name="opml_import_ask_read_permission">L\'accès au stockage externe est requis pour lire le fichier OPML</string>
<string name="successful_import_label">Import réussi</string> <string name="successful_import_label">Import réussi</string>

View File

@ -545,7 +545,7 @@
<string name="database_import_label">Importar base de datos</string> <string name="database_import_label">Importar base de datos</string>
<string name="database_import_warning">Ao importar a base de datos substituirás todas as subscricións actuais e historial de reprodución. Deberías exportar a base de datos actual como copia de apoio. Desexas substituíla?</string> <string name="database_import_warning">Ao importar a base de datos substituirás todas as subscricións actuais e historial de reprodución. Deberías exportar a base de datos actual como copia de apoio. Desexas substituíla?</string>
<string name="please_wait">Agarda…</string> <string name="please_wait">Agarda…</string>
<string name="export_error_label">Fallo ao exportar</string> <string name="import_export_error_label">Fallo ao exportar</string>
<string name="export_success_title">Exportado con éxito</string> <string name="export_success_title">Exportado con éxito</string>
<string name="opml_import_ask_read_permission">Precísase acceso ao almacenamento externo para ler o ficheiro OPML</string> <string name="opml_import_ask_read_permission">Precísase acceso ao almacenamento externo para ler o ficheiro OPML</string>
<string name="successful_import_label">Importación correcta</string> <string name="successful_import_label">Importación correcta</string>

View File

@ -420,7 +420,7 @@
<string name="database_import_label">Adatbázis importálása</string> <string name="database_import_label">Adatbázis importálása</string>
<string name="database_import_warning">Az adatbázis importálása lecseréli a jelenlegi feliratkozásait és lejátszási előzményeit. Célszerű biztonsági mentésként exportálni a jelenlegi adatbázist. Biztos, hogy lecseréli\?</string> <string name="database_import_warning">Az adatbázis importálása lecseréli a jelenlegi feliratkozásait és lejátszási előzményeit. Célszerű biztonsági mentésként exportálni a jelenlegi adatbázist. Biztos, hogy lecseréli\?</string>
<string name="please_wait">Várjon…</string> <string name="please_wait">Várjon…</string>
<string name="export_error_label">Exportálási hiba</string> <string name="import_export_error_label">Exportálási hiba</string>
<string name="export_success_title">Exportálás sikeres</string> <string name="export_success_title">Exportálás sikeres</string>
<string name="opml_import_ask_read_permission">A külső tároló elérése szükséges az OPML fájl olvasásához</string> <string name="opml_import_ask_read_permission">A külső tároló elérése szükséges az OPML fájl olvasásához</string>
<string name="successful_import_label">Importálás sikeres</string> <string name="successful_import_label">Importálás sikeres</string>

View File

@ -429,7 +429,7 @@
<string name="database_import_label">Import Database</string> <string name="database_import_label">Import Database</string>
<string name="database_import_warning">Mengimpor database akan menggantikan seluruh langganan dan riwayat pemutaran Anda. Anda sebaiknya mengekspor database yang sekarang Anda miliki sebagai cadangan. Tetap lanjutkan?</string> <string name="database_import_warning">Mengimpor database akan menggantikan seluruh langganan dan riwayat pemutaran Anda. Anda sebaiknya mengekspor database yang sekarang Anda miliki sebagai cadangan. Tetap lanjutkan?</string>
<string name="please_wait">Mohon tunggu…</string> <string name="please_wait">Mohon tunggu…</string>
<string name="export_error_label">Kesalahan ekspor</string> <string name="import_export_error_label">Kesalahan ekspor</string>
<string name="export_success_title">Ekspor berhasil</string> <string name="export_success_title">Ekspor berhasil</string>
<string name="opml_import_ask_read_permission">Akses ke penyimpanan eksternal diperlukan untuk membaca berkas OPML</string> <string name="opml_import_ask_read_permission">Akses ke penyimpanan eksternal diperlukan untuk membaca berkas OPML</string>
<string name="successful_import_label">Impor sukses!</string> <string name="successful_import_label">Impor sukses!</string>

View File

@ -563,7 +563,7 @@
<string name="database_import_label">Importa database</string> <string name="database_import_label">Importa database</string>
<string name="database_import_warning">L\'importazione di un database sostituirà tutte le iscrizioni attuali e la cronologia di riproduzione. Dovresti eseguire un backup del tuo database attuale. Vuoi proseguire\?</string> <string name="database_import_warning">L\'importazione di un database sostituirà tutte le iscrizioni attuali e la cronologia di riproduzione. Dovresti eseguire un backup del tuo database attuale. Vuoi proseguire\?</string>
<string name="please_wait">Attendi…</string> <string name="please_wait">Attendi…</string>
<string name="export_error_label">Errore di esportazione</string> <string name="import_export_error_label">Errore di esportazione</string>
<string name="export_success_title">Esportazione eseguita</string> <string name="export_success_title">Esportazione eseguita</string>
<string name="opml_import_ask_read_permission">Per leggere il file OPML è necessario l\'accesso alla memoria esterna</string> <string name="opml_import_ask_read_permission">Per leggere il file OPML è necessario l\'accesso alla memoria esterna</string>
<string name="successful_import_label">Importazione eseguita</string> <string name="successful_import_label">Importazione eseguita</string>

View File

@ -573,7 +573,7 @@
<string name="database_import_label">ייבוא מסד נתונים</string> <string name="database_import_label">ייבוא מסד נתונים</string>
<string name="database_import_warning">ייבוא מסד נתונים יחליף את כל המינויים הנוכחיים שלך לרבות היסטוריית הנגינה. עליך תחילה לייצא את מסד הנתונים הנוכחי שלך כגיבוי. להמשיך בהחלפה?</string> <string name="database_import_warning">ייבוא מסד נתונים יחליף את כל המינויים הנוכחיים שלך לרבות היסטוריית הנגינה. עליך תחילה לייצא את מסד הנתונים הנוכחי שלך כגיבוי. להמשיך בהחלפה?</string>
<string name="please_wait">נא להמתין…</string> <string name="please_wait">נא להמתין…</string>
<string name="export_error_label">שגיאת ייצוא</string> <string name="import_export_error_label">שגיאת ייצוא</string>
<string name="export_success_title">הייצוא הצליח</string> <string name="export_success_title">הייצוא הצליח</string>
<string name="opml_import_ask_read_permission">נדרשת גישה לאחסון חיצוני כדי לקרוא את קובץ ה־OPML</string> <string name="opml_import_ask_read_permission">נדרשת גישה לאחסון חיצוני כדי לקרוא את קובץ ה־OPML</string>
<string name="successful_import_label">הייבוא הצליח</string> <string name="successful_import_label">הייבוא הצליח</string>

View File

@ -383,7 +383,7 @@
<string name="database_import_label">データベースのインポート</string> <string name="database_import_label">データベースのインポート</string>
<string name="database_import_warning">データベースをインポートすると、現在の購読と再生履歴がすべて置き換えられます。現在のデータベースをバックアップとしてエクスポートする必要があります。それでも置き換えしますか?</string> <string name="database_import_warning">データベースをインポートすると、現在の購読と再生履歴がすべて置き換えられます。現在のデータベースをバックアップとしてエクスポートする必要があります。それでも置き換えしますか?</string>
<string name="please_wait">しばらくお待ちください…</string> <string name="please_wait">しばらくお待ちください…</string>
<string name="export_error_label">エクスポートエラー</string> <string name="import_export_error_label">エクスポートエラー</string>
<string name="export_success_title">エクスポートしました</string> <string name="export_success_title">エクスポートしました</string>
<string name="opml_import_ask_read_permission">OPML ファイルを読み込むために、外部ストレージへのアクセスが必要です</string> <string name="opml_import_ask_read_permission">OPML ファイルを読み込むために、外部ストレージへのアクセスが必要です</string>
<string name="successful_import_label">インポートしました</string> <string name="successful_import_label">インポートしました</string>

View File

@ -503,7 +503,7 @@
<string name="database_import_label">데이터베이스 가져오기</string> <string name="database_import_label">데이터베이스 가져오기</string>
<string name="database_import_warning">데이터베이스 가져오기를 하면 모든 구독과 재생 히스토리가 가져온 내용으로 바뀝니다. 백업을 하려면 현재 데이터베이스를 내보내기 해야 합니다. 정말로 데이터베이스를 바꾸시겠습니까\?</string> <string name="database_import_warning">데이터베이스 가져오기를 하면 모든 구독과 재생 히스토리가 가져온 내용으로 바뀝니다. 백업을 하려면 현재 데이터베이스를 내보내기 해야 합니다. 정말로 데이터베이스를 바꾸시겠습니까\?</string>
<string name="please_wait">기다리십시오…</string> <string name="please_wait">기다리십시오…</string>
<string name="export_error_label">내보내기 오류</string> <string name="import_export_error_label">내보내기 오류</string>
<string name="export_success_title">내보내기 성공</string> <string name="export_success_title">내보내기 성공</string>
<string name="opml_import_ask_read_permission">OPML 파일을 읽으려면 외부 저장소 접근이 필요합니다</string> <string name="opml_import_ask_read_permission">OPML 파일을 읽으려면 외부 저장소 접근이 필요합니다</string>
<string name="successful_import_label">가져오기 성공</string> <string name="successful_import_label">가져오기 성공</string>

View File

@ -395,7 +395,7 @@
<string name="database_import_label">Duomenų bazės importas</string> <string name="database_import_label">Duomenų bazės importas</string>
<string name="database_import_warning">Importuojant duomenų bazę bus pakeistos visos dabartinės jūsų prenumeratos ir atkūrimo istorija. Rekomenduojame eksportuoti dabartinę duomenų bazę kaip atsarginę kopiją. Ar norite pakeisti\?</string> <string name="database_import_warning">Importuojant duomenų bazę bus pakeistos visos dabartinės jūsų prenumeratos ir atkūrimo istorija. Rekomenduojame eksportuoti dabartinę duomenų bazę kaip atsarginę kopiją. Ar norite pakeisti\?</string>
<string name="please_wait">Prašome luktelėti…</string> <string name="please_wait">Prašome luktelėti…</string>
<string name="export_error_label">Eksporto klaida</string> <string name="import_export_error_label">Eksporto klaida</string>
<string name="export_success_title">Sėkmingai eksportuota</string> <string name="export_success_title">Sėkmingai eksportuota</string>
<string name="opml_import_ask_read_permission">Norint nuskaityti OPML failą reikalinga prieiga prie išorinės laikmenos</string> <string name="opml_import_ask_read_permission">Norint nuskaityti OPML failą reikalinga prieiga prie išorinės laikmenos</string>
<string name="successful_import_label">Sėkmingai importuota</string> <string name="successful_import_label">Sėkmingai importuota</string>

View File

@ -512,7 +512,7 @@
<string name="database_import_label">Database-importering</string> <string name="database_import_label">Database-importering</string>
<string name="database_import_warning">Å importere en database vil erstatte all dine abonnementer og avspillingshistorie. Det er anbefalt å eksportere din eksisterende database som backup. Vil du importere nå\?</string> <string name="database_import_warning">Å importere en database vil erstatte all dine abonnementer og avspillingshistorie. Det er anbefalt å eksportere din eksisterende database som backup. Vil du importere nå\?</string>
<string name="please_wait">Vennligst vent…</string> <string name="please_wait">Vennligst vent…</string>
<string name="export_error_label">Feil ved eksportering</string> <string name="import_export_error_label">Feil ved eksportering</string>
<string name="export_success_title">Eksportering vellykket!</string> <string name="export_success_title">Eksportering vellykket!</string>
<string name="opml_import_ask_read_permission">Tilgang til ekstern lagring er nødvendig for å lese OPML filen</string> <string name="opml_import_ask_read_permission">Tilgang til ekstern lagring er nødvendig for å lese OPML filen</string>
<string name="successful_import_label">Eksportering vellykket</string> <string name="successful_import_label">Eksportering vellykket</string>

View File

@ -480,7 +480,7 @@
<string name="database_import_label">Database importeren</string> <string name="database_import_label">Database importeren</string>
<string name="database_import_warning">Bij het importeren van een database worden alle huidige abonnementen en de afspeelgeschiedenis vervangen. Exporteer ter back-up de huidige database. Doorgaan\?</string> <string name="database_import_warning">Bij het importeren van een database worden alle huidige abonnementen en de afspeelgeschiedenis vervangen. Exporteer ter back-up de huidige database. Doorgaan\?</string>
<string name="please_wait">Even geduld…</string> <string name="please_wait">Even geduld…</string>
<string name="export_error_label">Exportfout</string> <string name="import_export_error_label">Exportfout</string>
<string name="export_success_title">Geëxporteerd</string> <string name="export_success_title">Geëxporteerd</string>
<string name="opml_import_ask_read_permission">Toegang tot externe locaties is nodig om het OPML-bestand te kunnen lezen</string> <string name="opml_import_ask_read_permission">Toegang tot externe locaties is nodig om het OPML-bestand te kunnen lezen</string>
<string name="successful_import_label">Importeren voltooid</string> <string name="successful_import_label">Importeren voltooid</string>

View File

@ -526,7 +526,7 @@
<string name="database_import_label">Import bazy danych</string> <string name="database_import_label">Import bazy danych</string>
<string name="database_import_warning">Import bazy danych nadpisze wszystkie twoje aktualne subskrypcje i historię odtworzeń. Zalecany jest eksport aktualnej bazy danych jako kopia zapasowa. Czy chcesz zamienić\?</string> <string name="database_import_warning">Import bazy danych nadpisze wszystkie twoje aktualne subskrypcje i historię odtworzeń. Zalecany jest eksport aktualnej bazy danych jako kopia zapasowa. Czy chcesz zamienić\?</string>
<string name="please_wait">Proszę czekać…</string> <string name="please_wait">Proszę czekać…</string>
<string name="export_error_label">Błąd eksportu</string> <string name="import_export_error_label">Błąd eksportu</string>
<string name="export_success_title">Export zakończony powodzeniem</string> <string name="export_success_title">Export zakończony powodzeniem</string>
<string name="opml_import_ask_read_permission">Dostęp do zewnętrznej pamięci jest potrzebny do odczytywania plików OPML</string> <string name="opml_import_ask_read_permission">Dostęp do zewnętrznej pamięci jest potrzebny do odczytywania plików OPML</string>
<string name="successful_import_label">Import udany</string> <string name="successful_import_label">Import udany</string>

View File

@ -519,7 +519,7 @@
<string name="database_import_label">Importação do banco de dados</string> <string name="database_import_label">Importação do banco de dados</string>
<string name="database_import_warning">A importação de um banco de dados substituirá todas as suas assinaturas atuais e histórico de reprodução. Você deve exportar seu banco de dados atual como um backup. Você quer substituir\?</string> <string name="database_import_warning">A importação de um banco de dados substituirá todas as suas assinaturas atuais e histórico de reprodução. Você deve exportar seu banco de dados atual como um backup. Você quer substituir\?</string>
<string name="please_wait">Por favor aguarde…</string> <string name="please_wait">Por favor aguarde…</string>
<string name="export_error_label">Erro na exportação</string> <string name="import_export_error_label">Erro na exportação</string>
<string name="export_success_title">Exportado com sucesso</string> <string name="export_success_title">Exportado com sucesso</string>
<string name="opml_import_ask_read_permission">É necessário acesso ao armazenamento externo para ler o arquivo OPML</string> <string name="opml_import_ask_read_permission">É necessário acesso ao armazenamento externo para ler o arquivo OPML</string>
<string name="successful_import_label">Importação bem sucedida</string> <string name="successful_import_label">Importação bem sucedida</string>

View File

@ -562,7 +562,7 @@
<string name="database_import_label">Importar base de dados</string> <string name="database_import_label">Importar base de dados</string>
<string name="database_import_warning">A importação de uma base de dados substitui as subscrições atuais e o histórico de reprodução. Deve efetuar um backup da base de dados atual. Substituir\?</string> <string name="database_import_warning">A importação de uma base de dados substitui as subscrições atuais e o histórico de reprodução. Deve efetuar um backup da base de dados atual. Substituir\?</string>
<string name="please_wait">Aguarde…</string> <string name="please_wait">Aguarde…</string>
<string name="export_error_label">Erro de exportação</string> <string name="import_export_error_label">Erro de exportação</string>
<string name="export_success_title">Exportação efetuada</string> <string name="export_success_title">Exportação efetuada</string>
<string name="opml_import_ask_read_permission">Requer acesso ao armazenamento externo para ler o ficheiro OPML</string> <string name="opml_import_ask_read_permission">Requer acesso ao armazenamento externo para ler o ficheiro OPML</string>
<string name="successful_import_label">Importação bem sucedida</string> <string name="successful_import_label">Importação bem sucedida</string>

View File

@ -559,7 +559,7 @@
<string name="database_import_label">import bază de date</string> <string name="database_import_label">import bază de date</string>
<string name="database_import_warning">Importarea unei baze de date va înlocui abonamentele tale și istoricul fișierelor redate. Ar trebui să exporți baza de date curentă ca un fișier de rezervă. Doreșți să înlocuieșți\?</string> <string name="database_import_warning">Importarea unei baze de date va înlocui abonamentele tale și istoricul fișierelor redate. Ar trebui să exporți baza de date curentă ca un fișier de rezervă. Doreșți să înlocuieșți\?</string>
<string name="please_wait">Vă rugăm așteptați…</string> <string name="please_wait">Vă rugăm așteptați…</string>
<string name="export_error_label">Eroare exportare</string> <string name="import_export_error_label">Eroare exportare</string>
<string name="export_success_title">Exportarea a reușit</string> <string name="export_success_title">Exportarea a reușit</string>
<string name="opml_import_ask_read_permission">Accesarea memoriei externe este necesară pentru a citi fișierul OPML</string> <string name="opml_import_ask_read_permission">Accesarea memoriei externe este necesară pentru a citi fișierul OPML</string>
<string name="successful_import_label">Importul a reușit</string> <string name="successful_import_label">Importul a reușit</string>

View File

@ -545,7 +545,7 @@
<string name="database_import_label">Импорт базы данных</string> <string name="database_import_label">Импорт базы данных</string>
<string name="database_import_warning">Импорт базы данных приведет к замене всех текущих подписок и истории воспроизведения. Следует экспортировать текущую базу данных, чтобы иметь резервную копию. Хотите заменить\?</string> <string name="database_import_warning">Импорт базы данных приведет к замене всех текущих подписок и истории воспроизведения. Следует экспортировать текущую базу данных, чтобы иметь резервную копию. Хотите заменить\?</string>
<string name="please_wait">Подождите…</string> <string name="please_wait">Подождите…</string>
<string name="export_error_label">Ошибка экспорта</string> <string name="import_export_error_label">Ошибка экспорта</string>
<string name="export_success_title">Экспорт выполнен</string> <string name="export_success_title">Экспорт выполнен</string>
<string name="opml_import_ask_read_permission">Для чтения файла OPML необходим доступ к внешнему хранилищу</string> <string name="opml_import_ask_read_permission">Для чтения файла OPML необходим доступ к внешнему хранилищу</string>
<string name="successful_import_label">Импорт успешен</string> <string name="successful_import_label">Импорт успешен</string>

View File

@ -573,7 +573,7 @@
<string name="database_import_label">Import databázy</string> <string name="database_import_label">Import databázy</string>
<string name="database_import_warning">Import databázy prepíše všetky vaše aktuálne odbery a históriu prehrávania. Aktuálnu databázu by ste mali exportovať ako zálohu. Chcete ju prepísať\?</string> <string name="database_import_warning">Import databázy prepíše všetky vaše aktuálne odbery a históriu prehrávania. Aktuálnu databázu by ste mali exportovať ako zálohu. Chcete ju prepísať\?</string>
<string name="please_wait">Prosím čakajte…</string> <string name="please_wait">Prosím čakajte…</string>
<string name="export_error_label">Chyba exportu</string> <string name="import_export_error_label">Chyba exportu</string>
<string name="export_success_title">Export bol úspešný</string> <string name="export_success_title">Export bol úspešný</string>
<string name="opml_import_ask_read_permission">Na načítanie súboru OPML je potrebný prístup k externému úložisku</string> <string name="opml_import_ask_read_permission">Na načítanie súboru OPML je potrebný prístup k externému úložisku</string>
<string name="successful_import_label">Import úspešný</string> <string name="successful_import_label">Import úspešný</string>

View File

@ -382,7 +382,7 @@
<string name="database_import_label">Uvoz podatkovne baze</string> <string name="database_import_label">Uvoz podatkovne baze</string>
<string name="database_import_warning">Uvoz podatkovne baze bo nadomestil vse vaše trenutne naročnine in zgodovino predvajanja. Priporočamo, da svojo trenutno zbirko podatkov izvozite kot varnostno kopijo. Ali želite vseeno zamenjati\?</string> <string name="database_import_warning">Uvoz podatkovne baze bo nadomestil vse vaše trenutne naročnine in zgodovino predvajanja. Priporočamo, da svojo trenutno zbirko podatkov izvozite kot varnostno kopijo. Ali želite vseeno zamenjati\?</string>
<string name="please_wait">Prosimo počakajte…</string> <string name="please_wait">Prosimo počakajte…</string>
<string name="export_error_label">Napaka pri izvozu</string> <string name="import_export_error_label">Napaka pri izvozu</string>
<string name="export_success_title">Izvoz uspešen</string> <string name="export_success_title">Izvoz uspešen</string>
<string name="opml_import_ask_read_permission">Za branje datoteke OPML je potreben dostop do zunanjega pomnilnika</string> <string name="opml_import_ask_read_permission">Za branje datoteke OPML je potreben dostop do zunanjega pomnilnika</string>
<string name="successful_import_label">Uvoz uspešen</string> <string name="successful_import_label">Uvoz uspešen</string>

View File

@ -545,7 +545,7 @@
<string name="database_import_label">Databasimport</string> <string name="database_import_label">Databasimport</string>
<string name="database_import_warning">Importering av en databas kommer att ersätta alla dina befintliga prenumerationer och spelhistoriken. Du bör exportera din nuvarande databas som en backup. Vill du ersätta den\?</string> <string name="database_import_warning">Importering av en databas kommer att ersätta alla dina befintliga prenumerationer och spelhistoriken. Du bör exportera din nuvarande databas som en backup. Vill du ersätta den\?</string>
<string name="please_wait">Vänta…</string> <string name="please_wait">Vänta…</string>
<string name="export_error_label">Exporteringsfel</string> <string name="import_export_error_label">Exporteringsfel</string>
<string name="export_success_title">Exporten lyckades</string> <string name="export_success_title">Exporten lyckades</string>
<string name="opml_import_ask_read_permission">Tillgång till extern lagring krävs för att läsa OPML-filen</string> <string name="opml_import_ask_read_permission">Tillgång till extern lagring krävs för att läsa OPML-filen</string>
<string name="successful_import_label">Importering lyckades</string> <string name="successful_import_label">Importering lyckades</string>

View File

@ -522,7 +522,7 @@
<string name="database_import_label">Veri tabanını içe aktarma</string> <string name="database_import_label">Veri tabanını içe aktarma</string>
<string name="database_import_warning">Bir veritabanını içe aktarmak, mevcut tüm aboneliklerinizi ve çalma geçmişinizi değiştirecektir. Mevcut veritabanınızı yedek olarak dışa aktarmalısınız. Yerini değiştirmek istiyor musunuz\?</string> <string name="database_import_warning">Bir veritabanını içe aktarmak, mevcut tüm aboneliklerinizi ve çalma geçmişinizi değiştirecektir. Mevcut veritabanınızı yedek olarak dışa aktarmalısınız. Yerini değiştirmek istiyor musunuz\?</string>
<string name="please_wait">Lütfen bekleyin…</string> <string name="please_wait">Lütfen bekleyin…</string>
<string name="export_error_label">Dışa aktarma hatası</string> <string name="import_export_error_label">Dışa aktarma hatası</string>
<string name="export_success_title">Dışa aktarma başarılı</string> <string name="export_success_title">Dışa aktarma başarılı</string>
<string name="opml_import_ask_read_permission">OPML dosyasını okumak için harici depolama alanına erişim gereklidir</string> <string name="opml_import_ask_read_permission">OPML dosyasını okumak için harici depolama alanına erişim gereklidir</string>
<string name="successful_import_label">İçe aktarma başarılı</string> <string name="successful_import_label">İçe aktarma başarılı</string>

View File

@ -573,7 +573,7 @@
<string name="database_import_label">Імпортувати базу даних</string> <string name="database_import_label">Імпортувати базу даних</string>
<string name="database_import_warning">Імпорт бази даних замінить усі ваші поточні підписки та історію відтворення. Ви повинні експортувати свою поточну базу даних як резервну копію. Бажаєте замінити\?</string> <string name="database_import_warning">Імпорт бази даних замінить усі ваші поточні підписки та історію відтворення. Ви повинні експортувати свою поточну базу даних як резервну копію. Бажаєте замінити\?</string>
<string name="please_wait">Будь ласка, зачекайте…</string> <string name="please_wait">Будь ласка, зачекайте…</string>
<string name="export_error_label">Помилка експорту</string> <string name="import_export_error_label">Помилка експорту</string>
<string name="export_success_title">Успішний експорт</string> <string name="export_success_title">Успішний експорт</string>
<string name="opml_import_ask_read_permission">Щоб прочитати файл OPML потрібен доступ до зовнішнього носія</string> <string name="opml_import_ask_read_permission">Щоб прочитати файл OPML потрібен доступ до зовнішнього носія</string>
<string name="successful_import_label">Імпорт - успішний</string> <string name="successful_import_label">Імпорт - успішний</string>

View File

@ -532,7 +532,7 @@
<string name="database_import_label">数据库导入</string> <string name="database_import_label">数据库导入</string>
<string name="database_import_warning">导入数据库将替换所有当前订阅和播放历史记录。您应该将当前数据库导出为备份。您要替换吗?</string> <string name="database_import_warning">导入数据库将替换所有当前订阅和播放历史记录。您应该将当前数据库导出为备份。您要替换吗?</string>
<string name="please_wait">请等待…</string> <string name="please_wait">请等待…</string>
<string name="export_error_label">导出出错</string> <string name="import_export_error_label">导出出错</string>
<string name="export_success_title">成功导出</string> <string name="export_success_title">成功导出</string>
<string name="opml_import_ask_read_permission">读取 OPML 文件需要访问外部存储的权限</string> <string name="opml_import_ask_read_permission">读取 OPML 文件需要访问外部存储的权限</string>
<string name="successful_import_label">导入成功</string> <string name="successful_import_label">导入成功</string>

View File

@ -311,7 +311,7 @@
<string name="database_import_label">資料庫匯入</string> <string name="database_import_label">資料庫匯入</string>
<string name="database_import_warning">匯入的資料將取代您目前的訂閱清單與播放歷史紀錄,您最好先匯出當前的資料庫以便備份。要取代目前的資料嗎?</string> <string name="database_import_warning">匯入的資料將取代您目前的訂閱清單與播放歷史紀錄,您最好先匯出當前的資料庫以便備份。要取代目前的資料嗎?</string>
<string name="please_wait">請稍候…</string> <string name="please_wait">請稍候…</string>
<string name="export_error_label">匯出錯誤</string> <string name="import_export_error_label">匯出錯誤</string>
<string name="export_success_title">匯出成功</string> <string name="export_success_title">匯出成功</string>
<string name="opml_import_ask_read_permission">讀取 OPML 檔需要存取外部儲存空間的權限</string> <string name="opml_import_ask_read_permission">讀取 OPML 檔需要存取外部儲存空間的權限</string>
<string name="successful_import_label">匯入完畢</string> <string name="successful_import_label">匯入完畢</string>

View File

@ -639,7 +639,7 @@
<string name="import_directory_toast">Only accepting directory name including: </string> <string name="import_directory_toast">Only accepting directory name including: </string>
<string name="import_file_type_toast">Only accepting file extension: </string> <string name="import_file_type_toast">Only accepting file extension: </string>
<string name="please_wait">Please wait&#8230;</string> <string name="please_wait">Please wait&#8230;</string>
<string name="export_error_label">Export error</string> <string name="import_export_error_label">Import/Export error</string>
<string name="export_success_title">Export successful</string> <string name="export_success_title">Export successful</string>
<string name="opml_import_ask_read_permission">Access to external storage is required to read the OPML file</string> <string name="opml_import_ask_read_permission">Access to external storage is required to read the OPML file</string>
<string name="successful_import_label">Import successful</string> <string name="successful_import_label">Import successful</string>

View File

@ -3,9 +3,9 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:search="http://schemas.android.com/apk/com.bytehamster.lib.preferencesearch"> xmlns:search="http://schemas.android.com/apk/com.bytehamster.lib.preferencesearch">
<Preference <!-- <Preference-->
android:title="@string/choose_data_directory" <!-- android:title="@string/choose_data_directory"-->
android:key="prefChooseDataDir"/> <!-- android:key="prefChooseDataDir"/>-->
<PreferenceCategory android:title="@string/automation"> <PreferenceCategory android:title="@string/automation">
<ac.mdiq.podcini.preferences.MaterialListPreference <ac.mdiq.podcini.preferences.MaterialListPreference

View File

@ -4,7 +4,6 @@ import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub
import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.enqueueLocation
import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue
import ac.mdiq.podcini.storage.database.Episodes.addToHistory import ac.mdiq.podcini.storage.database.Episodes.addToHistory
import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodes import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodes
@ -14,8 +13,10 @@ import ac.mdiq.podcini.storage.database.Episodes.getEpisodeMedia
import ac.mdiq.podcini.storage.database.Episodes.persistEpisode import ac.mdiq.podcini.storage.database.Episodes.persistEpisode
import ac.mdiq.podcini.storage.database.Episodes.persistEpisodeMedia import ac.mdiq.podcini.storage.database.Episodes.persistEpisodeMedia
import ac.mdiq.podcini.storage.database.Feeds.deleteFeedSync import ac.mdiq.podcini.storage.database.Feeds.deleteFeedSync
import ac.mdiq.podcini.storage.database.Queues
import ac.mdiq.podcini.storage.database.Queues.addToQueue import ac.mdiq.podcini.storage.database.Queues.addToQueue
import ac.mdiq.podcini.storage.database.Queues.clearQueue import ac.mdiq.podcini.storage.database.Queues.clearQueue
import ac.mdiq.podcini.storage.database.Queues.enqueueLocation
import ac.mdiq.podcini.storage.database.Queues.moveInQueue import ac.mdiq.podcini.storage.database.Queues.moveInQueue
import ac.mdiq.podcini.storage.database.Queues.removeFromQueue import ac.mdiq.podcini.storage.database.Queues.removeFromQueue
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
@ -540,7 +541,7 @@ class DbWriterTest {
@Throws(Exception::class) @Throws(Exception::class)
private fun queueTestSetupMultipleItems(numItems: Int): Feed { private fun queueTestSetupMultipleItems(numItems: Int): Feed {
enqueueLocation = UserPreferences.EnqueueLocation.BACK enqueueLocation = Queues.EnqueueLocation.BACK
val feed = Feed("url", null, "title") val feed = Feed("url", null, "title")
feed.episodes.clear() feed.episodes.clear()
for (i in 0 until numItems) { for (i in 0 until numItems) {

View File

@ -3,8 +3,8 @@ package ac.mdiq.podcini.storage
import ac.mdiq.podcini.feed.FeedMother.anyFeed import ac.mdiq.podcini.feed.FeedMother.anyFeed
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub
import ac.mdiq.podcini.preferences.UserPreferences.EnqueueLocation
import ac.mdiq.podcini.storage.database.Queues import ac.mdiq.podcini.storage.database.Queues
import ac.mdiq.podcini.storage.database.Queues.EnqueueLocation
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.Playable import ac.mdiq.podcini.storage.model.Playable
@ -74,7 +74,7 @@ open class BasicTest {
var idsExpected: List<Long?>? = null var idsExpected: List<Long?>? = null
@Parameterized.Parameter(2) @Parameterized.Parameter(2)
var options: EnqueueLocation? = null var options: Queues.EnqueueLocation? = null
@Parameterized.Parameter(3) @Parameterized.Parameter(3)
var curQueue: List<Episode?>? = null var curQueue: List<Episode?>? = null

View File

@ -14,7 +14,7 @@ buildscript {
} }
plugins { plugins {
id 'io.realm.kotlin' version '2.0.0' apply false id 'io.realm.kotlin' version '2.1.0' apply false
// id 'com.google.devtools.ksp' version '2.0.0-1.0.23' apply false // id 'com.google.devtools.ksp' version '2.0.0-1.0.23' apply false
} }

View File

@ -1,3 +1,17 @@
# 6.0.13
* removed from preferences "Choose data folder", it's not suitable for newer Androids
* some class restructuring, especially some functions moved out of Userpreferences
* fixed issue of early termination when exporting a large set of media files
* fixed the mal-functioning feeds and episodes search
* updated realm.kotlin to 2.1.0
# 5.5.5
* this is an extra minor release for better migration to Podcini 6
* fixed issue (in 5.5.4) of terminating pre-maturely when exporting a large set of media files
* this release is not updated to F-Droid due to Podcini 6 listing in progress
# 6.0.12 # 6.0.12
* re-enabled import of downloaded media files, which can be used to migrate from 5.5.4. Media files imported are restricted to existing feeds and episodes. * re-enabled import of downloaded media files, which can be used to migrate from 5.5.4. Media files imported are restricted to existing feeds and episodes.

View File

@ -0,0 +1,8 @@
Version 6.0.13 brings several changes:
* removed from preferences "Choose data folder", it's not suitable for newer Androids
* some class restructuring, especially some functions moved out of Userpreferences
* fixed issue of early termination when exporting a large set of media files
* fixed the mal-functioning feeds and episodes search
* updated realm.kotlin to 2.1.0