6.0.13 commit
This commit is contained in:
parent
b4b850baba
commit
91423f9267
|
@ -126,8 +126,8 @@ android {
|
|||
buildConfig true
|
||||
}
|
||||
defaultConfig {
|
||||
versionCode 3020212
|
||||
versionName "6.0.12"
|
||||
versionCode 3020213
|
||||
versionName "6.0.13"
|
||||
|
||||
applicationId "ac.mdiq.podcini.R"
|
||||
def commit = ""
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package de.test.podcini.storage
|
||||
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileStreaming
|
||||
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isAllowMobileStreaming
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isFollowQueue
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isFollowQueue
|
||||
import ac.mdiq.podcini.storage.algorithms.AutoDownloads
|
||||
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.downloadAlgorithm
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
|
|
|
@ -11,6 +11,12 @@ import androidx.test.espresso.matcher.ViewMatchers
|
|||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
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.storage.algorithms.AutoCleanups.APCleanupAlgorithm
|
||||
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.ExceptFavoriteCleanupAlgorithm
|
||||
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.fastForwardSecs
|
||||
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.isEnableAutodownload
|
||||
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.shouldDeleteRemoveFromQueue
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.shouldPauseForFocusLoss
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.showNextChapterOnFullNotification
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.showPlaybackSpeedOnFullNotification
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.showSkipOnFullNotification
|
||||
import ac.mdiq.podcini.storage.database.Queues
|
||||
import ac.mdiq.podcini.storage.database.Queues.enqueueLocation
|
||||
import de.test.podcini.EspressoTestUtils
|
||||
import org.awaitility.Awaitility
|
||||
import org.junit.Assert
|
||||
|
@ -104,13 +105,13 @@ class PreferencesTest {
|
|||
@Test
|
||||
fun testEnqueueLocation() {
|
||||
EspressoTestUtils.clickPreference(R.string.playback_pref)
|
||||
doTestEnqueueLocation(R.string.enqueue_location_after_current, EnqueueLocation.AFTER_CURRENTLY_PLAYING)
|
||||
doTestEnqueueLocation(R.string.enqueue_location_front, EnqueueLocation.FRONT)
|
||||
doTestEnqueueLocation(R.string.enqueue_location_back, EnqueueLocation.BACK)
|
||||
doTestEnqueueLocation(R.string.enqueue_location_random, EnqueueLocation.RANDOM)
|
||||
doTestEnqueueLocation(R.string.enqueue_location_after_current, Queues.EnqueueLocation.AFTER_CURRENTLY_PLAYING)
|
||||
doTestEnqueueLocation(R.string.enqueue_location_front, Queues.EnqueueLocation.FRONT)
|
||||
doTestEnqueueLocation(R.string.enqueue_location_back, Queues.EnqueueLocation.BACK)
|
||||
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)
|
||||
Espresso.onView(ViewMatchers.withText(optionResId)).perform(ViewActions.click())
|
||||
Awaitility.await().atMost(1000, TimeUnit.MILLISECONDS)
|
||||
|
@ -138,7 +139,7 @@ class PreferencesTest {
|
|||
Espresso.onView(ViewMatchers.withText(R.string.pref_pauseOnHeadsetDisconnect_title))
|
||||
.perform(ViewActions.click())
|
||||
Awaitility.await().atMost(1000, TimeUnit.MILLISECONDS)
|
||||
.until(UserPreferences::isPauseOnHeadsetDisconnect)
|
||||
.until(PlaybackService::isPauseOnHeadsetDisconnect)
|
||||
}
|
||||
val unpauseOnHeadsetReconnect = isUnpauseOnHeadsetReconnect
|
||||
Espresso.onView(ViewMatchers.withText(R.string.pref_unpauseOnHeadsetReconnect_title))
|
||||
|
@ -158,7 +159,7 @@ class PreferencesTest {
|
|||
Espresso.onView(ViewMatchers.withText(R.string.pref_pauseOnHeadsetDisconnect_title))
|
||||
.perform(ViewActions.click())
|
||||
Awaitility.await().atMost(1000, TimeUnit.MILLISECONDS)
|
||||
.until(UserPreferences::isPauseOnHeadsetDisconnect)
|
||||
.until(PlaybackService::isPauseOnHeadsetDisconnect)
|
||||
}
|
||||
val unpauseOnBluetoothReconnect = isUnpauseOnBluetoothReconnect
|
||||
Espresso.onView(ViewMatchers.withText(R.string.pref_unpauseOnBluetoothReconnect_title))
|
||||
|
|
|
@ -27,7 +27,6 @@ class PodciniApp : Application() {
|
|||
ClientConfig.applicationCallbacks = ApplicationCallbacksImpl()
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler(CrashReportWriter())
|
||||
// RxJavaErrorHandlerSetup.setupRxJavaErrorHandler()
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
val builder: StrictMode.VmPolicy.Builder = StrictMode.VmPolicy.Builder()
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
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. */
|
||||
/** Get machine-readable code. */
|
||||
enum class DownloadError(@JvmField val code: Int) {
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
package ac.mdiq.podcini.net.download.service
|
||||
|
||||
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.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 android.webkit.URLUtil
|
||||
import io.realm.kotlin.ext.isManaged
|
||||
import org.apache.commons.io.FilenameUtils
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
|
@ -17,8 +16,6 @@ import java.io.File
|
|||
*/
|
||||
object DownloadRequestCreator {
|
||||
private val TAG: String = DownloadRequestCreator::class.simpleName ?: "Anonymous"
|
||||
private const val FEED_DOWNLOADPATH = "cache/"
|
||||
private const val MEDIA_DOWNLOADPATH = "media/"
|
||||
|
||||
@JvmStatic
|
||||
fun create(feed: Feed): DownloadRequest.Builder {
|
||||
|
@ -53,83 +50,4 @@ object DownloadRequestCreator {
|
|||
|
||||
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")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.sync.model.EpisodeAction
|
||||
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileEpisodeDownload
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_ENQUEUE_DOWNLOADED
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.storage.database.Episodes
|
||||
import ac.mdiq.podcini.storage.database.LogsAndStats
|
||||
import ac.mdiq.podcini.storage.database.Queues
|
||||
|
@ -102,29 +105,35 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
|||
private val constraints: Constraints
|
||||
get() {
|
||||
val constraints = Builder()
|
||||
if (UserPreferences.isAllowMobileEpisodeDownload) constraints.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
if (isAllowMobileEpisodeDownload) constraints.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
else constraints.setRequiredNetworkType(NetworkType.UNMETERED)
|
||||
|
||||
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")
|
||||
val workRequest: OneTimeWorkRequest.Builder = OneTimeWorkRequest.Builder(EpisodeDownloadWorker::class.java)
|
||||
.setInitialDelay(0L, TimeUnit.MILLISECONDS)
|
||||
.addTag(WORK_TAG)
|
||||
.addTag(WORK_TAG_EPISODE_URL + item.media!!.downloadUrl)
|
||||
if (UserPreferences.enqueueDownloadedEpisodes()) {
|
||||
if (enqueueDownloadedEpisodes()) {
|
||||
runBlocking { Queues.addToQueueSync(false, item) }
|
||||
workRequest.addTag(WORK_DATA_WAS_QUEUED)
|
||||
}
|
||||
workRequest.setInputData(Data.Builder().putLong(WORK_DATA_MEDIA_ID, item.media!!.id).build())
|
||||
return workRequest
|
||||
}
|
||||
private fun enqueueDownloadedEpisodes(): Boolean {
|
||||
return appPrefs.getBoolean(PREF_ENQUEUE_DOWNLOADED, true)
|
||||
}
|
||||
}
|
||||
|
||||
class EpisodeDownloadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||
private var downloader: Downloader? = null
|
||||
private val isLastRunAttempt: Boolean
|
||||
get() = runAttemptCount >= 2
|
||||
|
||||
@UnstableApi
|
||||
override fun doWork(): Result {
|
||||
|
@ -136,7 +145,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
|||
Log.e(TAG, "media is null for mediaId: $mediaId")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
val request = create(media).build()
|
||||
val progressUpdaterThread: Thread = object : Thread() {
|
||||
override fun run() {
|
||||
|
@ -168,7 +176,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
|||
}
|
||||
if (result == Result.failure() && downloader?.downloadRequest?.destination != null)
|
||||
FileUtils.deleteQuietly(File(downloader!!.downloadRequest.destination!!))
|
||||
|
||||
progressUpdaterThread.interrupt()
|
||||
try {
|
||||
progressUpdaterThread.join()
|
||||
|
@ -185,17 +192,15 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
|||
Logd(TAG, "Worker for " + media.downloadUrl + " returned.")
|
||||
return result
|
||||
}
|
||||
|
||||
override fun onStopped() {
|
||||
super.onStopped()
|
||||
downloader?.cancel()
|
||||
}
|
||||
|
||||
override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> {
|
||||
return Futures.immediateFuture(ForegroundInfo(R.id.notification_downloading, generateProgressNotification()))
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) private fun performDownload(media: EpisodeMedia, request: DownloadRequest): Result {
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun performDownload(media: EpisodeMedia, request: DownloadRequest): Result {
|
||||
Logd(TAG, "starting performDownload")
|
||||
val dest = File(request.destination)
|
||||
if (!dest.exists()) {
|
||||
|
@ -205,7 +210,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
|||
Log.e(TAG, "performDownload Unable to create file")
|
||||
}
|
||||
}
|
||||
|
||||
if (dest.exists()) {
|
||||
try {
|
||||
media.setfileUrlOrNull(request.destination)
|
||||
|
@ -214,13 +218,11 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
|||
Log.e(TAG, "performDownload Exception in writeFileUrl: " + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
downloader = DefaultDownloaderFactory().create(request)
|
||||
if (downloader == null) {
|
||||
Log.e(TAG, "performDownload Unable to create downloader")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
try {
|
||||
downloader!!.call()
|
||||
} catch (e: Exception) {
|
||||
|
@ -229,10 +231,8 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
|||
sendErrorNotification(request.title?:"")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
// This also happens when the worker was preempted, not just when the user cancelled it
|
||||
if (downloader!!.cancelled) return Result.success()
|
||||
|
||||
val status = downloader!!.result
|
||||
if (status.isSuccessful) {
|
||||
val handler = MediaDownloadedHandler(applicationContext, downloader!!.result, request)
|
||||
|
@ -240,14 +240,12 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
|||
LogsAndStats.addDownloadStatus(handler.updatedStatus)
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (status.reason == DownloadError.ERROR_HTTP_DATA_ERROR && status.reasonDetailed.toInt() == 416) {
|
||||
Logd(TAG, "Requested invalid range, restarting download from the beginning")
|
||||
if (downloader?.downloadRequest?.destination != null) FileUtils.deleteQuietly(File(downloader!!.downloadRequest.destination!!))
|
||||
sendMessage(request.title?:"", false)
|
||||
return retry3times()
|
||||
}
|
||||
|
||||
Log.e(TAG, "Download failed ${request.title} ${status.reason}")
|
||||
LogsAndStats.addDownloadStatus(status)
|
||||
if (status.reason == DownloadError.ERROR_FORBIDDEN || status.reason == DownloadError.ERROR_NOT_FOUND
|
||||
|
@ -260,7 +258,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
|||
sendMessage(request.title?:"", false)
|
||||
return retry3times()
|
||||
}
|
||||
|
||||
private fun retry3times(): Result {
|
||||
if (isLastRunAttempt) {
|
||||
Log.e(TAG, "retry3times failure on isLastRunAttempt")
|
||||
|
@ -268,10 +265,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
|||
return Result.failure()
|
||||
} else return Result.retry()
|
||||
}
|
||||
|
||||
private val isLastRunAttempt: Boolean
|
||||
get() = runAttemptCount >= 2
|
||||
|
||||
private fun sendMessage(episodeTitle: String, isImmediateFail: Boolean) {
|
||||
var episodeTitle = episodeTitle
|
||||
val retrying = !isLastRunAttempt && !isImmediateFail
|
||||
|
@ -282,26 +275,22 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
|||
episodeTitle), { ctx: Context -> MainActivityStarter(ctx).withDownloadLogsOpen().start() }, applicationContext.getString(
|
||||
R.string.download_error_details)))
|
||||
}
|
||||
|
||||
private fun getDownloadLogsIntent(context: Context): PendingIntent {
|
||||
val intent = MainActivityStarter(context).withDownloadLogsOpen().getIntent()
|
||||
return PendingIntent.getActivity(context, R.id.pending_intent_download_service_report, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
private fun getDownloadsIntent(context: Context): PendingIntent {
|
||||
val intent = MainActivityStarter(context).withFragmentLoaded("DownloadsFragment").getIntent()
|
||||
return PendingIntent.getActivity(context, R.id.pending_intent_download_service_notification, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
private fun sendErrorNotification(title: String) {
|
||||
// TODO: need to get number of subscribers in SharedFlow
|
||||
// if (EventBus.getDefault().hasSubscriberForEvent(FlowEvent.MessageEvent::class.java)) {
|
||||
// sendMessage(title, false)
|
||||
// return
|
||||
// }
|
||||
|
||||
val builder = NotificationCompat.Builder(applicationContext, NotificationUtils.CHANNEL_ID_DOWNLOAD_ERROR)
|
||||
builder.setTicker(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
|
||||
nm.notify(R.id.notification_download_report, builder.build())
|
||||
}
|
||||
|
||||
private fun generateProgressNotification(): Notification {
|
||||
val bigTextB = StringBuilder()
|
||||
var progressCopy: Map<String, Int>
|
||||
|
@ -326,7 +314,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
|||
val bigText = bigTextB.toString().trim { it <= ' ' }
|
||||
val contentText = if (progressCopy.size == 1) bigText
|
||||
else applicationContext.resources.getQuantityString(R.plurals.downloads_left, progressCopy.size, progressCopy.size)
|
||||
|
||||
val builder = NotificationCompat.Builder(applicationContext, NotificationUtils.CHANNEL_ID_DOWNLOADING)
|
||||
builder.setTicker(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 {
|
||||
|
||||
@UnstableApi override fun run() {
|
||||
val media = Episodes.getEpisodeMedia(request.feedfileId)
|
||||
if (media == null) {
|
||||
|
@ -358,16 +344,11 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
|||
Logd(TAG, "media.episode.isNew: ${media.episode?.isNew} ${media.episode?.playState}")
|
||||
media.setfileUrlOrNull(request.destination)
|
||||
if (request.destination != null) media.size = File(request.destination).length()
|
||||
|
||||
media.checkEmbeddedPicture() // enforce check
|
||||
|
||||
// check if file has chapters
|
||||
if (media.episode != null && media.episode!!.chapters.isEmpty())
|
||||
media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context))
|
||||
|
||||
if (media.episode != null && media.episode!!.chapters.isEmpty()) media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context))
|
||||
if (media.episode?.podcastIndexChapterUrl != null)
|
||||
ChapterUtils.loadChaptersFromUrl(media.episode!!.podcastIndexChapterUrl!!, false)
|
||||
|
||||
// Get duration
|
||||
var durationStr: String? = null
|
||||
try {
|
||||
|
@ -385,7 +366,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
|||
}
|
||||
val item = media.episode
|
||||
item?.media = media
|
||||
|
||||
try {
|
||||
// we've received the media, we don't want to autodownload it again
|
||||
if (item != null) {
|
||||
|
@ -404,7 +384,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
|||
Log.e(TAG, "ExecutionException in MediaHandlerThread: " + e.message)
|
||||
updatedStatus = DownloadResult(media.id, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, e.message?:"")
|
||||
}
|
||||
|
||||
if (item != null) {
|
||||
val action = EpisodeAction.Builder(item, EpisodeAction.DOWNLOAD)
|
||||
.currentTimestamp()
|
||||
|
|
|
@ -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.feed.parser.FeedHandler
|
||||
import ac.mdiq.podcini.net.feed.parser.FeedHandlerResult
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileFeedRefresh
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
|
@ -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.isVpnOverWifi
|
||||
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.database.Feeds
|
||||
import ac.mdiq.podcini.storage.database.LogsAndStats
|
||||
|
@ -53,13 +56,20 @@ import java.util.concurrent.TimeUnit
|
|||
import javax.xml.parsers.ParserConfigurationException
|
||||
|
||||
object FeedUpdateManager {
|
||||
private val TAG: String = FeedUpdateManager::class.simpleName ?: "Anonymous"
|
||||
|
||||
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_MANUAL = "feedUpdateManual"
|
||||
const val EXTRA_FEED_ID: String = "feed_id"
|
||||
const val EXTRA_NEXT_PAGE: String = "next_page"
|
||||
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
|
||||
|
@ -67,12 +77,12 @@ object FeedUpdateManager {
|
|||
*/
|
||||
@JvmStatic
|
||||
fun restartUpdateAlarm(context: Context, replace: Boolean) {
|
||||
if (UserPreferences.isAutoUpdateDisabled) {
|
||||
if (isAutoUpdateDisabled) {
|
||||
WorkManager.getInstance(context).cancelUniqueWork(WORK_ID_FEED_UPDATE)
|
||||
} 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()
|
||||
.setRequiredNetworkType(if (UserPreferences.isAllowMobileFeedRefresh) NetworkType.CONNECTED else NetworkType.UNMETERED)
|
||||
.setRequiredNetworkType(if (isAllowMobileFeedRefresh) NetworkType.CONNECTED else NetworkType.UNMETERED)
|
||||
.build())
|
||||
.build()
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(WORK_ID_FEED_UPDATE,
|
||||
|
@ -116,7 +126,7 @@ object FeedUpdateManager {
|
|||
.setTitle(R.string.feed_refresh_title)
|
||||
.setPositiveButton(R.string.confirm_mobile_streaming_button_once) { _: DialogInterface?, _: Int -> runOnce(context, feed) }
|
||||
.setNeutralButton(R.string.confirm_mobile_streaming_button_always) { _: DialogInterface?, _: Int ->
|
||||
UserPreferences.isAllowMobileFeedRefresh = true
|
||||
isAllowMobileFeedRefresh = true
|
||||
runOnce(context, feed)
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
|
|
|
@ -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.nextcloud.NextcloudSyncService
|
||||
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueStorage
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileFor
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.setAllowMobileFor
|
||||
import ac.mdiq.podcini.net.utils.UrlChecker.containsUrl
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.gpodnetNotificationsEnabled
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isAllowMobileSync
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_GPODNET_NOTIFICATIONS
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.storage.database.Episodes.getEpisodeByGuidOrUrl
|
||||
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
|
||||
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.Queues.removeFromQueue
|
||||
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.utils.EpisodeUtil.hasAlmostEnded
|
||||
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.util.Logd
|
||||
import ac.mdiq.podcini.util.event.*
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.OptIn
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
Logd(TAG, "Posting sync error notification")
|
||||
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) {
|
||||
isCurrentlyActive = active
|
||||
}
|
||||
var isAllowMobileSync: Boolean
|
||||
get() = isAllowMobileFor("sync")
|
||||
set(allow) {
|
||||
setAllowMobileFor("sync", allow)
|
||||
}
|
||||
|
||||
private fun getWorkRequest(): OneTimeWorkRequest.Builder {
|
||||
val constraints = Builder()
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
package ac.mdiq.podcini.net.sync
|
||||
|
||||
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink.clearQueue
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.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 android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
|
@ -55,6 +56,10 @@ object SynchronizationCredentials {
|
|||
preferences.edit().putInt(PREF_HOSTPORT, value).apply()
|
||||
}
|
||||
|
||||
fun setGpodnetNotificationsEnabled() {
|
||||
appPrefs.edit().putBoolean(PREF_GPODNET_NOTIFICATIONS, true).apply()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun clear(context: Context) {
|
||||
username = null
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
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.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.wifi.WifiManager
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import android.os.Build
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedReader
|
||||
|
@ -22,6 +26,39 @@ object NetworkUtils {
|
|||
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
|
||||
val isAutoDownloadAllowed: Boolean
|
||||
get() {
|
||||
|
@ -29,11 +66,11 @@ object NetworkUtils {
|
|||
val networkInfo = cm.activeNetworkInfo ?: return false
|
||||
return when (networkInfo.type) {
|
||||
ConnectivityManager.TYPE_WIFI -> {
|
||||
if (UserPreferences.isEnableAutodownloadWifiFilter) isInAllowedWifiNetwork
|
||||
if (isEnableAutodownloadWifiFilter) isInAllowedWifiNetwork
|
||||
else !isNetworkMetered
|
||||
}
|
||||
ConnectivityManager.TYPE_ETHERNET -> true
|
||||
else -> UserPreferences.isAllowMobileAutoDownload || !isNetworkRestricted
|
||||
else -> isAllowMobileAutoDownload || !isNetworkRestricted
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,9 +81,21 @@ object NetworkUtils {
|
|||
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
|
||||
val isEpisodeDownloadAllowed: Boolean
|
||||
get() = UserPreferences.isAllowMobileEpisodeDownload || !isNetworkRestricted
|
||||
get() = isAllowMobileEpisodeDownload || !isNetworkRestricted
|
||||
|
||||
@JvmStatic
|
||||
val isEpisodeHeadDownloadAllowed: Boolean
|
||||
|
@ -54,17 +103,23 @@ object NetworkUtils {
|
|||
// that is probably not even considered a download by most users
|
||||
get() = isImageAllowed
|
||||
|
||||
var isAllowMobileImages: Boolean
|
||||
get() = isAllowMobileFor("images")
|
||||
set(allow) {
|
||||
setAllowMobileFor("images", allow)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val isImageAllowed: Boolean
|
||||
get() = UserPreferences.isAllowMobileImages || !isNetworkRestricted
|
||||
get() = isAllowMobileImages || !isNetworkRestricted
|
||||
|
||||
@JvmStatic
|
||||
val isStreamingAllowed: Boolean
|
||||
get() = UserPreferences.isAllowMobileStreaming || !isNetworkRestricted
|
||||
get() = isAllowMobileStreaming || !isNetworkRestricted
|
||||
|
||||
@JvmStatic
|
||||
val isFeedRefreshAllowed: Boolean
|
||||
get() = UserPreferences.isAllowMobileFeedRefresh || !isNetworkRestricted
|
||||
get() = isAllowMobileFeedRefresh || !isNetworkRestricted
|
||||
|
||||
@JvmStatic
|
||||
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
|
||||
get() {
|
||||
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())
|
||||
}
|
||||
|
||||
|
|
|
@ -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.curState
|
||||
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.FeedPreferences
|
||||
import ac.mdiq.podcini.storage.model.Playable
|
||||
|
@ -301,6 +305,17 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
|||
@JvmField
|
||||
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 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 (mediaType != null && playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL) playbackSpeed = UserPreferences.getPlaybackSpeed(mediaType)
|
||||
if (mediaType != null && playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL) playbackSpeed = getPlaybackSpeed(mediaType)
|
||||
return playbackSpeed
|
||||
}
|
||||
|
||||
fun getPlaybackSpeed(mediaType: MediaType): Float {
|
||||
return if (mediaType == MediaType.VIDEO) videoPlaybackSpeed else audioPlaybackSpeed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package ac.mdiq.podcini.playback.service
|
|||
|
||||
import ac.mdiq.podcini.R
|
||||
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.playback.PlaybackServiceStarter
|
||||
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.isInTimeRange
|
||||
import ac.mdiq.podcini.preferences.SleepTimerPreferences.timerMillis
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_FAVORITE_KEEPS_EPISODE
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_FOLLOW_QUEUE
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_HARDWARE_FORWARD_BUTTON
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_HARDWARE_PREVIOUS_BUTTON
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_PAUSE_ON_HEADSET_DISCONNECT
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_PERSISTENT_NOTIFICATION
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_SKIP_KEEPS_EPISODE
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_UNPAUSE_ON_HEADSET_RECONNECT
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
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.shouldAutoDeleteItem
|
||||
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.storage.database.Episodes.addToHistory
|
||||
import ac.mdiq.podcini.storage.database.Episodes.deleteMediaSync
|
||||
import ac.mdiq.podcini.storage.database.Episodes.getEpisodeByGuidOrUrl
|
||||
import ac.mdiq.podcini.storage.database.Episodes.setPlayStateSync
|
||||
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.removeFromQueueSync
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||
import ac.mdiq.podcini.storage.model.*
|
||||
import ac.mdiq.podcini.storage.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_PAUSED
|
||||
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.Playable
|
||||
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.widget.WidgetUpdater.WidgetState
|
||||
import ac.mdiq.podcini.util.IntentUtils.sendLocalBroadcast
|
||||
|
@ -82,6 +78,7 @@ import android.view.KeyEvent
|
|||
import android.view.ViewConfiguration
|
||||
import android.webkit.URLUtil
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.media3.common.MediaMetadata
|
||||
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) {
|
||||
Logd(TAG, "onPlaybackStart position: $position")
|
||||
taskManager.startWidgetUpdater()
|
||||
|
@ -1178,6 +1183,37 @@ class PlaybackService : MediaSessionService() {
|
|||
var currentMediaType: MediaType? = MediaType.UNKNOWN
|
||||
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) {
|
||||
val playable = curMedia
|
||||
if (playable is EpisodeMedia) {
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
package ac.mdiq.podcini.preferences
|
||||
|
||||
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.updateFeed
|
||||
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 android.app.backup.BackupAgentHelper
|
||||
import android.app.backup.BackupDataInputStream
|
||||
|
@ -29,6 +30,9 @@ import java.security.NoSuchAlgorithmException
|
|||
|
||||
|
||||
class OpmlBackupAgent : BackupAgentHelper() {
|
||||
val isAutoBackupOPML: Boolean
|
||||
get() = appPrefs.getBoolean(PREF_OPML_BACKUP, true)
|
||||
|
||||
override fun onCreate() {
|
||||
if (isAutoBackupOPML) {
|
||||
Logd(TAG, "Backup enabled in preferences")
|
||||
|
|
|
@ -2,24 +2,16 @@ package ac.mdiq.podcini.preferences
|
|||
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
import ac.mdiq.podcini.storage.model.ProxyConfig
|
||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
|
||||
import ac.mdiq.podcini.storage.model.MediaType
|
||||
import ac.mdiq.podcini.storage.utils.FilesUtils
|
||||
import ac.mdiq.podcini.storage.utils.FilesUtils.createNoMediaFile
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import androidx.annotation.VisibleForTesting
|
||||
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.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Provides access to preferences set by the user in the settings screen. A
|
||||
|
@ -30,7 +22,7 @@ import java.util.*
|
|||
object UserPreferences {
|
||||
private val TAG: String = UserPreferences::class.simpleName ?: "Anonymous"
|
||||
|
||||
private const val PREF_OPML_BACKUP = "prefOPMLBackup"
|
||||
const val PREF_OPML_BACKUP = "prefOPMLBackup"
|
||||
|
||||
// User Interface
|
||||
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_DIRECTION: String = "prefDrawerFeedOrderDir"
|
||||
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"
|
||||
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"
|
||||
private const val PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify"
|
||||
const val PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify"
|
||||
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"
|
||||
private const val PREF_BACK_OPENS_DRAWER: String = "prefBackButtonOpensDrawer"
|
||||
|
||||
private const val PREF_QUEUE_KEEP_SORTED: String = "prefQueueKeepSorted"
|
||||
private const val PREF_QUEUE_KEEP_SORTED_ORDER: String = "prefQueueKeepSortedOrder"
|
||||
private const val PREF_DOWNLOADS_SORTED_ORDER = "prefDownloadSortedOrder"
|
||||
private const val PREF_HISTORY_SORTED_ORDER = "prefHistorySortedOrder"
|
||||
const val PREF_QUEUE_KEEP_SORTED: String = "prefQueueKeepSorted"
|
||||
const val PREF_QUEUE_KEEP_SORTED_ORDER: String = "prefQueueKeepSortedOrder"
|
||||
const val PREF_DOWNLOADS_SORTED_ORDER = "prefDownloadSortedOrder"
|
||||
// private const val PREF_HISTORY_SORTED_ORDER = "prefHistorySortedOrder"
|
||||
|
||||
// Episodes
|
||||
private const val PREF_SORT_ALL_EPISODES: String = "prefEpisodesSort"
|
||||
private const val PREF_FILTER_ALL_EPISODES: String = "prefEpisodesFilter"
|
||||
const val PREF_SORT_ALL_EPISODES: String = "prefEpisodesSort"
|
||||
const val PREF_FILTER_ALL_EPISODES: String = "prefEpisodesFilter"
|
||||
|
||||
// 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_BLUETOOTH_RECONNECT: String = "prefUnpauseOnBluetoothReconnect"
|
||||
private const val PREF_HARDWARE_FORWARD_BUTTON: String = "prefHardwareForwardButton"
|
||||
private const val PREF_HARDWARE_PREVIOUS_BUTTON: String = "prefHardwarePreviousButton"
|
||||
const val PREF_HARDWARE_FORWARD_BUTTON: String = "prefHardwareForwardButton"
|
||||
const val PREF_HARDWARE_PREVIOUS_BUTTON: String = "prefHardwarePreviousButton"
|
||||
const val PREF_FOLLOW_QUEUE: String = "prefFollowQueue"
|
||||
const val PREF_SKIP_KEEPS_EPISODE: String = "prefSkipKeepsEpisode"
|
||||
const val PREF_REMOVDE_FROM_QUEUE_MARKED_PLAYED: String = "prefRemoveFromQueueMarkedPlayed"
|
||||
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_LOCAL = "prefAutoDeleteLocal"
|
||||
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_PAUSE_PLAYBACK_FOR_FOCUS_LOSS: String = "prefPauseForFocusLoss"
|
||||
private const val PREF_TIME_RESPECTS_SPEED = "prefPlaybackTimeRespectsSpeed"
|
||||
|
@ -80,16 +72,16 @@ object UserPreferences {
|
|||
private const val PREF_SPEEDFORWRD_SPEED = "prefSpeedforwardSpeed"
|
||||
|
||||
// Network
|
||||
private const val PREF_ENQUEUE_DOWNLOADED = "prefEnqueueDownloaded"
|
||||
const val PREF_ENQUEUE_DOWNLOADED = "prefEnqueueDownloaded"
|
||||
const val PREF_ENQUEUE_LOCATION: String = "prefEnqueueLocation"
|
||||
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_CACHE_SIZE: String = "prefEpisodeCacheSize"
|
||||
const val PREF_ENABLE_AUTODL: String = "prefEnableAutoDl"
|
||||
const val PREF_ENABLE_AUTODL_ON_BATTERY: String = "prefEnableAutoDownloadOnBattery"
|
||||
const val PREF_ENABLE_AUTODL_WIFI_FILTER: String = "prefEnableAutoDownloadWifiFilter"
|
||||
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_HOST = "prefProxyHost"
|
||||
private const val PREF_PROXY_PORT = "prefProxyPort"
|
||||
|
@ -97,19 +89,19 @@ object UserPreferences {
|
|||
private const val PREF_PROXY_PASSWORD = "prefProxyPassword"
|
||||
|
||||
// Services
|
||||
private const val PREF_GPODNET_NOTIFICATIONS = "pref_gpodnet_notifications"
|
||||
const val PREF_GPODNET_NOTIFICATIONS = "pref_gpodnet_notifications"
|
||||
|
||||
// Other
|
||||
private const val PREF_DATA_FOLDER = "prefDataFolder"
|
||||
// const val PREF_DATA_FOLDER = "prefDataFolder"
|
||||
const val PREF_DELETE_REMOVES_FROM_QUEUE: String = "prefDeleteRemovesFromQueue"
|
||||
|
||||
// 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_PLAYBACK_SKIP_SILENCE: String = "prefSkipSilence"
|
||||
private const val PREF_FAST_FORWARD_SECS = "prefFastForwardSecs"
|
||||
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"
|
||||
|
||||
// Experimental
|
||||
|
@ -141,7 +133,6 @@ object UserPreferences {
|
|||
private lateinit var context: Context
|
||||
lateinit var appPrefs: SharedPreferences
|
||||
|
||||
|
||||
var theme: ThemePreference
|
||||
get() = when (appPrefs.getString(PREF_THEME, "system")) {
|
||||
"0" -> ThemePreference.LIGHT
|
||||
|
@ -190,103 +181,12 @@ object UserPreferences {
|
|||
.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
|
||||
get() = appPrefs.getBoolean(PREF_AUTO_DELETE, false)
|
||||
|
||||
val isAutoDeleteLocal: Boolean
|
||||
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
|
||||
get() {
|
||||
try {
|
||||
|
@ -320,61 +220,6 @@ object UserPreferences {
|
|||
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
|
||||
* negative integer EPISODE_CACHE_SIZE_UNLIMITED if the cache size is set to
|
||||
|
@ -393,9 +238,6 @@ object UserPreferences {
|
|||
val isEnableAutodownloadOnBattery: Boolean
|
||||
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
|
||||
get() {
|
||||
try {
|
||||
|
@ -436,12 +278,6 @@ object UserPreferences {
|
|||
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
|
||||
get() {
|
||||
val type = Proxy.Type.valueOf(appPrefs.getString(PREF_PROXY_TYPE, Proxy.Type.DIRECT.name)!!)
|
||||
|
@ -469,24 +305,6 @@ object UserPreferences {
|
|||
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?
|
||||
get() = appPrefs.getString(PREF_DEFAULT_PAGE, "SubscriptionsFragment")
|
||||
set(defaultPage) {
|
||||
|
@ -499,61 +317,6 @@ object UserPreferences {
|
|||
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.
|
||||
* @throws IllegalArgumentException if context is null
|
||||
|
@ -561,6 +324,7 @@ object UserPreferences {
|
|||
fun init(context: Context) {
|
||||
Logd(TAG, "Creating new instance of UserPreferences")
|
||||
UserPreferences.context = context.applicationContext
|
||||
FilesUtils.context = context.applicationContext
|
||||
appPrefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
createNoMediaFile()
|
||||
|
@ -606,19 +370,6 @@ object UserPreferences {
|
|||
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
|
||||
* 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()
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
fun shouldPauseForFocusLoss(): Boolean {
|
||||
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 {
|
||||
return appPrefs.getBoolean(PREF_BACK_OPENS_DRAWER, false)
|
||||
}
|
||||
|
@ -687,101 +404,7 @@ object UserPreferences {
|
|||
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 {
|
||||
LIGHT, DARK, BLACK, SYSTEM
|
||||
}
|
||||
|
||||
enum class EnqueueLocation {
|
||||
BACK, FRONT, AFTER_CURRENTLY_PLAYING, RANDOM
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,12 +13,13 @@ import androidx.preference.ListPreference
|
|||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.autodownloadSelectedNetworks
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isEnableAutodownloadWifiFilter
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.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.isEnableAutodownloadWifiFilter
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.setAutodownloadSelectedNetworks
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
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() {
|
||||
if (selectedNetworks != null) {
|
||||
val prefScreen = preferenceScreen
|
||||
|
|
|
@ -4,16 +4,15 @@ import ac.mdiq.podcini.R
|
|||
import ac.mdiq.podcini.databinding.ChooseDataFolderDialogBinding
|
||||
import ac.mdiq.podcini.databinding.ChooseDataFolderDialogEntryBinding
|
||||
import ac.mdiq.podcini.databinding.ProxySettingsBinding
|
||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager.restartUpdateAlarm
|
||||
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.reinit
|
||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager.restartUpdateAlarm
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
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.setDataFolder
|
||||
import ac.mdiq.podcini.storage.model.ProxyConfig
|
||||
import ac.mdiq.podcini.storage.utils.FilesUtils.getDataFolder
|
||||
import ac.mdiq.podcini.storage.utils.StorageUtils.getFreeSpaceAvailable
|
||||
import ac.mdiq.podcini.storage.utils.StorageUtils.getTotalSpaceAvailable
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
||||
|
@ -79,7 +78,7 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere
|
|||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
setDataFolderText()
|
||||
// setDataFolderText()
|
||||
}
|
||||
|
||||
private fun setupNetworkScreen() {
|
||||
|
@ -93,13 +92,13 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere
|
|||
dialog.show()
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(PREF_CHOOSE_DATA_DIR)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
ChooseDataFolderDialog.showDialog(requireContext()) { path: String? ->
|
||||
setDataFolder(path!!)
|
||||
setDataFolderText()
|
||||
}
|
||||
true
|
||||
}
|
||||
// findPreference<Preference>(PREF_CHOOSE_DATA_DIR)?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
// ChooseDataFolderDialog.showDialog(requireContext()) { path: String? ->
|
||||
// setDataFolder(path!!)
|
||||
//// setDataFolderText()
|
||||
// }
|
||||
// true
|
||||
// }
|
||||
findPreference<Preference>(PREF_AUTO_DELETE_LOCAL)!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
|
||||
if (blockAutoDeleteLocal && newValue as Boolean) {
|
||||
showAutoDeleteEnableDialog()
|
||||
|
@ -108,10 +107,10 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere
|
|||
}
|
||||
}
|
||||
|
||||
private fun setDataFolderText() {
|
||||
val f = getDataFolder(null)
|
||||
if (f != null) findPreference<Preference>(PREF_CHOOSE_DATA_DIR)!!.summary = f.absolutePath
|
||||
}
|
||||
// private fun setDataFolderText() {
|
||||
// val f = getDataFolder(null)
|
||||
// if (f != null) findPreference<Preference>(PREF_CHOOSE_DATA_DIR)!!.summary = f.absolutePath
|
||||
// }
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
||||
if (UserPreferences.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_AUTO_DELETE_LOCAL = "prefAutoDeleteLocal"
|
||||
private const val PREF_PROXY = "prefProxy"
|
||||
private const val PREF_CHOOSE_DATA_DIR = "prefChooseDataDir"
|
||||
// private const val PREF_CHOOSE_DATA_DIR = "prefChooseDataDir"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.preferences.ExportWriter
|
||||
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.getEpisodes
|
||||
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.utils.EpisodeUtil.hasAlmostEnded
|
||||
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.PreferenceActivity
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
|
@ -102,10 +102,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
result: ActivityResult -> this.restoreMediaFilesResult(result) }
|
||||
|
||||
private val backupMediaFilesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
val data: Uri? = it.data?.data
|
||||
if (data != null) MediaFilesTransporter.exportToDocument(data, requireContext())
|
||||
}
|
||||
result: ActivityResult -> this.exportMediaFilesResult(result)
|
||||
}
|
||||
|
||||
private var progressDialog: ProgressDialog? = null
|
||||
|
@ -194,7 +191,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
showExportSuccessSnackbar(fileUri, exportType.contentType)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
showExportErrorDialog(e)
|
||||
showTransportErrorDialog(e)
|
||||
} finally {
|
||||
progressDialog!!.dismiss()
|
||||
}
|
||||
|
@ -208,7 +205,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
showExportSuccessSnackbar(output.uri, exportType.contentType)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
showExportErrorDialog(e)
|
||||
showTransportErrorDialog(e)
|
||||
} finally {
|
||||
progressDialog!!.dismiss()
|
||||
}
|
||||
|
@ -290,7 +287,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
builder.show()
|
||||
}
|
||||
|
||||
private fun showDatabaseImportSuccessDialog() {
|
||||
private fun showImportSuccessDialog() {
|
||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||
builder.setTitle(R.string.successful_import_label)
|
||||
builder.setMessage(R.string.import_ok)
|
||||
|
@ -305,11 +302,11 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
.show()
|
||||
}
|
||||
|
||||
private fun showExportErrorDialog(error: Throwable) {
|
||||
private fun showTransportErrorDialog(error: Throwable) {
|
||||
progressDialog!!.dismiss()
|
||||
val alert = MaterialAlertDialogBuilder(requireContext())
|
||||
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.show()
|
||||
}
|
||||
|
@ -371,17 +368,17 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
reader.close()
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
showDatabaseImportSuccessDialog()
|
||||
showImportSuccessDialog()
|
||||
progressDialog!!.dismiss()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
showExportErrorDialog(e)
|
||||
showTransportErrorDialog(e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val context = requireContext()
|
||||
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())
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
showDatabaseImportSuccessDialog()
|
||||
showImportSuccessDialog()
|
||||
progressDialog!!.dismiss()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
showExportErrorDialog(e)
|
||||
showTransportErrorDialog(e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val context = requireContext()
|
||||
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())
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
showDatabaseImportSuccessDialog()
|
||||
showImportSuccessDialog()
|
||||
progressDialog!!.dismiss()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
showExportErrorDialog(e)
|
||||
showTransportErrorDialog(e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val context = requireContext()
|
||||
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())
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
showDatabaseImportSuccessDialog()
|
||||
showImportSuccessDialog()
|
||||
progressDialog!!.dismiss()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
showExportErrorDialog(e)
|
||||
showTransportErrorDialog(e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val context = requireContext()
|
||||
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?) {
|
||||
if (uri == null) return
|
||||
|
@ -497,7 +512,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
progressDialog!!.dismiss()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
showExportErrorDialog(e)
|
||||
showTransportErrorDialog(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package ac.mdiq.podcini.storage.algorithms
|
||||
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.EPISODE_CLEANUP_NULL
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_EPISODE_CLEANUP
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.episodeCacheSize
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.episodeCleanupValue
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
|
||||
import ac.mdiq.podcini.storage.database.Episodes.deleteMediaOfEpisode
|
||||
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
|
||||
|
@ -22,6 +24,12 @@ import java.util.concurrent.ExecutionException
|
|||
|
||||
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
|
||||
* 'playbackCompletionDate'-value will be deleted first.
|
||||
|
|
|
@ -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.writeNoMediaPlaying
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.ACTION_SHUTDOWN_PLAYBACK_SERVICE
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_REMOVDE_FROM_QUEUE_MARKED_PLAYED
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
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.removeFromQueueSync
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
|
@ -84,12 +85,6 @@ object Episodes {
|
|||
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? {
|
||||
Logd(TAG, "getEpisodeMedia called $mediaId")
|
||||
val media = realm.query(EpisodeMedia::class).query("id == $0", mediaId).first().find()
|
||||
|
@ -290,4 +285,8 @@ object Episodes {
|
|||
EventFlow.postEvent(FlowEvent.EpisodePlayedEvent(result))
|
||||
return result
|
||||
}
|
||||
|
||||
private fun shouldRemoveFromQueuesMarkPlayed(): Boolean {
|
||||
return appPrefs.getBoolean(PREF_REMOVDE_FROM_QUEUE_MARKED_PLAYED, true)
|
||||
}
|
||||
}
|
|
@ -3,21 +3,18 @@ package ac.mdiq.podcini.storage.database
|
|||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.EnqueueLocation
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.enqueueLocation
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isQueueKeepSorted
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.queueKeepSortedOrder
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_ENQUEUE_LOCATION
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_QUEUE_KEEP_SORTED
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_QUEUE_KEEP_SORTED_ORDER
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_QUEUE_LOCKED
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia
|
||||
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsert
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
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.storage.model.*
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.event.EventFlow
|
||||
import ac.mdiq.podcini.util.event.FlowEvent
|
||||
|
@ -32,6 +29,10 @@ import java.util.*
|
|||
object Queues {
|
||||
private val TAG: String = Queues::class.simpleName ?: "Anonymous"
|
||||
|
||||
enum class EnqueueLocation {
|
||||
BACK, FRONT, AFTER_CURRENTLY_PLAYING, RANDOM
|
||||
}
|
||||
|
||||
fun getInQueueEpisodeIds(): Set<Long> {
|
||||
Logd(TAG, "getQueueIDList() called")
|
||||
val queues = realm.query(PlayQueue::class).find()
|
||||
|
@ -273,6 +274,60 @@ object Queues {
|
|||
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) {
|
||||
/**
|
||||
* Determine the position (0-based) that the item(s) should be inserted to the named queue.
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
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.Playable
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
|
||||
object EpisodeUtil {
|
||||
private val TAG: String = EpisodeUtil::class.simpleName ?: "Anonymous"
|
||||
val smartMarkAsPlayedSecs: Int
|
||||
get() = appPrefs.getString(PREF_SMART_MARK_AS_PLAYED_SECS, "30")!!.toInt()
|
||||
|
||||
@JvmStatic
|
||||
fun indexOfItemWithId(episodes: List<Episode?>, id: Long): Int {
|
||||
|
@ -43,7 +45,6 @@ object EpisodeUtil {
|
|||
|
||||
@JvmStatic
|
||||
fun hasAlmostEnded(media: Playable): Boolean {
|
||||
val smartMarkAsPlayedSecs = UserPreferences.smartMarkAsPlayedSecs
|
||||
return media.getDuration() > 0 && media.getPosition() >= media.getDuration() - smartMarkAsPlayedSecs * 1000
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,10 +13,7 @@ object FileNameGenerator {
|
|||
const val MAX_FILENAME_LENGTH: Int = 242 // limited by CircleCI
|
||||
private const val MD5_HEX_LENGTH = 32
|
||||
|
||||
private val validChars = ("abcdefghijklmnopqrstuvwxyz"
|
||||
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
+ "0123456789"
|
||||
+ " _-").toCharArray()
|
||||
private val validChars = ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 _-").toCharArray()
|
||||
|
||||
/**
|
||||
* This method will return a new string that doesn't contain any illegal
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,17 +4,25 @@ import ac.mdiq.podcini.storage.model.Episode
|
|||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.storage.model.Playable
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_USE_EPISODE_COVER
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
|
||||
/**
|
||||
* Utility class to use the appropriate image resource based on [UserPreferences].
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getEpisodeListImageLocation(playable: Playable): String? {
|
||||
return if (UserPreferences.useEpisodeCoverSetting) playable.getImageLocation()
|
||||
return if (useEpisodeCoverSetting) playable.getImageLocation()
|
||||
else getFallbackImageLocation(playable)
|
||||
}
|
||||
|
||||
|
@ -23,7 +31,7 @@ object ImageResourceUtils {
|
|||
*/
|
||||
@JvmStatic
|
||||
fun getEpisodeListImageLocation(episode: Episode): String? {
|
||||
return if (UserPreferences.useEpisodeCoverSetting) episode.imageLocation
|
||||
return if (useEpisodeCoverSetting) episode.imageLocation
|
||||
else getFallbackImageLocation(episode)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package ac.mdiq.podcini.storage.utils
|
|||
|
||||
import android.os.StatFs
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.storage.utils.FilesUtils.getDataFolder
|
||||
|
||||
/**
|
||||
* Utility functions for handling storage errors
|
||||
|
@ -13,7 +14,7 @@ object StorageUtils {
|
|||
@JvmStatic
|
||||
val freeSpaceAvailable: Long
|
||||
get() {
|
||||
val dataFolder = UserPreferences.getDataFolder(null)
|
||||
val dataFolder = getDataFolder(null)
|
||||
return if (dataFolder != null) getFreeSpaceAvailable(dataFolder.absolutePath) else 0
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,12 @@ import android.view.View
|
|||
import androidx.media3.common.util.UnstableApi
|
||||
|
||||
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 {
|
||||
return R.string.delete_label
|
||||
}
|
||||
|
@ -17,10 +23,4 @@ class DeleteActionButton(item: Episode) : EpisodeActionButton(item) {
|
|||
@UnstableApi override fun onClick(context: Context) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,14 +15,16 @@ import ac.mdiq.podcini.storage.model.EpisodeMedia
|
|||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
|
||||
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 {
|
||||
return R.string.download_label
|
||||
}
|
||||
|
||||
override fun getDrawable(): Int {
|
||||
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) {
|
||||
val media = item.media
|
||||
|
|
|
@ -13,17 +13,17 @@ import androidx.media3.common.util.UnstableApi
|
|||
abstract class EpisodeActionButton internal constructor(@JvmField var item: Episode) {
|
||||
val TAG = this::class.simpleName ?: "ItemActionButton"
|
||||
|
||||
open val visibility: Int
|
||||
get() = View.VISIBLE
|
||||
|
||||
var processing: Float = -1f
|
||||
|
||||
abstract fun getLabel(): Int
|
||||
|
||||
abstract fun getDrawable(): Int
|
||||
|
||||
abstract fun onClick(context: Context)
|
||||
|
||||
open val visibility: Int
|
||||
get() = View.VISIBLE
|
||||
|
||||
var processing: Float = -1f
|
||||
|
||||
fun configure(button: View, icon: ImageView, context: Context) {
|
||||
button.visibility = visibility
|
||||
button.contentDescription = context.getString(getLabel())
|
||||
|
|
|
@ -8,16 +8,19 @@ import ac.mdiq.podcini.storage.model.Episode
|
|||
import androidx.media3.common.util.UnstableApi
|
||||
|
||||
class MarkAsPlayedActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
override val visibility: Int
|
||||
get() = if (item.isPlayed()) View.INVISIBLE else View.VISIBLE
|
||||
|
||||
override fun getLabel(): Int {
|
||||
return (if (item.media != null) R.string.mark_read_label else R.string.mark_read_no_media_label)
|
||||
}
|
||||
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_check
|
||||
}
|
||||
|
||||
@UnstableApi override fun onClick(context: Context) {
|
||||
if (!item.isPlayed()) setPlayState(Episode.PLAYED, true, item)
|
||||
}
|
||||
|
||||
override val visibility: Int
|
||||
get() = if (item.isPlayed()) View.INVISIBLE else View.VISIBLE
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileStreaming
|
||||
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
||||
import ac.mdiq.podcini.preferences.UsageStatistics
|
||||
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.MediaType
|
||||
import ac.mdiq.podcini.storage.model.Playable
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.getMediafilePath
|
||||
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.getMediafilename
|
||||
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.storage.model.Episode
|
||||
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.ttsReady
|
||||
import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment.Companion.ttsWorking
|
||||
import ac.mdiq.podcini.util.AudioMediaOperation.mergeAudios
|
||||
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.FlowEvent
|
||||
import android.content.Context
|
||||
|
@ -25,7 +25,10 @@ import android.widget.Toast
|
|||
import androidx.annotation.OptIn
|
||||
import androidx.core.text.HtmlCompat
|
||||
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 java.io.File
|
||||
import java.util.*
|
||||
|
@ -35,7 +38,9 @@ import kotlin.math.min
|
|||
class TTSActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
|
||||
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 {
|
||||
return R.string.TTS_label
|
||||
|
@ -151,7 +156,4 @@ class TTSActionButton(item: Episode) : EpisodeActionButton(item) {
|
|||
EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item))
|
||||
}
|
||||
}
|
||||
|
||||
override val visibility: Int
|
||||
get() = if (item.link.isNullOrEmpty()) View.INVISIBLE else View.VISIBLE
|
||||
}
|
||||
|
|
|
@ -7,16 +7,18 @@ import ac.mdiq.podcini.util.IntentUtils.openInBrowser
|
|||
import ac.mdiq.podcini.storage.model.Episode
|
||||
|
||||
class VisitWebsiteActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
override val visibility: Int
|
||||
get() = if (item.link.isNullOrEmpty()) View.INVISIBLE else View.VISIBLE
|
||||
|
||||
override fun getLabel(): Int {
|
||||
return R.string.visit_website_label
|
||||
}
|
||||
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_web
|
||||
}
|
||||
|
||||
override fun onClick(context: Context) {
|
||||
if (!item.link.isNullOrEmpty()) openInBrowser(context, item.link!!)
|
||||
}
|
||||
|
||||
override val visibility: Int
|
||||
get() = if (item.link.isNullOrEmpty()) View.INVISIBLE else View.VISIBLE
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package ac.mdiq.podcini.ui.activity
|
|||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.BugReportBinding
|
||||
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.Logd
|
||||
import ac.mdiq.podcini.util.error.CrashReportWriter
|
||||
|
|
|
@ -4,13 +4,13 @@ import ac.mdiq.podcini.R
|
|||
import ac.mdiq.podcini.databinding.SortDialogBinding
|
||||
import ac.mdiq.podcini.databinding.SortDialogItemActiveBinding
|
||||
import ac.mdiq.podcini.databinding.SortDialogItemBinding
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.feedOrderBy
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.feedOrderDir
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.setFeedOrder
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_DRAWER_FEED_ORDER
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_DRAWER_FEED_ORDER_DIRECTION
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.storage.model.FeedSortOrder
|
||||
import ac.mdiq.podcini.storage.model.FeedSortOrder.Companion.getSortOrder
|
||||
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment
|
||||
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion
|
||||
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion.feedOrderBy
|
||||
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion.feedOrderDir
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.event.EventFlow
|
||||
import ac.mdiq.podcini.util.event.FlowEvent
|
||||
|
@ -134,4 +134,13 @@ open class FeedSortDialogNew : BottomSheetDialogFragment() {
|
|||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.service.PlaybackService.Companion.currentMediaType
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_PLAYBACK_SPEED_ARRAY
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
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.storage.database.Feeds.persistFeedPreferences
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
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.view.PlaybackSpeedSeekBar
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
|
@ -42,6 +41,9 @@ import com.google.android.material.snackbar.Snackbar
|
|||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
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>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val chip = Chip(context)
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package ac.mdiq.podcini.ui.fragment
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.allEpisodesSortOrder
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.prefFilterAllEpisodes
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_FILTER_ALL_EPISODES
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_SORT_ALL_EPISODES
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
|
||||
import ac.mdiq.podcini.storage.database.Episodes.getEpisodesCount
|
||||
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.EpisodeSortDialog
|
||||
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.event.EventFlow
|
||||
import ac.mdiq.podcini.util.event.FlowEvent
|
||||
|
@ -35,7 +35,7 @@ import kotlin.math.min
|
|||
*/
|
||||
@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 {
|
||||
val root = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
@ -181,8 +181,19 @@ import kotlin.math.min
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = AllEpisodesFragment::class.simpleName ?: "Anonymous"
|
||||
const val PREF_NAME: String = "PrefAllEpisodesFragment"
|
||||
var allEpisodesSortOrder: EpisodeSortOrder?
|
||||
get() = EpisodeSortOrder.fromCodeString(appPrefs.getString(PREF_SORT_ALL_EPISODES, "" + EpisodeSortOrder.DATE_NEW_OLD.code))
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,14 +6,15 @@ import ac.mdiq.podcini.databinding.SimpleListFragmentBinding
|
|||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.isCurMedia
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
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.RealmDB.realm
|
||||
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.utils.EpisodeUtil
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||
import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectHandler
|
||||
import ac.mdiq.podcini.ui.actions.actionbutton.DeleteActionButton
|
||||
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.dialog.EpisodeSortDialog
|
||||
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.LiftOnScrollListener
|
||||
import ac.mdiq.podcini.ui.view.EpisodesRecyclerView
|
||||
|
@ -327,7 +327,7 @@ import java.util.*
|
|||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
val sortOrder: EpisodeSortOrder? = UserPreferences.downloadsSortedOrder
|
||||
val sortOrder: EpisodeSortOrder? = downloadsSortedOrder
|
||||
val downloadedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.DOWNLOADED), sortOrder)
|
||||
if (runningDownloads.isEmpty()) episodes = downloadedItems.toMutableList()
|
||||
else {
|
||||
|
@ -412,7 +412,7 @@ import java.util.*
|
|||
class DownloadsSortDialog : EpisodeSortDialog() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
sortOrder = UserPreferences.downloadsSortedOrder
|
||||
sortOrder = downloadsSortedOrder
|
||||
}
|
||||
|
||||
override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) {
|
||||
|
@ -426,7 +426,7 @@ import java.util.*
|
|||
|
||||
override fun onSelectionChanged() {
|
||||
super.onSelectionChanged()
|
||||
UserPreferences.downloadsSortedOrder = sortOrder
|
||||
downloadsSortedOrder = sortOrder
|
||||
EventFlow.postEvent(FlowEvent.DownloadLogEvent())
|
||||
}
|
||||
}
|
||||
|
@ -436,5 +436,15 @@ import java.util.*
|
|||
|
||||
const val ARG_SHOW_LOGS: String = "show_logs"
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.preferences.UserPreferences
|
||||
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.moveInQueueSync
|
||||
import ac.mdiq.podcini.storage.database.Queues.queueKeepSortedOrder
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsert
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
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.utils.EpisodeUtil
|
||||
import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectHandler
|
||||
import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler
|
||||
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.EpisodeSortDialog
|
||||
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.LiftOnScrollListener
|
||||
import ac.mdiq.podcini.ui.view.EpisodesRecyclerView
|
||||
|
@ -393,8 +395,8 @@ import java.util.*
|
|||
}
|
||||
|
||||
private fun refreshToolbarState() {
|
||||
val keepSorted: Boolean = UserPreferences.isQueueKeepSorted
|
||||
toolbar.menu?.findItem(R.id.queue_lock)?.setChecked(UserPreferences.isQueueLocked)
|
||||
val keepSorted: Boolean = isQueueKeepSorted
|
||||
toolbar.menu?.findItem(R.id.queue_lock)?.setChecked(isQueueLocked)
|
||||
toolbar.menu?.findItem(R.id.queue_lock)?.setVisible(!keepSorted)
|
||||
}
|
||||
|
||||
|
@ -422,7 +424,7 @@ import java.util.*
|
|||
}
|
||||
|
||||
@UnstableApi private fun toggleQueueLock() {
|
||||
val isLocked: Boolean = UserPreferences.isQueueLocked
|
||||
val isLocked: Boolean = isQueueLocked
|
||||
if (isLocked) setQueueLocked(false)
|
||||
else {
|
||||
val shouldShowLockWarning: Boolean = prefs!!.getBoolean(PREF_SHOW_LOCK_WARNING, true)
|
||||
|
@ -448,7 +450,7 @@ import java.util.*
|
|||
}
|
||||
|
||||
@UnstableApi private fun setQueueLocked(locked: Boolean) {
|
||||
UserPreferences.isQueueLocked = locked
|
||||
isQueueLocked = locked
|
||||
refreshToolbarState()
|
||||
adapter?.updateDragDropEnabled()
|
||||
|
||||
|
@ -568,12 +570,12 @@ import java.util.*
|
|||
|
||||
class QueueSortDialog : EpisodeSortDialog() {
|
||||
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)!!
|
||||
binding.keepSortedCheckbox.visibility = View.VISIBLE
|
||||
binding.keepSortedCheckbox.setChecked(UserPreferences.isQueueKeepSorted)
|
||||
binding.keepSortedCheckbox.setChecked(isQueueKeepSorted)
|
||||
// Disable until something gets selected
|
||||
binding.keepSortedCheckbox.setEnabled(UserPreferences.isQueueKeepSorted)
|
||||
binding.keepSortedCheckbox.setEnabled(isQueueKeepSorted)
|
||||
return view
|
||||
}
|
||||
override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) {
|
||||
|
@ -584,8 +586,8 @@ import java.util.*
|
|||
super.onSelectionChanged()
|
||||
binding.keepSortedCheckbox.setEnabled(sortOrder != EpisodeSortOrder.RANDOM)
|
||||
if (sortOrder == EpisodeSortOrder.RANDOM) binding.keepSortedCheckbox.setChecked(false)
|
||||
UserPreferences.isQueueKeepSorted = binding.keepSortedCheckbox.isChecked
|
||||
UserPreferences.queueKeepSortedOrder = sortOrder
|
||||
isQueueKeepSorted = binding.keepSortedCheckbox.isChecked
|
||||
queueKeepSortedOrder = sortOrder
|
||||
reorderQueue(sortOrder, true)
|
||||
}
|
||||
/**
|
||||
|
@ -659,10 +661,10 @@ import java.util.*
|
|||
private var dragDropEnabled: Boolean
|
||||
|
||||
init {
|
||||
dragDropEnabled = !(UserPreferences.isQueueKeepSorted || UserPreferences.isQueueLocked)
|
||||
dragDropEnabled = !(isQueueKeepSorted || isQueueLocked)
|
||||
}
|
||||
fun updateDragDropEnabled() {
|
||||
dragDropEnabled = !(UserPreferences.isQueueKeepSorted || UserPreferences.isQueueLocked)
|
||||
dragDropEnabled = !(isQueueKeepSorted || isQueueLocked)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
@UnstableApi
|
||||
|
@ -701,7 +703,7 @@ import java.util.*
|
|||
|
||||
if (!inActionMode()) {
|
||||
// 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(itemCount - 1)?.id === longPressedItem?.id || keepSorted) menu.findItem(R.id.move_to_bottom_item).setVisible(false)
|
||||
} else {
|
||||
|
|
|
@ -19,12 +19,10 @@ import ac.mdiq.podcini.ui.actions.menuhandler.MenuItemUtils
|
|||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.adapter.EpisodesAdapter
|
||||
import ac.mdiq.podcini.ui.adapter.SelectableAdapter
|
||||
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion
|
||||
import ac.mdiq.podcini.ui.utils.EmptyViewHandler
|
||||
import ac.mdiq.podcini.ui.utils.LiftOnScrollListener
|
||||
import ac.mdiq.podcini.ui.view.EpisodesRecyclerView
|
||||
import ac.mdiq.podcini.ui.view.SquareImageView
|
||||
import ac.mdiq.podcini.ui.view.viewholder.EpisodeViewHolder
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.event.EventFlow
|
||||
import ac.mdiq.podcini.util.event.FlowEvent
|
||||
|
@ -74,7 +72,7 @@ import java.lang.ref.WeakReference
|
|||
private lateinit var emptyViewHandler: EmptyViewHandler
|
||||
private lateinit var recyclerView: EpisodesRecyclerView
|
||||
private lateinit var searchView: SearchView
|
||||
private lateinit var speedDialBinding: MultiSelectSpeedDialBinding
|
||||
private lateinit var sdBinding: MultiSelectSpeedDialBinding
|
||||
private lateinit var chip: Chip
|
||||
private lateinit var automaticSearchDebouncer: Handler
|
||||
|
||||
|
@ -95,7 +93,7 @@ import java.lang.ref.WeakReference
|
|||
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
setupToolbar(binding.toolbar)
|
||||
speedDialBinding = MultiSelectSpeedDialBinding.bind(binding.root)
|
||||
sdBinding = MultiSelectSpeedDialBinding.bind(binding.root)
|
||||
progressBar = binding.progressBar
|
||||
recyclerView = binding.recyclerView
|
||||
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?) {
|
||||
super.onCreateContextMenu(menu, v, menuInfo)
|
||||
if (!inActionMode()) menu.findItem(R.id.multi_select).setVisible(true)
|
||||
|
||||
MenuItemUtils.setOnClickListeners(menu) { item: MenuItem -> this@SearchFragment.onContextItemSelected(item) }
|
||||
}
|
||||
}
|
||||
|
@ -151,21 +148,20 @@ import java.lang.ref.WeakReference
|
|||
}
|
||||
}
|
||||
})
|
||||
speedDialBinding.fabSD.overlayLayout = speedDialBinding.fabSDOverlay
|
||||
speedDialBinding.fabSD.inflate(R.menu.episodes_apply_action_speeddial)
|
||||
speedDialBinding.fabSD.setOnChangeListener(object : SpeedDialView.OnChangeListener {
|
||||
sdBinding.fabSD.overlayLayout = sdBinding.fabSDOverlay
|
||||
sdBinding.fabSD.inflate(R.menu.episodes_apply_action_speeddial)
|
||||
sdBinding.fabSD.setOnChangeListener(object : SpeedDialView.OnChangeListener {
|
||||
override fun onMainActionSelected(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onToggleChanged(open: Boolean) {
|
||||
if (open && adapter.selectedCount == 0) {
|
||||
(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)
|
||||
.handleAction(adapter.selectedItems.filterIsInstance<Episode>())
|
||||
adapter.endSelectMode()
|
||||
|
@ -210,7 +206,6 @@ import java.lang.ref.WeakReference
|
|||
searchWithProgressBar()
|
||||
return true
|
||||
}
|
||||
|
||||
@UnstableApi override fun onQueryTextChange(s: String): Boolean {
|
||||
automaticSearchDebouncer.removeCallbacksAndMessages(null)
|
||||
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 {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
parentFragmentManager.popBackStack()
|
||||
return true
|
||||
|
@ -337,7 +331,6 @@ import java.lang.ref.WeakReference
|
|||
if (requireArguments().getLong(ARG_FEED, 0) == 0L) {
|
||||
if (results.second != null) adapterFeeds.updateData(results.second!!)
|
||||
} else adapterFeeds.updateData(emptyList())
|
||||
|
||||
if (searchView.query.toString().isEmpty()) emptyViewHandler.setMessage(R.string.type_to_search)
|
||||
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()
|
||||
if (query.isEmpty()) return Pair<List<Episode>, List<Feed>>(emptyList(), emptyList())
|
||||
|
||||
val feed = requireArguments().getLong(ARG_FEED)
|
||||
val items: List<Episode> = searchEpisodes(feed, query)
|
||||
val feedID = requireArguments().getLong(ARG_FEED)
|
||||
val items: List<Episode> = searchEpisodes(feedID, query)
|
||||
val feeds: List<Feed> = searchFeeds(query)
|
||||
Logd(TAG, "performSearch items: ${items.size} feeds: ${feeds.size}")
|
||||
return Pair<List<Episode>, List<Feed>>(items, feeds)
|
||||
}
|
||||
|
||||
|
@ -362,23 +356,23 @@ import java.lang.ref.WeakReference
|
|||
val sb = StringBuilder()
|
||||
for (i in queryWords.indices) {
|
||||
sb.append("(")
|
||||
.append("feedTitle TEXT ${queryWords[i]}")
|
||||
.append("eigenTitle TEXT '${queryWords[i]}'")
|
||||
.append(" OR ")
|
||||
.append("customTitle TEXT ${queryWords[i]}")
|
||||
.append("customTitle TEXT '${queryWords[i]}'")
|
||||
.append(" OR ")
|
||||
.append("author TEXT ${queryWords[i]}")
|
||||
.append("author TEXT '${queryWords[i]}'")
|
||||
.append(" OR ")
|
||||
.append("description TEXT ${queryWords[i]}")
|
||||
.append("description TEXT '${queryWords[i]}'")
|
||||
.append(") ")
|
||||
if (i != queryWords.size - 1) sb.append("AND ")
|
||||
}
|
||||
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun searchFeeds(query: String): List<Feed> {
|
||||
Logd(TAG, "searchFeeds called")
|
||||
val queryString = prepareFeedQueryString(query)
|
||||
Logd(TAG, "searchFeeds queryString: $queryString")
|
||||
return realm.query(Feed::class).query(queryString).find()
|
||||
}
|
||||
|
||||
|
@ -387,9 +381,9 @@ import java.lang.ref.WeakReference
|
|||
val sb = StringBuilder()
|
||||
for (i in queryWords.indices) {
|
||||
sb.append("(")
|
||||
.append("description TEXT ${queryWords[i]}")
|
||||
.append("description TEXT '${queryWords[i]}'")
|
||||
.append(" OR ")
|
||||
.append("title TEXT ${queryWords[i]}" )
|
||||
.append("title TEXT '${queryWords[i]}'" )
|
||||
.append(") ")
|
||||
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> {
|
||||
Logd(TAG, "searchEpisodes called")
|
||||
val queryString = prepareEpisodeQueryString(query)
|
||||
val idString = if (feedID != 0L) "(id = $feedID)" else ""
|
||||
return realm.query(Episode::class).query("$idString AND $queryString").find()
|
||||
var queryString = prepareEpisodeQueryString(query)
|
||||
if (feedID != 0L) queryString = "(feedId == $feedID) AND $queryString"
|
||||
Logd(TAG, "searchEpisodes queryString: $queryString")
|
||||
return realm.query(Episode::class).query(queryString).find()
|
||||
}
|
||||
|
||||
private fun showInputMethod(view: View) {
|
||||
|
@ -431,14 +426,14 @@ import java.lang.ref.WeakReference
|
|||
override fun onStartSelectMode() {
|
||||
searchViewFocusOff()
|
||||
// speedDialBinding.fabSD.removeActionItemById(R.id.remove_from_inbox_batch)
|
||||
speedDialBinding.fabSD.removeActionItemById(R.id.remove_from_queue_batch)
|
||||
speedDialBinding.fabSD.removeActionItemById(R.id.delete_batch)
|
||||
speedDialBinding.fabSD.visibility = View.VISIBLE
|
||||
sdBinding.fabSD.removeActionItemById(R.id.remove_from_queue_batch)
|
||||
sdBinding.fabSD.removeActionItemById(R.id.delete_batch)
|
||||
sdBinding.fabSD.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
override fun onEndSelectMode() {
|
||||
speedDialBinding.fabSD.close()
|
||||
speedDialBinding.fabSD.visibility = View.GONE
|
||||
sdBinding.fabSD.close()
|
||||
sdBinding.fabSD.visibility = View.GONE
|
||||
searchViewFocusOn()
|
||||
}
|
||||
|
||||
|
@ -452,14 +447,13 @@ import java.lang.ref.WeakReference
|
|||
searchView.requestFocus()
|
||||
}
|
||||
|
||||
open class HorizontalFeedListAdapter(mainActivity: MainActivity) :
|
||||
RecyclerView.Adapter<HorizontalFeedListAdapter.Holder>(), View.OnCreateContextMenuListener {
|
||||
open class HorizontalFeedListAdapter(mainActivity: MainActivity)
|
||||
: RecyclerView.Adapter<HorizontalFeedListAdapter.Holder>(), View.OnCreateContextMenuListener {
|
||||
|
||||
private val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity)
|
||||
private val data: MutableList<Feed> = ArrayList()
|
||||
private var dummyViews = 0
|
||||
var longPressedItem: Feed? = null
|
||||
|
||||
@StringRes
|
||||
private var endButtonText = 0
|
||||
private var endButtonAction: Runnable? = null
|
||||
|
@ -467,18 +461,15 @@ import java.lang.ref.WeakReference
|
|||
fun setDummyViews(dummyViews: Int) {
|
||||
this.dummyViews = dummyViews
|
||||
}
|
||||
|
||||
fun updateData(newData: List<Feed>?) {
|
||||
data.clear()
|
||||
data.addAll(newData!!)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
|
||||
val convertView = View.inflate(mainActivityRef.get(), R.layout.horizontal_feed_item, null)
|
||||
return Holder(convertView)
|
||||
}
|
||||
|
||||
@UnstableApi override fun onBindViewHolder(holder: Holder, position: Int) {
|
||||
if (position == itemCount - 1 && endButtonAction != null) {
|
||||
holder.cardView.visibility = View.GONE
|
||||
|
@ -497,49 +488,41 @@ import java.lang.ref.WeakReference
|
|||
holder.imageView.setImageResource(R.color.medium_gray)
|
||||
return
|
||||
}
|
||||
|
||||
holder.itemView.alpha = 1.0f
|
||||
val podcast: Feed = data[position]
|
||||
holder.imageView.setContentDescription(podcast.title)
|
||||
holder.imageView.setOnClickListener {
|
||||
mainActivityRef.get()?.loadChildFragment(FeedEpisodesFragment.newInstance(podcast.id))
|
||||
}
|
||||
|
||||
holder.imageView.setOnCreateContextMenuListener(this)
|
||||
holder.imageView.setOnLongClickListener {
|
||||
val currentItemPosition = holder.bindingAdapterPosition
|
||||
longPressedItem = data[currentItemPosition]
|
||||
false
|
||||
}
|
||||
|
||||
holder.imageView.load(podcast.imageUrl) {
|
||||
placeholder(R.color.light_gray)
|
||||
error(R.mipmap.ic_launcher)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
if (position >= data.size) return RecyclerView.NO_ID // Dummy views
|
||||
return data[position].id
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return dummyViews + data.size + (if ((endButtonAction == null)) 0 else 1)
|
||||
}
|
||||
|
||||
override fun onCreateContextMenu(contextMenu: ContextMenu, view: View, contextMenuInfo: ContextMenu.ContextMenuInfo?) {
|
||||
val inflater: MenuInflater = mainActivityRef.get()!!.menuInflater
|
||||
if (longPressedItem == null) return
|
||||
inflater.inflate(R.menu.nav_feed_context, contextMenu)
|
||||
contextMenu.setHeaderTitle(longPressedItem!!.title)
|
||||
}
|
||||
|
||||
fun setEndButton(@StringRes text: Int, action: Runnable?) {
|
||||
endButtonAction = action
|
||||
endButtonText = text
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val binding = HorizontalFeedItemBinding.bind(itemView)
|
||||
var imageView: SquareImageView = binding.discoveryCover
|
||||
|
@ -584,9 +567,9 @@ import java.lang.ref.WeakReference
|
|||
/**
|
||||
* 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()
|
||||
fragment.requireArguments().putLong(ARG_FEED, feed)
|
||||
fragment.requireArguments().putLong(ARG_FEED, feedId)
|
||||
fragment.requireArguments().putString(ARG_FEED_NAME, feedTitle)
|
||||
return fragment
|
||||
}
|
||||
|
|
|
@ -4,9 +4,11 @@ import ac.mdiq.podcini.R
|
|||
import ac.mdiq.podcini.databinding.*
|
||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.feedOrderBy
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.feedOrderDir
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.useGridLayout
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.FEED_ORDER_UNPLAYED
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_DRAWER_FEED_ORDER
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_DRAWER_FEED_ORDER_DIRECTION
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_FEED_GRID_LAYOUT
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getTags
|
||||
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 var useGrid: Boolean? = null
|
||||
val useGridLayout: Boolean
|
||||
get() = appPrefs.getBoolean(PREF_FEED_GRID_LAYOUT, false)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -878,6 +882,18 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
|||
|
||||
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 {
|
||||
val fragment = SubscriptionsFragment()
|
||||
val args = Bundle()
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
package ac.mdiq.podcini.ui.utils
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_GPODNET_NOTIFICATIONS
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.PREF_SHOW_DOWNLOAD_REPORT
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationChannelGroupCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.gpodnetNotificationsEnabledRaw
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.showDownloadReportRaw
|
||||
|
||||
object NotificationUtils {
|
||||
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_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) {
|
||||
val mNotificationManager = NotificationManagerCompat.from(context)
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package ac.mdiq.podcini.util.error
|
||||
|
||||
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.util.Log
|
||||
import org.apache.commons.io.IOUtils
|
||||
|
|
|
@ -578,7 +578,7 @@
|
|||
<string name="database_import_label">استيراد قاعدة البيانات</string>
|
||||
<string name="database_import_warning">استيراد قاعدة بيانات سيمسح كل اشتراكاتك الحالية وسجل الاستماع. الأفضل أن تصدر قاعدة البيانات الحالية لأرشيف. هل تريد تبديل البيانات؟</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="opml_import_ask_read_permission">الوصول الى مساحة التخزين الخارجية مطلوب لقراءة ملف الـ OPML</string>
|
||||
<string name="successful_import_label">استيراد ناجح</string>
|
||||
|
|
|
@ -459,7 +459,7 @@
|
|||
<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="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="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>
|
||||
|
|
|
@ -521,7 +521,7 @@
|
|||
<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="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="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>
|
||||
|
|
|
@ -573,7 +573,7 @@
|
|||
<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="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="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>
|
||||
|
|
|
@ -545,7 +545,7 @@
|
|||
<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="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="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>
|
||||
|
|
|
@ -549,7 +549,7 @@
|
|||
<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="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="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>
|
||||
|
|
|
@ -562,7 +562,7 @@
|
|||
<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="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="opml_import_ask_read_permission">Necesita acceso al almacenamiento externo para leer archivos OPML</string>
|
||||
<string name="successful_import_label">Importación exitosa</string>
|
||||
|
|
|
@ -403,7 +403,7 @@
|
|||
<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="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="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>
|
||||
|
|
|
@ -488,7 +488,7 @@
|
|||
<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="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="opml_import_ask_read_permission">Kanpo biltegiratzerako sarbidea behar duzu OPML fitxategia irakurtzeko</string>
|
||||
<string name="successful_import_label">Inportate arrakastatsua</string>
|
||||
|
|
|
@ -524,7 +524,7 @@
|
|||
<string name="database_import_label">وارد کردن پایگاه داده</string>
|
||||
<string name="database_import_warning">وارد کردن یک پایگاه داده باعث جایگزین تمام اشتراکهای فعلی و سابقه پخش شما خواهد شد. شما باید پایگاه داده فعلی خود را به عنوان پشتیبان خارج کنید. آیا شما میخواهید جایگزین کنید؟</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="opml_import_ask_read_permission">برای خواندن پروندهٔ OPML نباز به دسترسی حافظهٔ خارجی است</string>
|
||||
<string name="successful_import_label">وارد کردن موفقیت آمیز</string>
|
||||
|
|
|
@ -506,7 +506,7 @@
|
|||
<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="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="opml_import_ask_read_permission">Pääsy ulkoiseen solvellukseen tarvitaan OPML tiedoston lukemiseen</string>
|
||||
<string name="successful_import_label">Tuonti onnistui</string>
|
||||
|
|
|
@ -562,7 +562,7 @@
|
|||
<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="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="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>
|
||||
|
|
|
@ -545,7 +545,7 @@
|
|||
<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="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="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>
|
||||
|
|
|
@ -420,7 +420,7 @@
|
|||
<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="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="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>
|
||||
|
|
|
@ -429,7 +429,7 @@
|
|||
<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="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="opml_import_ask_read_permission">Akses ke penyimpanan eksternal diperlukan untuk membaca berkas OPML</string>
|
||||
<string name="successful_import_label">Impor sukses!</string>
|
||||
|
|
|
@ -563,7 +563,7 @@
|
|||
<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="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="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>
|
||||
|
|
|
@ -573,7 +573,7 @@
|
|||
<string name="database_import_label">ייבוא מסד נתונים</string>
|
||||
<string name="database_import_warning">ייבוא מסד נתונים יחליף את כל המינויים הנוכחיים שלך לרבות היסטוריית הנגינה. עליך תחילה לייצא את מסד הנתונים הנוכחי שלך כגיבוי. להמשיך בהחלפה?</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="opml_import_ask_read_permission">נדרשת גישה לאחסון חיצוני כדי לקרוא את קובץ ה־OPML</string>
|
||||
<string name="successful_import_label">הייבוא הצליח</string>
|
||||
|
|
|
@ -383,7 +383,7 @@
|
|||
<string name="database_import_label">データベースのインポート</string>
|
||||
<string name="database_import_warning">データベースをインポートすると、現在の購読と再生履歴がすべて置き換えられます。現在のデータベースをバックアップとしてエクスポートする必要があります。それでも置き換えしますか?</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="opml_import_ask_read_permission">OPML ファイルを読み込むために、外部ストレージへのアクセスが必要です</string>
|
||||
<string name="successful_import_label">インポートしました</string>
|
||||
|
|
|
@ -503,7 +503,7 @@
|
|||
<string name="database_import_label">데이터베이스 가져오기</string>
|
||||
<string name="database_import_warning">데이터베이스 가져오기를 하면 모든 구독과 재생 히스토리가 가져온 내용으로 바뀝니다. 백업을 하려면 현재 데이터베이스를 내보내기 해야 합니다. 정말로 데이터베이스를 바꾸시겠습니까\?</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="opml_import_ask_read_permission">OPML 파일을 읽으려면 외부 저장소 접근이 필요합니다</string>
|
||||
<string name="successful_import_label">가져오기 성공</string>
|
||||
|
|
|
@ -395,7 +395,7 @@
|
|||
<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="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="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>
|
||||
|
|
|
@ -512,7 +512,7 @@
|
|||
<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="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="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>
|
||||
|
|
|
@ -480,7 +480,7 @@
|
|||
<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="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="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>
|
||||
|
|
|
@ -526,7 +526,7 @@
|
|||
<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="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="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>
|
||||
|
|
|
@ -519,7 +519,7 @@
|
|||
<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="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="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>
|
||||
|
|
|
@ -562,7 +562,7 @@
|
|||
<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="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="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>
|
||||
|
|
|
@ -559,7 +559,7 @@
|
|||
<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="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="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>
|
||||
|
|
|
@ -545,7 +545,7 @@
|
|||
<string name="database_import_label">Импорт базы данных</string>
|
||||
<string name="database_import_warning">Импорт базы данных приведет к замене всех текущих подписок и истории воспроизведения. Следует экспортировать текущую базу данных, чтобы иметь резервную копию. Хотите заменить\?</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="opml_import_ask_read_permission">Для чтения файла OPML необходим доступ к внешнему хранилищу</string>
|
||||
<string name="successful_import_label">Импорт успешен</string>
|
||||
|
|
|
@ -573,7 +573,7 @@
|
|||
<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="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="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>
|
||||
|
|
|
@ -382,7 +382,7 @@
|
|||
<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="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="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>
|
||||
|
|
|
@ -545,7 +545,7 @@
|
|||
<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="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="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>
|
||||
|
|
|
@ -522,7 +522,7 @@
|
|||
<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="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="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>
|
||||
|
|
|
@ -573,7 +573,7 @@
|
|||
<string name="database_import_label">Імпортувати базу даних</string>
|
||||
<string name="database_import_warning">Імпорт бази даних замінить усі ваші поточні підписки та історію відтворення. Ви повинні експортувати свою поточну базу даних як резервну копію. Бажаєте замінити\?</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="opml_import_ask_read_permission">Щоб прочитати файл OPML потрібен доступ до зовнішнього носія</string>
|
||||
<string name="successful_import_label">Імпорт - успішний</string>
|
||||
|
|
|
@ -532,7 +532,7 @@
|
|||
<string name="database_import_label">数据库导入</string>
|
||||
<string name="database_import_warning">导入数据库将替换所有当前订阅和播放历史记录。您应该将当前数据库导出为备份。您要替换吗?</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="opml_import_ask_read_permission">读取 OPML 文件需要访问外部存储的权限</string>
|
||||
<string name="successful_import_label">导入成功</string>
|
||||
|
|
|
@ -311,7 +311,7 @@
|
|||
<string name="database_import_label">資料庫匯入</string>
|
||||
<string name="database_import_warning">匯入的資料將取代您目前的訂閱清單與播放歷史紀錄,您最好先匯出當前的資料庫以便備份。要取代目前的資料嗎?</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="opml_import_ask_read_permission">讀取 OPML 檔需要存取外部儲存空間的權限</string>
|
||||
<string name="successful_import_label">匯入完畢</string>
|
||||
|
|
|
@ -639,7 +639,7 @@
|
|||
<string name="import_directory_toast">Only accepting directory name including: </string>
|
||||
<string name="import_file_type_toast">Only accepting file extension: </string>
|
||||
<string name="please_wait">Please wait…</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="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>
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:search="http://schemas.android.com/apk/com.bytehamster.lib.preferencesearch">
|
||||
|
||||
<Preference
|
||||
android:title="@string/choose_data_directory"
|
||||
android:key="prefChooseDataDir"/>
|
||||
<!-- <Preference-->
|
||||
<!-- android:title="@string/choose_data_directory"-->
|
||||
<!-- android:key="prefChooseDataDir"/>-->
|
||||
|
||||
<PreferenceCategory android:title="@string/automation">
|
||||
<ac.mdiq.podcini.preferences.MaterialListPreference
|
||||
|
|
|
@ -4,7 +4,6 @@ import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
|||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.enqueueLocation
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue
|
||||
import ac.mdiq.podcini.storage.database.Episodes.addToHistory
|
||||
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.persistEpisodeMedia
|
||||
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.clearQueue
|
||||
import ac.mdiq.podcini.storage.database.Queues.enqueueLocation
|
||||
import ac.mdiq.podcini.storage.database.Queues.moveInQueue
|
||||
import ac.mdiq.podcini.storage.database.Queues.removeFromQueue
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
|
@ -540,7 +541,7 @@ class DbWriterTest {
|
|||
|
||||
@Throws(Exception::class)
|
||||
private fun queueTestSetupMultipleItems(numItems: Int): Feed {
|
||||
enqueueLocation = UserPreferences.EnqueueLocation.BACK
|
||||
enqueueLocation = Queues.EnqueueLocation.BACK
|
||||
val feed = Feed("url", null, "title")
|
||||
feed.episodes.clear()
|
||||
for (i in 0 until numItems) {
|
||||
|
|
|
@ -3,8 +3,8 @@ package ac.mdiq.podcini.storage
|
|||
import ac.mdiq.podcini.feed.FeedMother.anyFeed
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
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.EnqueueLocation
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.storage.model.Playable
|
||||
|
@ -74,7 +74,7 @@ open class BasicTest {
|
|||
var idsExpected: List<Long?>? = null
|
||||
|
||||
@Parameterized.Parameter(2)
|
||||
var options: EnqueueLocation? = null
|
||||
var options: Queues.EnqueueLocation? = null
|
||||
|
||||
@Parameterized.Parameter(3)
|
||||
var curQueue: List<Episode?>? = null
|
||||
|
|
|
@ -14,7 +14,7 @@ buildscript {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
14
changelog.md
14
changelog.md
|
@ -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
|
||||
|
||||
* 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.
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue