diff --git a/app/build.gradle b/app/build.gradle index 9d045bd9..193c109f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -26,8 +26,8 @@ android { vectorDrawables.useSupportLibrary false vectorDrawables.generatedDensities = [] - versionCode 3020297 - versionName "6.13.10" + versionCode 3020298 + versionName "6.13.11" applicationId "ac.mdiq.podcini.R" def commit = "" diff --git a/app/src/main/assets/licenses.xml b/app/src/main/assets/licenses.xml index d933ed38..a4390fb0 100644 --- a/app/src/main/assets/licenses.xml +++ b/app/src/main/assets/licenses.xml @@ -114,10 +114,10 @@ website="https://github.com/ByteHamster/SearchPreference" license="MIT" licenseText="LICENSE_SEARCHPREFERENCE.txt" /> - + + + + + + diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt index 67bd4c2c..5354be0c 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt @@ -15,13 +15,12 @@ import ac.mdiq.podcini.storage.database.LogsAndStats import ac.mdiq.podcini.storage.database.Queues 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.DownloadResult import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.EpisodeMedia +import ac.mdiq.podcini.storage.model.EpisodeMedia.MediaMetadataRetrieverCompat import ac.mdiq.podcini.storage.utils.ChapterUtils -import ac.mdiq.podcini.storage.utils.MediaMetadataRetrieverCompat import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter import ac.mdiq.podcini.ui.utils.NotificationUtils import ac.mdiq.podcini.util.EventFlow @@ -314,7 +313,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() { MediaMetadataRetrieverCompat().use { mmr -> if (it.media != null) mmr.setDataSource(it.media!!.fileUrl) durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) - if (durationStr != null) it.media?.setDuration(durationStr!!.toInt()) + if (durationStr != null) it.media?.setDuration(durationStr.toInt()) } } catch (e: NumberFormatException) { Logd(TAG, "Invalid file duration: $durationStr") } catch (e: Exception) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedBuilder.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedBuilder.kt index f43e23ad..bdb46e10 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedBuilder.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedBuilder.kt @@ -344,9 +344,7 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit) } fun subscribe(feed: Feed) { - while (feed.isBuilding) { - runBlocking { delay(200) } - } + while (feed.isBuilding) runBlocking { delay(200) } feed.id = 0L for (item in feed.episodes) { item.id = 0L diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/LocalFeedUpdater.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/LocalFeedUpdater.kt index f7b2991b..e919f7b3 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/LocalFeedUpdater.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/LocalFeedUpdater.kt @@ -11,7 +11,7 @@ import ac.mdiq.podcini.net.feed.parser.utils.MimeTypeUtils import ac.mdiq.podcini.storage.database.Feeds import ac.mdiq.podcini.storage.database.LogsAndStats import ac.mdiq.podcini.storage.model.* -import ac.mdiq.podcini.storage.utils.MediaMetadataRetrieverCompat +import ac.mdiq.podcini.storage.model.EpisodeMedia.MediaMetadataRetrieverCompat import ac.mdiq.podcini.util.Logd import android.content.Context import android.media.MediaMetadataRetriever diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/LocalMediaPlayer.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/LocalMediaPlayer.kt index 3d004bfb..b523dfaa 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/LocalMediaPlayer.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/LocalMediaPlayer.kt @@ -193,7 +193,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP if (curMedia is EpisodeMedia) { val media_ = curMedia as EpisodeMedia var item = media_.episodeOrFetch() - if (item != null && item.playState < PlayState.INPROGRESS.code) item = runBlocking { setPlayStateSync(PlayState.INPROGRESS.code, item!!, false) } + if (item != null && item.playState < PlayState.INPROGRESS.code) item = runBlocking { setPlayStateSync(PlayState.INPROGRESS.code, item, false) } val eList = if (item?.feed?.preferences?.queue != null) curQueue.episodes else item?.feed?.getVirtualQueueItems() ?: listOf() curIndexInQueue = EpisodeUtil.indexOfItemWithId(eList, media_.id) } else curIndexInQueue = -1 @@ -629,8 +629,8 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP var exoPlayer: ExoPlayer? = null private var exoplayerListener: Listener? = null - private var audioSeekCompleteListener: java.lang.Runnable? = null - private var audioCompletionListener: java.lang.Runnable? = null + private var audioSeekCompleteListener: Runnable? = null + private var audioCompletionListener: Runnable? = null private var audioErrorListener: Consumer? = null private var bufferingUpdateListener: Consumer? = null private var loudnessEnhancer: LoudnessEnhancer? = null diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt index ab70b976..d82eba74 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt @@ -41,7 +41,6 @@ import androidx.media3.extractor.DefaultExtractorsFactory import androidx.media3.extractor.mp3.Mp3Extractor import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean -import kotlin.concurrent.Volatile import kotlin.math.max /* diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt index a3c9062d..566d1e7a 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt @@ -92,7 +92,6 @@ import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata import androidx.media3.common.Player.STATE_ENDED import androidx.media3.common.Player.STATE_IDLE - import androidx.media3.session.* import androidx.work.impl.utils.futures.SettableFuture import com.google.common.collect.ImmutableList @@ -104,7 +103,6 @@ import java.util.* import java.util.concurrent.ScheduledFuture import java.util.concurrent.ScheduledThreadPoolExecutor import java.util.concurrent.TimeUnit -import kotlin.concurrent.Volatile import kotlin.math.max import kotlin.math.sqrt @@ -309,8 +307,7 @@ class PlaybackService : MediaLibraryService() { else -> {} } } - if (Build.VERSION.SDK_INT >= VERSION_CODES.N) - TileService.requestListeningState(applicationContext, ComponentName(applicationContext, QuickSettingsTileService::class.java)) + TileService.requestListeningState(applicationContext, ComponentName(applicationContext, QuickSettingsTileService::class.java)) sendLocalBroadcast(applicationContext, ACTION_PLAYER_STATUS_CHANGED) bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED) @@ -359,9 +356,38 @@ class PlaybackService : MediaLibraryService() { if (ended || smartMarkAsPlayed || autoSkipped || (skipped && !shouldSkipKeepEpisode)) { Logd(TAG, "onPostPlayback ended: $ended smartMarkAsPlayed: $smartMarkAsPlayed autoSkipped: $autoSkipped skipped: $skipped") // only mark the item as played if we're not keeping it anyways - item = setPlayStateSync(PlayState.PLAYED.code, item!!, ended || (skipped && smartMarkAsPlayed), false) - if (playable is EpisodeMedia && (ended || skipped || playingNext)) { - item = upsert(item!!) { it.media?.playbackCompletionDate = Date() } + +// item = setPlayStateSync(PlayState.PLAYED.code, item!!, ended || (skipped && smartMarkAsPlayed), false) +// if (playable is EpisodeMedia && (ended || skipped || playingNext)) { +// item = upsert(item!!) { +// it.media?.playbackCompletionDate = Date() +// } +// EventFlow.postEvent(FlowEvent.HistoryEvent()) +// } + + if (playable !is EpisodeMedia) + item = setPlayStateSync(PlayState.PLAYED.code, item!!, ended || (skipped && smartMarkAsPlayed), false) + else { + val item_ = realm.query(Episode::class).query("id == $0", item!!.id).first().find() + if (item_ != null) { + item = upsert(item_) { + it.playState = PlayState.PLAYED.code + val media = it.media + if (media != null) { + media.startPosition = playable.startPosition + media.startTime = playable.startTime + media.playedDurationWhenStarted = playable.playedDurationWhenStarted + media.setPosition(playable.getPosition()) + media.setLastPlayedTime(System.currentTimeMillis()) + if (media.startPosition >= 0 && media.getPosition() > media.startPosition) + media.playedDuration = (media.playedDurationWhenStarted + media.getPosition() - media.startPosition) + media.timeSpent = media.timeSpentOnStart + (System.currentTimeMillis() - media.startTime).toInt() + if (ended || (skipped && smartMarkAsPlayed)) media.setPosition(0) + if (ended || skipped || playingNext) media.playbackCompletionDate = Date() + } + } + } + EventFlow.postEvent(FlowEvent.EpisodePlayedEvent(item)) EventFlow.postEvent(FlowEvent.HistoryEvent()) } val action = item?.feed?.preferences?.autoDeleteAction @@ -370,8 +396,8 @@ class PlaybackService : MediaLibraryService() { val isItemdeletable = (!shouldKeepSuperEpisode || (item?.isSUPER != true && item?.playState != PlayState.AGAIN.code && item?.playState != PlayState.FOREVER.code)) if (playable is EpisodeMedia && shouldAutoDelete && isItemdeletable) { if (playable.localFileAvailable()) item = deleteMediaSync(this@PlaybackService, item!!) - if (prefDeleteRemovesFromQueue) removeFromAllQueuesSync( item!!) - } else if (prefRemoveFromQueueMarkedPlayed) removeFromAllQueuesSync(item!!) + if (prefDeleteRemovesFromQueue) removeFromAllQueuesSync(item) + } else if (prefRemoveFromQueueMarkedPlayed) removeFromAllQueuesSync(item) } } } @@ -674,7 +700,7 @@ class PlaybackService : MediaLibraryService() { recreateMediaPlayer() if (LocalMediaPlayer.exoPlayer == null) LocalMediaPlayer.createStaticPlayer(applicationContext) val intent = packageManager.getLaunchIntentForPackage(packageName) - val pendingIntent = PendingIntent.getActivity(this, 0, intent, if (Build.VERSION.SDK_INT >= 23) FLAG_IMMUTABLE else FLAG_UPDATE_CURRENT) + val pendingIntent = PendingIntent.getActivity(this, 0, intent, FLAG_IMMUTABLE) mediaSession = MediaLibrarySession.Builder(applicationContext, LocalMediaPlayer.exoPlayer!!, mediaLibrarySessionCK) .setId(packageName) .setSessionActivity(pendingIntent) @@ -757,7 +783,7 @@ class PlaybackService : MediaLibraryService() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val keycode = intent?.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1) ?: -1 val customAction = intent?.getStringExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION) - val hardwareButton = intent?.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false) ?: false + val hardwareButton = intent?.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false) == true val keyEvent: KeyEvent? = if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) intent?.getParcelableExtra(EXTRA_KEY_EVENT, KeyEvent::class.java) else { @@ -793,8 +819,8 @@ class PlaybackService : MediaLibraryService() { playable != null -> { recreateMediaSessionIfNeeded() Logd(TAG, "onStartCommand status: $status") - val allowStreamThisTime = intent?.getBooleanExtra(EXTRA_ALLOW_STREAM_THIS_TIME, false) ?: false - val allowStreamAlways = intent?.getBooleanExtra(EXTRA_ALLOW_STREAM_ALWAYS, false) ?: false + val allowStreamThisTime = intent?.getBooleanExtra(EXTRA_ALLOW_STREAM_THIS_TIME, false) == true + val allowStreamAlways = intent?.getBooleanExtra(EXTRA_ALLOW_STREAM_ALWAYS, false) == true sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0) if (allowStreamAlways) isAllowMobileStreaming = true startPlaying(allowStreamThisTime) @@ -849,8 +875,7 @@ class PlaybackService : MediaLibraryService() { val pendingIntentAlwaysAllow = if (Build.VERSION.SDK_INT >= VERSION_CODES.O) PendingIntent.getForegroundService(this, R.id.pending_intent_allow_stream_always, intentAlwaysAllow, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) - else PendingIntent.getService(this, R.id.pending_intent_allow_stream_always, intentAlwaysAllow, - FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) + else PendingIntent.getService(this, R.id.pending_intent_allow_stream_always, intentAlwaysAllow, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) val builder = Notification.Builder(this, NotificationUtils.CHANNEL_ID.user_action.name) .setSmallIcon(R.drawable.ic_notification_stream) @@ -863,7 +888,7 @@ class PlaybackService : MediaLibraryService() { .addAction(R.drawable.ic_notification_stream, getString(R.string.confirm_mobile_streaming_button_always), pendingIntentAlwaysAllow) .setAutoCancel(true) - val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager notificationManager.notify(5566, builder.build()) } @@ -941,7 +966,7 @@ class PlaybackService : MediaLibraryService() { return true } } - KeyEvent.KEYCODE_MEDIA_STOP -> { + KEYCODE_MEDIA_STOP -> { if (status == PlayerStatus.FALLBACK || status == PlayerStatus.PLAYING) mPlayer?.pause(abandonFocus = true, reinit = true) return true } @@ -1154,9 +1179,8 @@ class PlaybackService : MediaLibraryService() { media.setPosition(position) media.setLastPlayedTime(System.currentTimeMillis()) if (it.isNew) it.playState = PlayState.UNPLAYED.code - if (media.startPosition >= 0 && media.getPosition() > media.startPosition) { + if (media.startPosition >= 0 && media.getPosition() > media.startPosition) media.playedDuration = (media.playedDurationWhenStarted + media.getPosition() - media.startPosition) - } media.timeSpent = media.timeSpentOnStart + (System.currentTimeMillis() - media.startTime).toInt() Logd(TAG, "saveCurrentPosition ${media.startTime} timeSpent: ${media.timeSpent} playedDuration: ${media.playedDuration}") } @@ -1246,7 +1270,6 @@ class PlaybackService : MediaLibraryService() { .build(), ), } - class CustomMediaNotificationProvider(context: Context) : DefaultMediaNotificationProvider(context) { override fun addNotificationActions(mediaSession: MediaSession, mediaButtons: ImmutableList, @@ -1283,7 +1306,7 @@ class PlaybackService : MediaLibraryService() { * to notify the PlaybackService about updates from the running tasks. */ class TaskManager(private val context: Context, private val callback: PSTMCallback) { - private val schedExecutor: ScheduledThreadPoolExecutor = ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE) { r: java.lang.Runnable? -> + private val schedExecutor: ScheduledThreadPoolExecutor = ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE) { r: Runnable? -> val t = Thread(r) t.priority = Thread.MIN_PRIORITY t @@ -1441,7 +1464,7 @@ class PlaybackService : MediaLibraryService() { schedExecutor.shutdownNow() } - private fun useMainThreadIfNecessary(runnable: java.lang.Runnable): java.lang.Runnable { + private fun useMainThreadIfNecessary(runnable: Runnable): Runnable { if (Looper.myLooper() == Looper.getMainLooper()) { // Called in main thread => ExoPlayer is used // Run on ui thread even if called from schedExecutor @@ -1453,7 +1476,7 @@ class PlaybackService : MediaLibraryService() { /** * Sleeps for a given time and then pauses playback. */ - internal inner class SleepTimer(private val waitingTime: Long) : java.lang.Runnable { + internal inner class SleepTimer(private val waitingTime: Long) : Runnable { private var hasVibrated = false private var timeLeft = waitingTime private var shakeListener: ShakeListener? = null @@ -1479,7 +1502,7 @@ class PlaybackService : MediaLibraryService() { if (timeLeft < NOTIFICATION_THRESHOLD) { Logd(TAG, "Sleep timer is about to expire") if (SleepTimerPreferences.vibrate() && !hasVibrated) { - val v = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator + val v = context.getSystemService(VIBRATOR_SERVICE) as? Vibrator if (v != null) { v.vibrate(500) hasVibrated = true @@ -1527,7 +1550,7 @@ class PlaybackService : MediaLibraryService() { private fun resume() { // only a precaution, the user should actually not be able to activate shake to reset // when the accelerometer is not available - mSensorMgr = mContext.getSystemService(Context.SENSOR_SERVICE) as SensorManager + mSensorMgr = mContext.getSystemService(SENSOR_SERVICE) as SensorManager if (mSensorMgr == null) throw UnsupportedOperationException("Sensors not supported") mAccelerometer = mSensorMgr!!.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) @@ -1656,7 +1679,7 @@ class PlaybackService : MediaLibraryService() { } var isStartWhenPrepared: Boolean - get() = playbackService?.mPlayer?.startWhenPrepared?.get() ?: false + get() = playbackService?.mPlayer?.startWhenPrepared?.get() == true set(s) { playbackService?.mPlayer?.startWhenPrepared?.set(s) } @@ -1694,7 +1717,7 @@ class PlaybackService : MediaLibraryService() { } fun isSleepTimerActive(): Boolean { - return playbackService?.taskManager?.isSleepTimerActive ?: false + return playbackService?.taskManager?.isSleepTimerActive == true } fun clearCurTempSpeed() { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/QuickSettingsTileService.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/QuickSettingsTileService.kt index 277d270f..8eaba3bc 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/QuickSettingsTileService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/QuickSettingsTileService.kt @@ -1,21 +1,16 @@ package ac.mdiq.podcini.playback.service +import ac.mdiq.podcini.playback.base.InTheatre.curState import ac.mdiq.podcini.receiver.MediaButtonReceiver import ac.mdiq.podcini.storage.model.CurrentState.Companion.PLAYER_STATUS_PLAYING -import ac.mdiq.podcini.playback.base.InTheatre.curState import ac.mdiq.podcini.util.Logd import android.content.ComponentName import android.content.Intent -import android.os.Build import android.os.IBinder import android.service.quicksettings.Tile import android.service.quicksettings.TileService import android.view.KeyEvent -import androidx.annotation.RequiresApi - - -@RequiresApi(api = Build.VERSION_CODES.N) class QuickSettingsTileService : TileService() { override fun onTileAdded() { super.onTileAdded() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt index 89714921..f616ce96 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt @@ -13,7 +13,6 @@ import ac.mdiq.podcini.playback.service.PlaybackService.Companion.ACTION_SHUTDOW import ac.mdiq.podcini.preferences.UserPreferences.Prefs import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueuesSync -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.upsert @@ -215,11 +214,7 @@ object Episodes { */ fun setPlayState(played: Int, resetMediaPosition: Boolean, vararg episodes: Episode) : Job { Logd(TAG, "setPlayState called") - return runOnIOScope { - for (episode in episodes) { - setPlayStateSync(played, episode, resetMediaPosition) - } - } + return runOnIOScope { for (episode in episodes) setPlayStateSync(played, episode, resetMediaPosition) } } suspend fun setPlayStateSync(played: Int, episode: Episode, resetMediaPosition: Boolean, removeFromQueue: Boolean = true) : Episode { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt index 9dee91c2..7dae28d1 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt @@ -285,7 +285,7 @@ object Feeds { else savedFeed.episodes.add(idx, episode) val pubDate = episode.getPubDate() - if (pubDate == null || priorMostRecentDate == null || priorMostRecentDate.before(pubDate) || priorMostRecentDate == pubDate) { + if (priorMostRecentDate == null || priorMostRecentDate.before(pubDate) || priorMostRecentDate == pubDate) { Logd(TAG, "Marking episode published on $pubDate new, prior most recent date = $priorMostRecentDate") episode.playState = PlayState.NEW.code if (savedFeed.preferences?.autoAddNewToQueue == true) { @@ -385,8 +385,7 @@ object Feeds { for (e in savedFeed.episodes) savedFeed.totleDuration += e.media?.duration ?: 0 val resultFeed = savedFeed - try { - upsertBlk(savedFeed) {} + try { upsertBlk(savedFeed) {} } catch (e: InterruptedException) { e.printStackTrace() } catch (e: ExecutionException) { e.printStackTrace() } return resultFeed @@ -536,7 +535,7 @@ object Feeds { feed.downloadUrl = null feed.hasVideoMedia = video feed.fileUrl = File(feedfilePath, getFeedfileName(feed)).toString() - feed.preferences = FeedPreferences(feed.id, false, FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, "", "") + feed.preferences = FeedPreferences(feed.id, false, AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, "", "") feed.preferences!!.keepUpdated = false feed.preferences!!.queueId = -2L return feed @@ -736,10 +735,10 @@ object Feeds { return string1 == string2 } internal fun datesLookSimilar(item1: Episode, item2: Episode): Boolean { - if (item1.getPubDate() == null || item2.getPubDate() == null) return false +// if (item1.getPubDate() == null || item2.getPubDate() == null) return false val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.US) // MM/DD/YY - val dateOriginal = dateFormat.format(item2.getPubDate()!!) - val dateNew = dateFormat.format(item1.getPubDate()!!) + val dateOriginal = dateFormat.format(item2.getPubDate()) + val dateNew = dateFormat.format(item1.getPubDate()) return dateOriginal == dateNew // Same date; time is ignored. } internal fun durationsLookSimilar(media1: EpisodeMedia, media2: EpisodeMedia): Boolean { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/LogsAndStats.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/LogsAndStats.kt index 3e0b0ed5..36fa09a6 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/LogsAndStats.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/LogsAndStats.kt @@ -4,7 +4,6 @@ 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.DownloadResult -import ac.mdiq.podcini.storage.utils.DownloadResultComparator import ac.mdiq.podcini.util.EventFlow import ac.mdiq.podcini.util.FlowEvent import ac.mdiq.podcini.util.Logd @@ -30,4 +29,11 @@ object LogsAndStats { } } } + + /** Compares the completion date of two DownloadResult objects. */ + class DownloadResultComparator : Comparator { + override fun compare(lhs: DownloadResult, rhs: DownloadResult): Int { + return rhs.getCompletionDate().compareTo(lhs.getCompletionDate()) + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt index af8e630d..974439b4 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt @@ -344,7 +344,7 @@ object Queues { val random = Random() return random.nextInt(queueItems.size + 1) } - else -> throw AssertionError("calcPosition() : unrecognized enqueueLocation option: $enqueueLocation") +// else -> throw AssertionError("calcPosition() : unrecognized enqueueLocation option: $enqueueLocation") } } private fun getPositionOfFirstNonDownloadingItem(startPosition: Int, queueItems: List): Int { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt index fbfebacb..14ee7f23 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt @@ -4,10 +4,9 @@ import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.unmanaged import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.model.VolumeAdaptionSetting.Companion.fromInteger -import ac.mdiq.podcini.storage.utils.MediaMetadataRetrieverCompat import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.showStackTrace import android.content.Context +import android.media.MediaMetadataRetriever import android.os.Parcel import android.os.Parcelable import androidx.compose.runtime.getValue @@ -18,6 +17,7 @@ import io.realm.kotlin.types.EmbeddedRealmObject import io.realm.kotlin.types.annotations.Ignore import io.realm.kotlin.types.annotations.Index import java.io.File +import java.io.IOException import java.util.* import kotlin.math.max @@ -229,7 +229,7 @@ class EpisodeMedia: EmbeddedRealmObject, Playable { fun hasEmbeddedPicture(): Boolean { // TODO: checkEmbeddedPicture needs to update current copy if (hasEmbeddedPicture == null) checkEmbeddedPicture() - return hasEmbeddedPicture ?: false + return hasEmbeddedPicture == true } override fun writeToParcel(dest: Parcel, flags: Int) { @@ -411,6 +411,15 @@ class EpisodeMedia: EmbeddedRealmObject, Playable { return result } + /** + * On SDK<29, this class does not have a close method yet, so the app crashes when using try-with-resources. + */ + class MediaMetadataRetrieverCompat : MediaMetadataRetriever(), AutoCloseable { + override fun close() { + try { release() } catch (e: IOException) { e.printStackTrace() } + } + } + companion object { private val TAG: String = EpisodeMedia::class.simpleName ?: "Anonymous" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/DownloadResultComparator.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/DownloadResultComparator.kt deleted file mode 100644 index 9e88f180..00000000 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/DownloadResultComparator.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ac.mdiq.podcini.storage.utils - -import ac.mdiq.podcini.storage.model.DownloadResult - -/** Compares the completion date of two DownloadResult objects. */ -class DownloadResultComparator : Comparator { - override fun compare(lhs: DownloadResult, rhs: DownloadResult): Int { - return rhs.getCompletionDate().compareTo(lhs.getCompletionDate()) - } -} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/MediaMetadataRetrieverCompat.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/MediaMetadataRetrieverCompat.kt deleted file mode 100644 index bff11088..00000000 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/MediaMetadataRetrieverCompat.kt +++ /dev/null @@ -1,17 +0,0 @@ -package ac.mdiq.podcini.storage.utils - -import android.media.MediaMetadataRetriever -import java.io.IOException - -/** - * On SDK<29, this class does not have a close method yet, so the app crashes when using try-with-resources. - */ -class MediaMetadataRetrieverCompat : MediaMetadataRetriever(), AutoCloseable { - override fun close() { - try { - release() - } catch (e: IOException) { - e.printStackTrace() - } - } -} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeActionButton.kt index 3ed89b5e..5a583668 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeActionButton.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeActionButton.kt @@ -19,9 +19,9 @@ import ac.mdiq.podcini.storage.database.RealmDB import ac.mdiq.podcini.storage.model.* import ac.mdiq.podcini.storage.utils.AudioMediaTools import ac.mdiq.podcini.storage.utils.FilesUtils +import ac.mdiq.podcini.ui.actions.SwipeActions.Companion.deleteEpisodesWarnLocal import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode import ac.mdiq.podcini.ui.fragment.FeedEpisodesFragment -import ac.mdiq.podcini.ui.utils.LocalDeleteModal import ac.mdiq.podcini.util.EventFlow import ac.mdiq.podcini.util.FlowEvent import ac.mdiq.podcini.util.IntentUtils @@ -86,7 +86,7 @@ abstract class EpisodeActionButton internal constructor(@JvmField var item: Epis val media = item.media ?: return TTSActionButton(item) val isDownloadingMedia = when (media.downloadUrl) { null -> false - else -> DownloadServiceInterface.get()?.isDownloadingEpisode(media.downloadUrl!!)?:false + else -> DownloadServiceInterface.get()?.isDownloadingEpisode(media.downloadUrl!!) == true } Logd("ItemActionButton", "forItem: local feed: ${item.feed?.isLocalFeed} downloaded: ${media.downloaded} playing: ${isCurrentlyPlaying(media)} ${item.title} ") return when { @@ -292,7 +292,7 @@ class DeleteActionButton(item: Episode) : EpisodeActionButton(item) { } override fun onClick(context: Context) { - LocalDeleteModal.deleteEpisodesWarnLocal(context, listOf(item)) + deleteEpisodesWarnLocal(context, listOf(item)) actionState.value = getLabel() } } @@ -360,7 +360,7 @@ class DownloadActionButton(item: Episode) : EpisodeActionButton(item) { private fun shouldNotDownload(media: EpisodeMedia?): Boolean { if (media?.downloadUrl == null) return true - val isDownloading = DownloadServiceInterface.get()?.isDownloadingEpisode(media.downloadUrl!!)?:false + val isDownloading = DownloadServiceInterface.get()?.isDownloadingEpisode(media.downloadUrl!!) == true return isDownloading || media.downloaded } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/MenuItemUtils.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/MenuItemUtils.kt deleted file mode 100644 index 9456d45c..00000000 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/MenuItemUtils.kt +++ /dev/null @@ -1,25 +0,0 @@ -package ac.mdiq.podcini.ui.actions - -import android.view.Menu -import android.view.MenuItem - -/** - * Utilities for menu items - */ -object MenuItemUtils { - /** - * When pressing a context menu item, Android calls onContextItemSelected - * for ALL fragments in arbitrary order, not just for the fragment that the - * context menu was created from. This assigns the listener to every menu item, - * so that the correct fragment is always called first and can consume the click. - * - * Note that Android still calls the onContextItemSelected methods of all fragments - * when the passed listener returns false. - */ - fun setOnClickListeners(menu: Menu?, listener: MenuItem.OnMenuItemClickListener?) { - for (i in 0 until menu!!.size()) { - if (menu.getItem(i).subMenu != null) setOnClickListeners(menu.getItem(i).subMenu, listener) - menu.getItem(i).setOnMenuItemClickListener(listener) - } - } -} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/SwipeAction.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/SwipeAction.kt deleted file mode 100644 index d2dfc921..00000000 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/SwipeAction.kt +++ /dev/null @@ -1,42 +0,0 @@ -package ac.mdiq.podcini.ui.actions - -import android.content.Context -import androidx.annotation.AttrRes -import androidx.annotation.DrawableRes -import androidx.fragment.app.Fragment -import ac.mdiq.podcini.storage.model.Episode -import ac.mdiq.podcini.storage.model.EpisodeFilter - -interface SwipeAction { - fun getId(): String? - fun getTitle(context: Context): String - - @DrawableRes - fun getActionIcon(): Int - - @AttrRes - @DrawableRes - fun getActionColor(): Int - - fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) - - fun willRemove(filter: EpisodeFilter, item: Episode): Boolean { - return false - } - - enum class ActionTypes { - NO_ACTION, - COMBO, - RATING, - COMMENT, - SET_PLAY_STATE, - ADD_TO_QUEUE, - PUT_TO_QUEUE, - REMOVE_FROM_QUEUE, - START_DOWNLOAD, - DELETE, - REMOVE_FROM_HISTORY, - SHELVE, - ERASE - } -} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/SwipeActions.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/SwipeActions.kt index 2accb853..71f5b0be 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/SwipeActions.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/SwipeActions.kt @@ -2,6 +2,7 @@ package ac.mdiq.podcini.ui.actions import ac.mdiq.podcini.R import ac.mdiq.podcini.playback.base.InTheatre.curQueue +import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodeMedia import ac.mdiq.podcini.storage.database.Episodes.setPlayState import ac.mdiq.podcini.storage.database.Episodes.setPlayStateSync import ac.mdiq.podcini.storage.database.Queues.addToQueue @@ -14,19 +15,19 @@ import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.EpisodeFilter import ac.mdiq.podcini.storage.model.PlayState import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded -import ac.mdiq.podcini.ui.actions.SwipeAction.ActionTypes -import ac.mdiq.podcini.ui.actions.SwipeAction.ActionTypes.NO_ACTION import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.compose.* import ac.mdiq.podcini.ui.fragment.* -import ac.mdiq.podcini.ui.utils.LocalDeleteModal.deleteEpisodesWarnLocal import ac.mdiq.podcini.util.EventFlow import ac.mdiq.podcini.util.FlowEvent import ac.mdiq.podcini.util.Logd import android.content.Context +import android.content.DialogInterface import android.content.SharedPreferences import android.util.TypedValue import android.view.ViewGroup +import androidx.annotation.AttrRes +import androidx.annotation.DrawableRes import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -50,12 +51,31 @@ import androidx.compose.ui.window.Dialog import androidx.fragment.app.Fragment import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.Job import kotlinx.coroutines.runBlocking import java.util.* -open class SwipeActions(private val fragment: Fragment, private val tag: String) : DefaultLifecycleObserver { +interface SwipeAction { + fun getId(): String? + fun getTitle(context: Context): String + + @DrawableRes + fun getActionIcon(): Int + + @AttrRes + @DrawableRes + fun getActionColor(): Int + + fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) + + fun willRemove(filter: EpisodeFilter, item: Episode): Boolean { + return false + } +} + +class SwipeActions(private val fragment: Fragment, private val tag: String) : DefaultLifecycleObserver { @set:JvmName("setFilterProperty") var filter: EpisodeFilter? = null @@ -111,6 +131,22 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String) } } + enum class ActionTypes { + NO_ACTION, + COMBO, + RATING, + COMMENT, + SET_PLAY_STATE, + ADD_TO_QUEUE, + PUT_TO_QUEUE, + REMOVE_FROM_QUEUE, + START_DOWNLOAD, + DELETE, + REMOVE_FROM_HISTORY, + SHELVE, + ERASE + } + class AddToQueueSwipeAction : SwipeAction { override fun getId(): String { return ActionTypes.ADD_TO_QUEUE.name @@ -155,7 +191,7 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String) Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { for (action in swipeActions) { - if (action.getId() == NO_ACTION.name || action.getId() == ActionTypes.COMBO.name) continue + if (action.getId() == ActionTypes.NO_ACTION.name || action.getId() == ActionTypes.COMBO.name) continue Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(4.dp).clickable { action.performAction(item, fragment, filter) showDialog = false @@ -280,7 +316,7 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String) class NoActionSwipeAction : SwipeAction { override fun getId(): String { - return NO_ACTION.name + return ActionTypes.NO_ACTION.name } override fun getActionIcon(): Int { return R.drawable.ic_questionmark @@ -569,7 +605,7 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String) @JvmStatic fun getPrefsWithDefaults(tag: String): Actions { - val defaultActions = "${NO_ACTION.name},${NO_ACTION.name}" + val defaultActions = "${ActionTypes.NO_ACTION.name},${ActionTypes.NO_ACTION.name}" return getPrefs(tag, defaultActions) } @@ -577,6 +613,25 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String) // return prefs!!.getBoolean(KEY_PREFIX_NO_ACTION + tag, true) // } + fun deleteEpisodesWarnLocal(context: Context, items: Iterable) { + val localItems: MutableList = mutableListOf() + for (item in items) { + if (item.feed?.isLocalFeed == true) localItems.add(item) + else deleteEpisodeMedia(context, item) + } + + if (localItems.isNotEmpty()) { + MaterialAlertDialogBuilder(context) + .setTitle(R.string.delete_episode_label) + .setMessage(R.string.delete_local_feed_warning_body) + .setPositiveButton(R.string.delete_label) { dialog: DialogInterface?, which: Int -> + for (item in localItems) deleteEpisodeMedia(context, item) + } + .setNegativeButton(R.string.cancel_label, null) + .show() + } + } + fun showSettingDialog(fragment: Fragment, tag: String) { val composeView = ComposeView(fragment.requireContext()).apply { setContent { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/BugReportActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/BugReportActivity.kt index 641deb28..45fa96b7 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/BugReportActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/BugReportActivity.kt @@ -12,7 +12,6 @@ import android.content.ClipboardManager import android.content.DialogInterface import android.os.Build import android.os.Bundle -import android.util.Log import android.view.Menu import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity @@ -26,9 +25,6 @@ import java.io.FileInputStream import java.io.IOException import java.nio.charset.Charset -/** - * Displays the 'crash report' screen - */ class BugReportActivity : AppCompatActivity() { private var _binding: BugReportBinding? = null private val binding get() = _binding!! diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt index 909c12e3..74188be2 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt @@ -92,10 +92,6 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.collectLatest import kotlin.math.min -/** - * The activity that is shown when the user launches the app. - */ - class MainActivity : CastEnabledActivity() { private var drawerLayout: DrawerLayout? = null @@ -119,7 +115,7 @@ class MainActivity : CastEnabledActivity() { private var lastTheme = 0 private var navigationBarInsets = Insets.NONE - val prefs by lazy { getSharedPreferences(PREF_NAME, MODE_PRIVATE) } + val prefs by lazy { getSharedPreferences("MainActivityPrefs", MODE_PRIVATE) } private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> Toast.makeText(this, R.string.notification_permission_text, Toast.LENGTH_LONG).show() @@ -129,54 +125,11 @@ class MainActivity : CastEnabledActivity() { } MaterialAlertDialogBuilder(this) .setMessage(R.string.notification_permission_text) - .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> - checkAndRequestUnrestrictedBackgroundActivity(this) - } - .setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> - checkAndRequestUnrestrictedBackgroundActivity(this) - } + .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> checkAndRequestUnrestrictedBackgroundActivity(this) } + .setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> checkAndRequestUnrestrictedBackgroundActivity(this) } .show() } - @Composable - fun UnrestrictedBackgroundPermissionDialog(onDismiss: () -> Unit) { - var dontAskAgain by remember { mutableStateOf(false) } - AlertDialog(onDismissRequest = onDismiss, title = { Text("Permission Required") }, - text = { - Column { - Text(stringResource(R.string.unrestricted_background_permission_text)) - Row(verticalAlignment = Alignment.CenterVertically) { - Checkbox(checked = dontAskAgain, onCheckedChange = { dontAskAgain = it }) - Text(stringResource(R.string.dont_ask_again)) - } - } - }, - confirmButton = { - TextButton(onClick = { - if (dontAskAgain) prefs.edit().putBoolean("dont_ask_again_unrestricted_background", true).apply() - val intent = Intent() - intent.action = Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS - this@MainActivity.startActivity(intent) - onDismiss() - }) { Text("OK") } - }, - dismissButton = { TextButton(onClick = onDismiss) { Text("Cancel") } } - ) - } - - fun checkAndRequestUnrestrictedBackgroundActivity(context: Context) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return - val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager - val isIgnoringBatteryOptimizations = powerManager.isIgnoringBatteryOptimizations(context.packageName) - val dontAskAgain = prefs.getBoolean("dont_ask_again_unrestricted_background", false) - if (!isIgnoringBatteryOptimizations && !dontAskAgain) { - val composeView = ComposeView(this).apply { - setContent { UnrestrictedBackgroundPermissionDialog(onDismiss = { (parent as? ViewGroup)?.removeView(this) }) } - } - (window.decorView as? ViewGroup)?.addView(composeView) - } - } - private var prevState: Int = 0 private val bottomSheetCallback: BottomSheetCallback = object : BottomSheetCallback() { override fun onStateChanged(view: View, state: Int) { @@ -205,7 +158,7 @@ class MainActivity : CastEnabledActivity() { } private val isDrawerOpen: Boolean - get() = drawerLayout?.isDrawerOpen(navDrawer)?:false + get() = drawerLayout?.isDrawerOpen(navDrawer) == true private val screenWidth: Int get() { @@ -227,19 +180,13 @@ class MainActivity : CastEnabledActivity() { } val ioScope = CoroutineScope(Dispatchers.IO) -// init shared preferences ioScope.launch { // RealmDB.apply { } NavDrawerFragment.getSharedPrefs(this@MainActivity) SwipeActions.getSharedPrefs(this@MainActivity) -// QueuesFragment.getSharedPrefs(this@MainActivity) buildTags() monitorFeeds() -// AudioPlayerFragment.getSharedPrefs(this@MainActivity) PlayerWidget.getSharedPrefs(this@MainActivity) -// StatisticsFragment.getSharedPrefs(this@MainActivity) -// OnlineFeedFragment.getSharedPrefs(this@MainActivity) -// ItunesTopListLoader.getSharedPrefs(this@MainActivity) } if (savedInstanceState != null) ensureGeneratedViewIdGreaterThan(savedInstanceState.getInt(Extras.generated_view_id.name, 0)) @@ -247,7 +194,6 @@ class MainActivity : CastEnabledActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) super.onCreate(savedInstanceState) _binding = MainActivityBinding.inflate(layoutInflater) -// setContentView(R.layout.main_activity) setContentView(binding.root) recycledViewPool.setMaxRecycledViews(R.id.view_type_episode_item, 25) dummyView = object : View(this) {} @@ -321,6 +267,45 @@ class MainActivity : CastEnabledActivity() { observeDownloads() } + @Composable + fun UnrestrictedBackgroundPermissionDialog(onDismiss: () -> Unit) { + var dontAskAgain by remember { mutableStateOf(false) } + AlertDialog(onDismissRequest = onDismiss, title = { Text("Permission Required") }, + text = { + Column { + Text(stringResource(R.string.unrestricted_background_permission_text)) + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox(checked = dontAskAgain, onCheckedChange = { dontAskAgain = it }) + Text(stringResource(R.string.dont_ask_again)) + } + } + }, + confirmButton = { + TextButton(onClick = { + if (dontAskAgain) prefs.edit().putBoolean("dont_ask_again_unrestricted_background", true).apply() + val intent = Intent() + intent.action = Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS + this@MainActivity.startActivity(intent) + onDismiss() + }) { Text("OK") } + }, + dismissButton = { TextButton(onClick = onDismiss) { Text("Cancel") } } + ) + } + + fun checkAndRequestUnrestrictedBackgroundActivity(context: Context) { +// if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return + val powerManager = context.getSystemService(POWER_SERVICE) as PowerManager + val isIgnoringBatteryOptimizations = powerManager.isIgnoringBatteryOptimizations(context.packageName) + val dontAskAgain = prefs.getBoolean("dont_ask_again_unrestricted_background", false) + if (!isIgnoringBatteryOptimizations && !dontAskAgain) { + val composeView = ComposeView(this).apply { + setContent { UnrestrictedBackgroundPermissionDialog(onDismiss = { (parent as? ViewGroup)?.removeView(this) }) } + } + (window.decorView as? ViewGroup)?.addView(composeView) + } + } + private fun observeDownloads() { lifecycleScope.launch { withContext(Dispatchers.IO) { WorkManager.getInstance(this@MainActivity).pruneWork().result.get() } @@ -718,7 +703,7 @@ class MainActivity : CastEnabledActivity() { val s: Snackbar if (bottomSheet.state == BottomSheetBehavior.STATE_COLLAPSED) { s = Snackbar.make(mainView, text, duration) - if (audioPlayerView.visibility == View.VISIBLE) s.setAnchorView(audioPlayerView) + if (audioPlayerView.visibility == View.VISIBLE) s.anchorView = audioPlayerView } else s = Snackbar.make(binding.root, text, duration) s.show() @@ -813,7 +798,7 @@ class MainActivity : CastEnabledActivity() { companion object { private val TAG: String = MainActivity::class.simpleName ?: "Anonymous" const val MAIN_FRAGMENT_TAG: String = "main" - const val PREF_NAME: String = "MainActivityPrefs" +// const val PREF_NAME: String = "MainActivityPrefs" const val REQUEST_CODE_FIRST_PERMISSION = 1001 const val REQUEST_CODE_SECOND_PERMISSION = 1002 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/OpmlImportActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/OpmlImportActivity.kt index 72b56014..43d5c6e6 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/OpmlImportActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/OpmlImportActivity.kt @@ -90,11 +90,11 @@ class OpmlImportActivity : AppCompatActivity() { } if (listAdapter != null) { if (checkedCount == listAdapter!!.count) { - selectAll.setVisible(false) - deselectAll.setVisible(true) + selectAll.isVisible = false + deselectAll.isVisible = true } else { - deselectAll.setVisible(false) - selectAll.setVisible(true) + deselectAll.isVisible = false + selectAll.isVisible = true } } } @@ -160,7 +160,7 @@ class OpmlImportActivity : AppCompatActivity() { inflater.inflate(R.menu.opml_selection_options, menu) selectAll = menu.findItem(R.id.select_all_item) deselectAll = menu.findItem(R.id.deselect_all_item) - deselectAll.setVisible(false) + deselectAll.isVisible = false return true } @@ -168,15 +168,15 @@ class OpmlImportActivity : AppCompatActivity() { val itemId = item.itemId when (itemId) { R.id.select_all_item -> { - selectAll.setVisible(false) + selectAll.isVisible = false selectAllItems(true) - deselectAll.setVisible(true) + deselectAll.isVisible = true return true } R.id.deselect_all_item -> { - deselectAll.setVisible(false) + deselectAll.isVisible = false selectAllItems(false) - selectAll.setVisible(true) + selectAll.isVisible = true return true } android.R.id.home -> finish() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt index 25cc1f16..e66ede71 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt @@ -46,15 +46,11 @@ class PreferenceActivity : AppCompatActivity(), SearchPreferenceResultListener { _binding = SettingsActivityBinding.inflate(layoutInflater) setContentView(binding.root) - if (supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) == null) { - supportFragmentManager.beginTransaction() - .replace(binding.settingsContainer.id, MainPreferencesFragment(), FRAGMENT_TAG) - .commit() - } + if (supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) == null) + supportFragmentManager.beginTransaction().replace(binding.settingsContainer.id, MainPreferencesFragment(), FRAGMENT_TAG).commit() + val intent = intent - if (intent.getBooleanExtra(OPEN_AUTO_DOWNLOAD_SETTINGS, false)) { - openScreen(R.xml.preferences_autodownload) - } + if (intent.getBooleanExtra(OPEN_AUTO_DOWNLOAD_SETTINGS, false)) openScreen(R.xml.preferences_autodownload) } private fun getPreferenceScreen(screen: Int): PreferenceFragmentCompat? { @@ -81,27 +77,20 @@ class PreferenceActivity : AppCompatActivity(), SearchPreferenceResultListener { intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS) intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName) startActivity(intent) - } else { - supportFragmentManager.beginTransaction() - .replace(binding.settingsContainer.id, fragment!!) - .addToBackStack(getString(getTitleOfPage(screen))) - .commit() - } + } else + supportFragmentManager.beginTransaction().replace(binding.settingsContainer.id, fragment!!).addToBackStack(getString(getTitleOfPage(screen))).commit() return fragment } override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - if (supportFragmentManager.backStackEntryCount == 0) { - finish() - } else { + if (supportFragmentManager.backStackEntryCount == 0) finish() + else { val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager var view = currentFocus //If no view currently has focus, create a new one, just so we can grab a window token from it - if (view == null) { - view = View(this) - } + if (view == null) view = View(this) imm.hideSoftInputFromWindow(view.windowToken, 0) supportFragmentManager.popBackStack() } @@ -156,9 +145,7 @@ class PreferenceActivity : AppCompatActivity(), SearchPreferenceResultListener { fun onEventMainThread(event: FlowEvent.MessageEvent) { // Logd(FRAGMENT_TAG, "onEvent($event)") val s = Snackbar.make(binding.root, event.message, Snackbar.LENGTH_LONG) - if (event.action != null) { - s.setAction(event.actionText) { event.action.accept(this) } - } + if (event.action != null) s.setAction(event.actionText) { event.action.accept(this) } s.show() } @@ -167,35 +154,16 @@ class PreferenceActivity : AppCompatActivity(), SearchPreferenceResultListener { const val OPEN_AUTO_DOWNLOAD_SETTINGS: String = "OpenAutoDownloadSettings" @JvmStatic fun getTitleOfPage(preferences: Int): Int { - when (preferences) { - R.xml.preferences_downloads -> { - return R.string.downloads_pref - } - R.xml.preferences_autodownload -> { - return R.string.pref_automatic_download_title - } - R.xml.preferences_playback -> { - return R.string.playback_pref - } - R.xml.preferences_import_export -> { - return R.string.import_export_pref - } - R.xml.preferences_user_interface -> { - return R.string.user_interface_label - } - R.xml.preferences_synchronization -> { - return R.string.synchronization_pref - } - R.xml.preferences_notifications -> { - return R.string.notification_pref_fragment - } -// R.xml.feed_settings -> { -// return R.string.feed_settings_label -// } - R.xml.preferences_swipe -> { - return R.string.swipeactions_label - } - else -> return R.string.settings_label + return when (preferences) { + R.xml.preferences_downloads -> R.string.downloads_pref + R.xml.preferences_autodownload -> R.string.pref_automatic_download_title + R.xml.preferences_playback -> R.string.playback_pref + R.xml.preferences_import_export -> R.string.import_export_pref + R.xml.preferences_user_interface -> R.string.user_interface_label + R.xml.preferences_synchronization -> R.string.synchronization_pref + R.xml.preferences_notifications -> R.string.notification_pref_fragment + R.xml.preferences_swipe -> R.string.swipeactions_label + else -> R.string.settings_label } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt index 3802d8d4..9321903d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt @@ -87,7 +87,7 @@ class SelectSubscriptionActivity : AppCompatActivity() { .setIcon(icon) .build() - setResult(Activity.RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this, shortcut)) + setResult(RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this, shortcut)) finish() } @@ -127,11 +127,8 @@ class SelectSubscriptionActivity : AppCompatActivity() { val adapter: ArrayAdapter = ArrayAdapter(this@SelectSubscriptionActivity, R.layout.simple_list_item_multiple_choice_on_start, titles) binding.list.adapter = adapter } - } catch (e: Throwable) { - Log.e(TAG, Log.getStackTraceString(e)) - } + } catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) } } - } override fun onDestroy() { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt index e5afdaab..3ebecba6 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt @@ -35,7 +35,6 @@ import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter import ac.mdiq.podcini.ui.compose.ChaptersDialog import ac.mdiq.podcini.ui.compose.CustomTheme import ac.mdiq.podcini.ui.dialog.* -import ac.mdiq.podcini.ui.utils.PictureInPictureUtil import ac.mdiq.podcini.ui.utils.ShownotesCleaner import ac.mdiq.podcini.ui.view.ShownotesWebView import ac.mdiq.podcini.util.EventFlow @@ -48,10 +47,10 @@ import android.app.Dialog import android.content.DialogInterface import android.content.Intent import android.content.pm.ActivityInfo +import android.content.pm.PackageManager import android.graphics.PixelFormat import android.graphics.drawable.ColorDrawable import android.media.AudioManager -import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper @@ -84,12 +83,7 @@ import kotlinx.coroutines.withContext import kotlin.math.max import kotlin.math.min -/** - * Activity for playing video files. - */ - class VideoplayerActivity : CastEnabledActivity() { - private var _binding: VideoplayerActivityBinding? = null private val binding get() = _binding!! private lateinit var videoEpisodeFragment: VideoEpisodeFragment @@ -185,10 +179,10 @@ class VideoplayerActivity : CastEnabledActivity() { } public override fun onUserLeaveHint() { - if (!PictureInPictureUtil.isInPictureInPictureMode(this)) compatEnterPictureInPicture() + super.onUserLeaveHint() + if (!isInPictureInPictureMode()) compatEnterPictureInPicture() } - override fun onStart() { super.onStart() procFlowEvents() @@ -249,15 +243,15 @@ class VideoplayerActivity : CastEnabledActivity() { val media = curMedia val isEpisodeMedia = (media is EpisodeMedia) - menu.findItem(R.id.show_home_reader_view).setVisible(false) - menu.findItem(R.id.open_feed_item).setVisible(isEpisodeMedia) // EpisodeMedia implies it belongs to a Feed + menu.findItem(R.id.show_home_reader_view).isVisible = false + menu.findItem(R.id.open_feed_item).isVisible = isEpisodeMedia // EpisodeMedia implies it belongs to a Feed val hasWebsiteLink = getWebsiteLinkWithFallback(media) != null - menu.findItem(R.id.visit_website_item).setVisible(hasWebsiteLink) + menu.findItem(R.id.visit_website_item).isVisible = hasWebsiteLink - val isItemAndHasLink = isEpisodeMedia && hasLinkToShare((media as EpisodeMedia).episodeOrFetch()) + val isItemAndHasLink = isEpisodeMedia && hasLinkToShare(media.episodeOrFetch()) val isItemHasDownloadLink = isEpisodeMedia && (media as EpisodeMedia?)?.downloadUrl != null - menu.findItem(R.id.share_item).setVisible(hasWebsiteLink || isItemAndHasLink || isItemHasDownloadLink) + menu.findItem(R.id.share_item).isVisible = hasWebsiteLink || isItemAndHasLink || isItemHasDownloadLink // menu.findItem(R.id.add_to_favorites_item).setVisible(false) // menu.findItem(R.id.remove_from_favorites_item).setVisible(false) @@ -266,13 +260,13 @@ class VideoplayerActivity : CastEnabledActivity() { // menu.findItem(R.id.remove_from_favorites_item).setVisible(videoEpisodeFragment.isFavorite) // } - menu.findItem(R.id.set_sleeptimer_item).setVisible(!isSleepTimerActive()) - menu.findItem(R.id.disable_sleeptimer_item).setVisible(isSleepTimerActive()) - menu.findItem(R.id.player_switch_to_audio_only).setVisible(true) + menu.findItem(R.id.set_sleeptimer_item).isVisible = !isSleepTimerActive() + menu.findItem(R.id.disable_sleeptimer_item).isVisible = isSleepTimerActive() + menu.findItem(R.id.player_switch_to_audio_only).isVisible = true - menu.findItem(R.id.audio_controls).setVisible(audioTracks.size >= 2) - menu.findItem(R.id.playback_speed).setVisible(true) - menu.findItem(R.id.player_show_chapters).setVisible(true) + menu.findItem(R.id.audio_controls).isVisible = audioTracks.size >= 2 + menu.findItem(R.id.playback_speed).isVisible = true + menu.findItem(R.id.player_show_chapters).isVisible = true if (videoMode == VideoMode.WINDOW_VIEW) { // menu.findItem(R.id.add_to_favorites_item).setShowAsAction(SHOW_AS_ACTION_NEVER) @@ -360,7 +354,7 @@ class VideoplayerActivity : CastEnabledActivity() { } private fun compatEnterPictureInPicture() { - if (PictureInPictureUtil.supportsPictureInPicture(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) { if (videoMode == VideoMode.FULL_SCREEN_VIEW) supportActionBar?.hide() videoEpisodeFragment.hideVideoControls(false) enterPictureInPictureMode() @@ -418,6 +412,16 @@ class VideoplayerActivity : CastEnabledActivity() { return super.onKeyUp(keyCode, event) } +// fun supportsPictureInPicture(): Boolean { +//// val packageManager = activity.packageManager +// return packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) +// } + + override fun isInPictureInPictureMode(): Boolean { + return if (packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) super.isInPictureInPictureMode + else false + } + class PlaybackControlsDialog : DialogFragment() { private lateinit var dialog: AlertDialog private var _binding: AudioControlsBinding? = null @@ -474,7 +478,6 @@ class VideoplayerActivity : CastEnabledActivity() { } } - class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener { private var _binding: VideoEpisodeFragmentBinding? = null private val binding get() = _binding!! @@ -498,7 +501,7 @@ class VideoplayerActivity : CastEnabledActivity() { private val onVideoviewTouched = View.OnTouchListener { v: View, event: MotionEvent -> Logd(TAG, "onVideoviewTouched ${event.action}") if (event.action != MotionEvent.ACTION_DOWN) return@OnTouchListener false - if (PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) return@OnTouchListener true + if (requireActivity().isInPictureInPictureMode()) return@OnTouchListener true videoControlsHider.removeCallbacks(hideVideoControls) Logd(TAG, "onVideoviewTouched $videoControlsVisible ${System.currentTimeMillis() - lastScreenTap}") if (System.currentTimeMillis() - lastScreenTap < 300) { @@ -546,7 +549,6 @@ class VideoplayerActivity : CastEnabledActivity() { videoControlsVisible = false } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { super.onCreateView(inflater, container, savedInstanceState) @@ -559,7 +561,6 @@ class VideoplayerActivity : CastEnabledActivity() { return root } - private fun newStatusHandler(): ServiceStatusHandler { return object : ServiceStatusHandler(requireActivity()) { override fun updatePlayButton(showPlay: Boolean) { @@ -583,7 +584,6 @@ class VideoplayerActivity : CastEnabledActivity() { } } } - override fun onStart() { super.onStart() @@ -591,11 +591,10 @@ class VideoplayerActivity : CastEnabledActivity() { procFlowEvents() } - override fun onStop() { super.onStop() cancelFlowEvents() - if (!PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) videoControlsHider.removeCallbacks(hideVideoControls) + if (!requireActivity().isInPictureInPictureMode()) videoControlsHider.removeCallbacks(hideVideoControls) // Controller released; we will not receive buffering updates binding.progressBar.visibility = View.GONE } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/WidgetConfigActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/WidgetConfigActivity.kt index 12f8839b..c9db5fdd 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/WidgetConfigActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/WidgetConfigActivity.kt @@ -1,16 +1,5 @@ package ac.mdiq.podcini.ui.activity -import android.appwidget.AppWidgetManager -import android.content.Intent -import android.graphics.Color -import android.os.Build -import android.os.Bundle -import android.view.View -import android.widget.CheckBox -import android.widget.SeekBar -import android.widget.SeekBar.OnSeekBarChangeListener -import android.widget.TextView -import androidx.appcompat.app.AppCompatActivity import ac.mdiq.podcini.R import ac.mdiq.podcini.databinding.ActivityWidgetConfigBinding import ac.mdiq.podcini.databinding.PlayerWidgetBinding @@ -19,6 +8,17 @@ import ac.mdiq.podcini.receiver.PlayerWidget import ac.mdiq.podcini.receiver.PlayerWidget.Companion.prefs import ac.mdiq.podcini.ui.widget.WidgetUpdaterWorker import ac.mdiq.podcini.util.Logd +import android.appwidget.AppWidgetManager +import android.content.Intent +import android.graphics.Color +import android.os.Bundle +import android.view.View +import android.widget.CheckBox +import android.widget.SeekBar +import android.widget.SeekBar.OnSeekBarChangeListener +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import kotlin.math.roundToInt class WidgetConfigActivity : AppCompatActivity() { @@ -96,18 +96,15 @@ class WidgetConfigActivity : AppCompatActivity() { private fun setInitialState() { PlayerWidget.getSharedPrefs(this) - // val prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE) ckPlaybackSpeed.isChecked = prefs!!.getBoolean(PlayerWidget.Prefs.widget_playback_speed.name + appWidgetId, true) ckRewind.isChecked = prefs!!.getBoolean(PlayerWidget.Prefs.widget_rewind.name + appWidgetId, true) ckFastForward.isChecked = prefs!!.getBoolean(PlayerWidget.Prefs.widget_fast_forward.name + appWidgetId, true) ckSkip.isChecked = prefs!!.getBoolean(PlayerWidget.Prefs.widget_skip.name + appWidgetId, true) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - val color = prefs!!.getInt(PlayerWidget.Prefs.widget_color.name + appWidgetId, PlayerWidget.DEFAULT_COLOR) - val opacity = Color.alpha(color) * 100 / 0xFF - opacitySeekBar.setProgress(opacity, false) - } + val color = prefs!!.getInt(PlayerWidget.Prefs.widget_color.name + appWidgetId, PlayerWidget.DEFAULT_COLOR) + val opacity = Color.alpha(color) * 100 / 0xFF + opacitySeekBar.setProgress(opacity, false) displayPreviewPanel() } @@ -142,6 +139,6 @@ class WidgetConfigActivity : AppCompatActivity() { } private fun getColorWithAlpha(color: Int, opacity: Int): Int { - return Math.round(0xFF * (0.01 * opacity)).toInt() * 0x1000000 + (color and 0xffffff) + return (0xFF * (0.01 * opacity)).roundToInt() * 0x1000000 + (color and 0xffffff) } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/ChaptersDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/ChaptersDialog.kt index bb298613..6746698e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/ChaptersDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/ChaptersDialog.kt @@ -24,7 +24,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.font.FontWeight @@ -58,7 +57,7 @@ fun ChaptersDialog(media: Playable, onDismissRequest: () -> Unit) { Text(ch.title ?: "No title", color = textColor, fontWeight = FontWeight.Bold) // Text(ch.link?: "") val duration = if (index + 1 < chapters.size) chapters[index + 1].start - ch.start - else (media.getDuration() ?: 0) - ch.start + else media.getDuration() - ch.start Text(stringResource(R.string.chapter_duration0) + getDurationStringLocalized(LocalContext.current, duration), color = textColor) } val playRes = if (index == currentChapterIndex) R.drawable.ic_replay else R.drawable.ic_play_48dp diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Composables.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Composables.kt index d438e926..7148c519 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Composables.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Composables.kt @@ -6,8 +6,6 @@ import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -91,9 +89,8 @@ fun Spinner(items: List, selectedItem: String, modifier: Modifier = Modi @Composable fun Spinner(items: List, selectedIndex: Int, modifier: Modifier = Modifier, onItemSelected: (Int) -> Unit) { var expanded by remember { mutableStateOf(false) } - var currentSelectedIndex by remember { mutableStateOf(selectedIndex) } ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) { - BasicTextField(readOnly = true, value = items.getOrNull(currentSelectedIndex) ?: "Select Item", onValueChange = { }, + BasicTextField(readOnly = true, value = items.getOrNull(selectedIndex) ?: "Select Item", onValueChange = { }, textStyle = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.bodyLarge.fontSize, fontWeight = FontWeight.Bold), modifier = modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable, true), // Material3 requirement decorationBox = { innerTextField -> @@ -106,7 +103,6 @@ fun Spinner(items: List, selectedIndex: Int, modifier: Modifier = Modifi for (i in items.indices) { DropdownMenuItem(text = { Text(items[i]) }, onClick = { - currentSelectedIndex = i onItemSelected(i) expanded = false } @@ -132,36 +128,6 @@ fun CustomToast(message: String, durationMillis: Long = 2000L, onDismiss: () -> } } -@Composable -fun AutoCompleteTextView(suggestions: List, onItemSelected: (String) -> Unit, modifier: Modifier = Modifier) { - var text by remember { mutableStateOf("") } - var expanded by remember { mutableStateOf(false) } - - Column(modifier = modifier) { - TextField(value = text, - onValueChange = { text = it }, - label = { Text("Search") }, - trailingIcon = { - IconButton(onClick = { expanded = !expanded }) { - Icon(imageVector = Icons.Filled.ArrowDropDown, contentDescription = "Expand") - } - } - ) - - if (expanded) { - DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { - suggestions.forEach { suggestion -> - DropdownMenuItem(text = {Text(suggestion)}, onClick = { - onItemSelected(suggestion) - text = suggestion - expanded = false - }) - } - } - } - } -} - @Composable fun LargeTextEditingDialog(textState: TextFieldValue, onTextChange: (TextFieldValue) -> Unit, onDismissRequest: () -> Unit, onSave: (String) -> Unit) { Dialog(onDismissRequest = { onDismissRequest() }, properties = DialogProperties(usePlatformDefaultWidth = false)) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt index 7ac5f0c3..ef574d06 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt @@ -112,24 +112,16 @@ fun InforBar(text: MutableState, leftAction: MutableState, Logd("InforBar", "textState: ${text.value}") Row { Icon(imageVector = ImageVector.vectorResource(leftAction.value.getActionIcon()), tint = textColor, contentDescription = "left_action_icon", - modifier = Modifier - .width(24.dp) - .height(24.dp) - .clickable(onClick = actionConfig)) - Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_arrow_left_alt_24), tint = textColor, contentDescription = "left_arrow", modifier = Modifier - .width(24.dp) - .height(24.dp)) + modifier = Modifier.width(24.dp).height(24.dp).clickable(onClick = actionConfig)) + Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_arrow_left_alt_24), tint = textColor, contentDescription = "left_arrow", + modifier = Modifier.width(24.dp).height(24.dp)) Spacer(modifier = Modifier.weight(1f)) Text(text.value, color = textColor, style = MaterialTheme.typography.bodyMedium) Spacer(modifier = Modifier.weight(1f)) - Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_arrow_right_alt_24), tint = textColor, contentDescription = "right_arrow", modifier = Modifier - .width(24.dp) - .height(24.dp)) + Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_arrow_right_alt_24), tint = textColor, contentDescription = "right_arrow", + modifier = Modifier.width(24.dp).height(24.dp)) Icon(imageVector = ImageVector.vectorResource(rightAction.value.getActionIcon()), tint = textColor, contentDescription = "right_action_icon", - modifier = Modifier - .width(24.dp) - .height(24.dp) - .clickable(onClick = actionConfig)) + modifier = Modifier.width(24.dp).height(24.dp).clickable(onClick = actionConfig)) } } @@ -213,20 +205,6 @@ class EpisodeVM(var episode: Episode) { } } } - -// override fun equals(other: Any?): Boolean { -// if (this === other) return true -// if (javaClass != other?.javaClass) return false -// other as EpisodeVM -// -// if (episode.id != other.episode.id) return false -// return true -// } -// -// override fun hashCode(): Int { -// var result = episode.id.hashCode() -// return result -// } } @Composable @@ -333,9 +311,7 @@ fun PutToQueueDialog(selected: List, onDismissRequest: () -> Unit) { if (removeChecked) { val toRemove = mutableSetOf() val toRemoveCur = mutableListOf() - selected.forEach { e -> - if (curQueue.contains(e)) toRemoveCur.add(e) - } + selected.forEach { e -> if (curQueue.contains(e)) toRemoveCur.add(e) } selected.forEach { e -> for (q in queues) { if (q.contains(e)) { @@ -347,13 +323,9 @@ fun PutToQueueDialog(selected: List, onDismissRequest: () -> Unit) { if (toRemove.isNotEmpty()) runBlocking { removeFromAllQueuesQuiet(toRemove.toList()) } if (toRemoveCur.isNotEmpty()) EventFlow.postEvent(FlowEvent.QueueEvent.removed(toRemoveCur)) } - selected.forEach { e -> - runBlocking { addToQueueSync(e, toQueue) } - } + selected.forEach { e -> runBlocking { addToQueueSync(e, toQueue) } } onDismissRequest() - }) { - Text("Confirm") - } + }) { Text("Confirm") } } } } @@ -395,11 +367,7 @@ fun ShelveDialog(selected: List, onDismissRequest: () -> Unit) { e_.media?.id = e_.id } else { val feed = realm.query(Feed::class).query("id == $0", e_.feedId).first().find() - if (feed != null) { - upsertBlk(feed) { - it.episodes.remove(e_) - } - } + if (feed != null) upsertBlk(feed) { it.episodes.remove(e_) } } upsertBlk(e_) { it.feed = toFeed @@ -407,13 +375,9 @@ fun ShelveDialog(selected: List, onDismissRequest: () -> Unit) { eList.add(it) } } - upsertBlk(toFeed!!) { - it.episodes.addAll(eList) - } + upsertBlk(toFeed!!) { it.episodes.addAll(eList) } onDismissRequest() - }) { - Text("Confirm") - } + }) { Text("Confirm") } } } } @@ -464,9 +428,7 @@ fun EraseEpisodesDialog(selected: List, feed: Feed?, onDismissRequest: } catch (e: Throwable) { Log.e("EraseEpisodesDialog", Log.getStackTraceString(e)) } } onDismissRequest() - }) { - Text("Confirm") - } + }) { Text("Confirm") } } } } @@ -490,9 +452,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList, feed: val showConfirmYoutubeDialog = remember { mutableStateOf(false) } val youtubeUrls = remember { mutableListOf() } - ConfirmAddYoutubeEpisode(youtubeUrls, showConfirmYoutubeDialog.value, onDismissRequest = { - showConfirmYoutubeDialog.value = false - }) + ConfirmAddYoutubeEpisode(youtubeUrls, showConfirmYoutubeDialog.value, onDismissRequest = { showConfirmYoutubeDialog.value = false }) var showChooseRatingDialog by remember { mutableStateOf(false) } if (showChooseRatingDialog) ChooseRatingDialog(selected) { showChooseRatingDialog = false } @@ -513,152 +473,133 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList, feed: fun EpisodeSpeedDial(modifier: Modifier = Modifier) { var isExpanded by remember { mutableStateOf(false) } val options = mutableListOf<@Composable () -> Unit>( - { Row(modifier = Modifier - .padding(horizontal = 16.dp) - .clickable { - isExpanded = false - selectMode = false - Logd(TAG, "ic_delete: ${selected.size}") - runOnIOScope { - for (item_ in selected) { - var item = item_ - if (!item.isDownloaded && item.feed?.isLocalFeed != true) continue - val media = item.media - if (media != null) { - val almostEnded = hasAlmostEnded(media) - if (almostEnded && item.playState < PlayState.PLAYED.code) item = setPlayStateSync(PlayState.PLAYED.code, item, almostEnded, false) - if (almostEnded) item = upsert(item) { it.media?.playbackCompletionDate = Date() } - deleteEpisodeMedia(activity, item) - } + { Row(modifier = Modifier.padding(horizontal = 16.dp).clickable { + isExpanded = false + selectMode = false + Logd(TAG, "ic_delete: ${selected.size}") + runOnIOScope { + for (item_ in selected) { + var item = item_ + if (!item.isDownloaded && item.feed?.isLocalFeed != true) continue + val media = item.media + if (media != null) { + val almostEnded = hasAlmostEnded(media) + if (almostEnded && item.playState < PlayState.PLAYED.code) item = setPlayStateSync(PlayState.PLAYED.code, item, almostEnded, false) + if (almostEnded) item = upsert(item) { it.media?.playbackCompletionDate = Date() } + deleteEpisodeMedia(activity, item) } } + } // LocalDeleteModal.deleteEpisodesWarnLocal(activity, selected) - }, verticalAlignment = Alignment.CenterVertically) { + }, verticalAlignment = Alignment.CenterVertically) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_delete), "Delete media") Text(stringResource(id = R.string.delete_episode_label)) } }, - { Row(modifier = Modifier - .padding(horizontal = 16.dp) - .clickable { - isExpanded = false - selectMode = false - Logd(TAG, "ic_download: ${selected.size}") - for (episode in selected) { - if (episode.media != null && episode.feed != null && !episode.feed!!.isLocalFeed) DownloadServiceInterface.get()?.download(activity, episode) - } - }, verticalAlignment = Alignment.CenterVertically) { + { Row(modifier = Modifier.padding(horizontal = 16.dp).clickable { + isExpanded = false + selectMode = false + Logd(TAG, "ic_download: ${selected.size}") + for (episode in selected) { + if (episode.media != null && episode.feed != null && !episode.feed!!.isLocalFeed) DownloadServiceInterface.get()?.download(activity, episode) + } + }, verticalAlignment = Alignment.CenterVertically) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_download), "Download") Text(stringResource(id = R.string.download_label)) } }, - { Row(modifier = Modifier - .padding(horizontal = 16.dp) - .clickable { - showPlayStateDialog = true - isExpanded = false - selectMode = false - Logd(TAG, "ic_mark_played: ${selected.size}") + { Row(modifier = Modifier.padding(horizontal = 16.dp).clickable { + showPlayStateDialog = true + isExpanded = false + selectMode = false + Logd(TAG, "ic_mark_played: ${selected.size}") // setPlayState(PlayState.UNSPECIFIED.code, false, *selected.toTypedArray()) - }, verticalAlignment = Alignment.CenterVertically) { + }, verticalAlignment = Alignment.CenterVertically) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_mark_played), "Toggle played state") Text(stringResource(id = R.string.set_play_state_label)) } }, - { Row(modifier = Modifier - .padding(horizontal = 16.dp) - .clickable { - isExpanded = false - selectMode = false - Logd(TAG, "ic_playlist_remove: ${selected.size}") - runOnIOScope { - for (item_ in selected) { - var item = item_ - val media = item.media - if (media != null) { - val almostEnded = hasAlmostEnded(media) - if (almostEnded && item.playState < PlayState.PLAYED.code) item = setPlayStateSync(PlayState.PLAYED.code, item, almostEnded, false) - if (almostEnded) item = upsert(item) { it.media?.playbackCompletionDate = Date() } - } - if (item.playState < PlayState.SKIPPED.code) setPlayState(PlayState.SKIPPED.code, false, item) + { Row(modifier = Modifier.padding(horizontal = 16.dp).clickable { + isExpanded = false + selectMode = false + Logd(TAG, "ic_playlist_remove: ${selected.size}") + runOnIOScope { + for (item_ in selected) { + var item = item_ + val media = item.media + if (media != null) { + val almostEnded = hasAlmostEnded(media) + if (almostEnded && item.playState < PlayState.PLAYED.code) item = setPlayStateSync(PlayState.PLAYED.code, item, almostEnded, false) + if (almostEnded) item = upsert(item) { it.media?.playbackCompletionDate = Date() } } - removeFromQueueSync(curQueue, *selected.toTypedArray()) -// removeFromQueue(*selected.toTypedArray()) + if (item.playState < PlayState.SKIPPED.code) setPlayState(PlayState.SKIPPED.code, false, item) } - }, verticalAlignment = Alignment.CenterVertically) { + removeFromQueueSync(curQueue, *selected.toTypedArray()) +// removeFromQueue(*selected.toTypedArray()) + } + }, verticalAlignment = Alignment.CenterVertically) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_remove), "Remove from active queue") Text(stringResource(id = R.string.remove_from_queue_label)) } }, - { Row(modifier = Modifier - .padding(horizontal = 16.dp) - .clickable { - isExpanded = false - selectMode = false - Logd(TAG, "ic_playlist_play: ${selected.size}") - Queues.addToQueue(*selected.toTypedArray()) - }, verticalAlignment = Alignment.CenterVertically) { + { Row(modifier = Modifier.padding(horizontal = 16.dp).clickable { + isExpanded = false + selectMode = false + Logd(TAG, "ic_playlist_play: ${selected.size}") + Queues.addToQueue(*selected.toTypedArray()) + }, verticalAlignment = Alignment.CenterVertically) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "Add to active queue") Text(stringResource(id = R.string.add_to_queue_label)) } }, - { Row(modifier = Modifier - .padding(horizontal = 16.dp) - .clickable { - isExpanded = false - selectMode = false - Logd(TAG, "shelve_label: ${selected.size}") - showShelveDialog = true - }, verticalAlignment = Alignment.CenterVertically) { + { Row(modifier = Modifier.padding(horizontal = 16.dp).clickable { + isExpanded = false + selectMode = false + Logd(TAG, "shelve_label: ${selected.size}") + showShelveDialog = true + }, verticalAlignment = Alignment.CenterVertically) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.baseline_shelves_24), "Shelve") Text(stringResource(id = R.string.shelve_label)) } }, - { Row(modifier = Modifier - .padding(horizontal = 16.dp) - .clickable { - isExpanded = false - selectMode = false - Logd(TAG, "ic_playlist_play: ${selected.size}") - showPutToQueueDialog = true - }, verticalAlignment = Alignment.CenterVertically) { + { Row(modifier = Modifier.padding(horizontal = 16.dp).clickable { + isExpanded = false + selectMode = false + Logd(TAG, "ic_playlist_play: ${selected.size}") + showPutToQueueDialog = true + }, verticalAlignment = Alignment.CenterVertically) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "Add to queue...") Text(stringResource(id = R.string.put_in_queue_label)) } }, - { Row(modifier = Modifier - .padding(horizontal = 16.dp) - .clickable { - selectMode = false - Logd(TAG, "ic_star: ${selected.size}") - showChooseRatingDialog = true - isExpanded = false - }, verticalAlignment = Alignment.CenterVertically) { + { Row(modifier = Modifier.padding(horizontal = 16.dp).clickable { + selectMode = false + Logd(TAG, "ic_star: ${selected.size}") + showChooseRatingDialog = true + isExpanded = false + }, verticalAlignment = Alignment.CenterVertically) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_star), "Set rating") Text(stringResource(id = R.string.set_rating_label)) } }, ) if (selected.isNotEmpty() && selected[0].isRemote.value) options.add { - Row(modifier = Modifier.padding(horizontal = 16.dp) - .clickable { - isExpanded = false - selectMode = false - Logd(TAG, "reserve: ${selected.size}") - CoroutineScope(Dispatchers.IO).launch { - youtubeUrls.clear() - for (e in selected) { - Logd(TAG, "downloadUrl: ${e.media?.downloadUrl}") - val url = URL(e.media?.downloadUrl ?: "") - if ((isYoutubeURL(url) && url.path.startsWith("/watch")) || isYoutubeServiceURL(url)) { - youtubeUrls.add(e.media!!.downloadUrl!!) - } else addToMiscSyndicate(e) - } - Logd(TAG, "youtubeUrls: ${youtubeUrls.size}") - withContext(Dispatchers.Main) { - showConfirmYoutubeDialog.value = youtubeUrls.isNotEmpty() - } + Row(modifier = Modifier.padding(horizontal = 16.dp).clickable { + isExpanded = false + selectMode = false + Logd(TAG, "reserve: ${selected.size}") + CoroutineScope(Dispatchers.IO).launch { + youtubeUrls.clear() + for (e in selected) { + Logd(TAG, "downloadUrl: ${e.media?.downloadUrl}") + val url = URL(e.media?.downloadUrl ?: "") + if ((isYoutubeURL(url) && url.path.startsWith("/watch")) || isYoutubeServiceURL(url)) { + youtubeUrls.add(e.media!!.downloadUrl!!) + } else addToMiscSyndicate(e) } - }, verticalAlignment = Alignment.CenterVertically) { + Logd(TAG, "youtubeUrls: ${youtubeUrls.size}") + withContext(Dispatchers.Main) { + showConfirmYoutubeDialog.value = youtubeUrls.isNotEmpty() + } + } + }, verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Filled.AddCircle, "Reserve episodes") Text(stringResource(id = R.string.reserve_episodes_label)) } } if (feed != null && feed.id <= MAX_SYNTHETIC_ID) { options.add { - Row(modifier = Modifier - .padding(horizontal = 16.dp) - .clickable { - isExpanded = false - selectMode = false - showEraseDialog = true - Logd(TAG, "reserve: ${selected.size}") - }, verticalAlignment = Alignment.CenterVertically) { + Row(modifier = Modifier.padding(horizontal = 16.dp).clickable { + isExpanded = false + selectMode = false + showEraseDialog = true + Logd(TAG, "reserve: ${selected.size}") + }, verticalAlignment = Alignment.CenterVertically) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.baseline_delete_forever_24), "Erase episodes") Text(stringResource(id = R.string.erase_episodes_label)) } @@ -668,14 +609,9 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList, feed: val scrollState = rememberScrollState() Column(modifier = modifier.verticalScroll(scrollState), verticalArrangement = Arrangement.Bottom) { if (isExpanded) options.forEachIndexed { _, button -> - FloatingActionButton(modifier = Modifier - .padding(start = 4.dp, bottom = 6.dp) - .height(40.dp), - containerColor = Color.LightGray, - onClick = {}) { button() } + FloatingActionButton(modifier = Modifier.padding(start = 4.dp, bottom = 6.dp).height(40.dp), containerColor = Color.LightGray, onClick = {}) { button() } } - FloatingActionButton(containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.secondary, + FloatingActionButton(containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.secondary, onClick = { isExpanded = !isExpanded }) { Icon(Icons.Filled.Edit, "Edit") } } } @@ -807,8 +743,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList, feed: if (isDownloading() && vm.dlPercent >= 0) CircularProgressIndicator(progress = { 0.01f * vm.dlPercent }, strokeWidth = 4.dp, color = textColor, modifier = Modifier.width(30.dp).height(35.dp)) } - if (vm.showAltActionsDialog) actionButton.AltActionsDialog(activity, vm.showAltActionsDialog, - onDismiss = { vm.showAltActionsDialog = false }) + if (vm.showAltActionsDialog) actionButton.AltActionsDialog(activity, vm.showAltActionsDialog, onDismiss = { vm.showAltActionsDialog = false }) } } @@ -1037,9 +972,7 @@ fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: Mutable } onFilterChanged(filterValues) }, - ) { - Text(text = stringResource(item.values[0].displayName), color = textColor) - } + ) { Text(text = stringResource(item.values[0].displayName), color = textColor) } Spacer(Modifier.weight(0.1f)) OutlinedButton( modifier = Modifier.padding(0.dp), border = BorderStroke(2.dp, if (selectedIndex != 1) textColor else Color.Green), @@ -1055,9 +988,7 @@ fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: Mutable } onFilterChanged(filterValues) }, - ) { - Text(text = stringResource(item.values[1].displayName), color = textColor) - } + ) { Text(text = stringResource(item.values[1].displayName), color = textColor) } Spacer(Modifier.weight(0.5f)) } } else { @@ -1133,9 +1064,7 @@ fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: Mutable else filterValues.remove(item.values[index].filterId) onFilterChanged(filterValues) }, - ) { - Text(text = stringResource(item.values[index].displayName), maxLines = 1, color = textColor) - } + ) { Text(text = stringResource(item.values[index].displayName), maxLines = 1, color = textColor) } } } } @@ -1145,15 +1074,9 @@ fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: Mutable Button(onClick = { selectNone = true onFilterChanged(setOf("")) - }) { - Text(stringResource(R.string.reset)) - } + }) { Text(stringResource(R.string.reset)) } Spacer(Modifier.weight(0.4f)) - Button(onClick = { - onDismissRequest() - }) { - Text(stringResource(R.string.close_label)) - } + Button(onClick = { onDismissRequest() }) { Text(stringResource(R.string.close_label)) } Spacer(Modifier.weight(0.3f)) } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Feeds.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Feeds.kt index 3d46bd7b..3b5edd42 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Feeds.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Feeds.kt @@ -244,7 +244,7 @@ fun RenameOrCreateSyntheticFeed(feed_: Feed? = null, onDismissRequest: () -> Uni Row { Button({ onDismissRequest() }) { Text(stringResource(R.string.cancel_label)) } Button({ - val feed = if (feed_ == null) createSynthetic(0, name, hasVideo) else feed_ + val feed = feed_ ?: createSynthetic(0, name, hasVideo) if (feed_ == null) { feed.type = if (isYoutube) Feed.FeedType.YOUTUBE.name else Feed.FeedType.RSS.name if (hasVideo) feed.preferences!!.videoModePolicy = VideoMode.WINDOW_VIEW diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt index 7b6d08dc..cc7fd305 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt @@ -8,7 +8,6 @@ import ac.mdiq.podcini.storage.database.Episodes.getEpisodesCount import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.EpisodeFilter import ac.mdiq.podcini.storage.model.EpisodeSortOrder -import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog import ac.mdiq.podcini.util.EventFlow import ac.mdiq.podcini.util.FlowEvent diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt index 422ce67d..0a2a6b1a 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt @@ -97,7 +97,7 @@ import java.text.NumberFormat import kotlin.math.max class AudioPlayerFragment : Fragment() { - val prefs: SharedPreferences by lazy { requireContext().getSharedPreferences(PREF, Context.MODE_PRIVATE) } + val prefs: SharedPreferences by lazy { requireContext().getSharedPreferences("AudioPlayerFragmentPrefs", Context.MODE_PRIVATE) } private var isCollapsed by mutableStateOf(true) @@ -999,13 +999,7 @@ class AudioPlayerFragment : Fragment() { val TAG = AudioPlayerFragment::class.simpleName ?: "Anonymous" var media3Controller: MediaController? = null - private const val PREF = "ItemDescriptionFragmentPrefs" private const val PREF_SCROLL_Y = "prefScrollY" private const val PREF_PLAYABLE_ID = "prefPlayableId" - -// var prefs: SharedPreferences? = null -// fun getSharedPrefs(context: Context) { -// if (prefs == null) prefs = context.getSharedPreferences(PREF, Context.MODE_PRIVATE) -// } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt index 865ba6ed..9a141d52 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt @@ -34,7 +34,6 @@ import ac.mdiq.podcini.util.IntentUtils import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.MiscFormatter.formatAbbrev import android.content.Context -import android.os.Build import android.os.Bundle import android.speech.tts.TextToSpeech import android.text.TextUtils @@ -90,9 +89,6 @@ import okhttp3.Request.Builder import java.io.File import java.util.* -/** - * Displays information about an Episode (FeedItem) and actions. - */ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { private var _binding: ComposeFragmentBinding? = null private val binding get() = _binding!! @@ -348,8 +344,9 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { if (episode == null) return false val notes = episode!!.description if (!notes.isNullOrEmpty()) { - val shareText = if (Build.VERSION.SDK_INT >= 24) HtmlCompat.fromHtml(notes, HtmlCompat.FROM_HTML_MODE_COMPACT).toString() - else HtmlCompat.fromHtml(notes, HtmlCompat.FROM_HTML_MODE_COMPACT).toString() +// val shareText = if (Build.VERSION.SDK_INT >= 24) HtmlCompat.fromHtml(notes, HtmlCompat.FROM_HTML_MODE_COMPACT).toString() +// else HtmlCompat.fromHtml(notes, HtmlCompat.FROM_HTML_MODE_COMPACT).toString() + val shareText = HtmlCompat.fromHtml(notes, HtmlCompat.FROM_HTML_MODE_COMPACT).toString() val context = requireContext() val intent = ShareCompat.IntentBuilder(context) .setType("text/plain") @@ -774,8 +771,8 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { if (textSpeech?.isVisible == true) { if (ttsPlaying) textSpeech.setIcon(R.drawable.ic_pause) else textSpeech.setIcon(R.drawable.ic_play_24dp) } - menu.findItem(R.id.share_notes)?.setVisible(readMode) - menu.findItem(R.id.switchJS)?.setVisible(!readMode) + menu.findItem(R.id.share_notes)?.isVisible = readMode + menu.findItem(R.id.switchJS)?.isVisible = !readMode val btn = menu.findItem(R.id.switch_home) if (readMode) btn?.setIcon(R.drawable.baseline_home_24) else btn?.setIcon(R.drawable.outline_home_24) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt index 712630a7..eb615f7f 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt @@ -62,9 +62,6 @@ import java.util.* import java.util.concurrent.ExecutionException import java.util.concurrent.Semaphore -/** - * Displays a list of FeedItems. - */ class FeedEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListener { private var _binding: ComposeFragmentBinding? = null @@ -384,10 +381,10 @@ import java.util.concurrent.Semaphore private fun updateToolbar() { if (feed == null) return - binding.toolbar.menu.findItem(R.id.visit_website_item).setVisible(feed!!.link != null) - binding.toolbar.menu.findItem(R.id.refresh_complete_item).setVisible(feed!!.isPaged) - if (StringUtils.isBlank(feed!!.link)) binding.toolbar.menu.findItem(R.id.visit_website_item).setVisible(false) - if (feed!!.isLocalFeed) binding.toolbar.menu.findItem(R.id.share_feed).setVisible(false) + binding.toolbar.menu.findItem(R.id.visit_website_item).isVisible = feed!!.link != null + binding.toolbar.menu.findItem(R.id.refresh_complete_item).isVisible = feed!!.isPaged + if (StringUtils.isBlank(feed!!.link)) binding.toolbar.menu.findItem(R.id.visit_website_item).isVisible = false + if (feed!!.isLocalFeed) binding.toolbar.menu.findItem(R.id.share_feed).isVisible = false } // override fun onConfigurationChanged(newConfig: Configuration) { @@ -451,20 +448,21 @@ import java.util.concurrent.Semaphore return true } + // TODO: not really needed private fun onQueueEvent(event: FlowEvent.QueueEvent) { if (feed == null || episodes.isEmpty()) return - var i = 0 - val size: Int = event.episodes.size - while (i < size) { - val item = event.episodes[i++] - if (item.feedId != feed!!.id) continue - val pos: Int = ieMap[item.id] ?: -1 - if (pos >= 0) { -// episodes[pos].inQueueState.value = event.inQueue() -// queueChanged++ - } - break - } +// var i = 0 +// val size: Int = event.episodes.size +// while (i < size) { +// val item = event.episodes[i++] +// if (item.feedId != feed!!.id) continue +// val pos: Int = ieMap[item.id] ?: -1 +// if (pos >= 0) { +//// episodes[pos].inQueueState.value = event.inQueue() +//// queueChanged++ +// } +// break +// } } private fun onPlayEvent(event: FlowEvent.PlayEvent) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt index 3e188449..e0524bec 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt @@ -318,11 +318,10 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { } private fun refreshToolbarState() { - toolbar.menu?.findItem(R.id.reconnect_local_folder)?.setVisible(feed.isLocalFeed) - toolbar.menu?.findItem(R.id.share_item)?.setVisible(!feed.isLocalFeed) - toolbar.menu?.findItem(R.id.visit_website_item) - ?.setVisible(feed.link != null && IntentUtils.isCallable(requireContext(), Intent(Intent.ACTION_VIEW, Uri.parse(feed.link)))) - toolbar.menu?.findItem(R.id.edit_feed_url_item)?.setVisible(!feed.isLocalFeed) + toolbar.menu?.findItem(R.id.reconnect_local_folder)?.isVisible = feed.isLocalFeed + toolbar.menu?.findItem(R.id.share_item)?.isVisible = !feed.isLocalFeed + toolbar.menu?.findItem(R.id.visit_website_item)?.isVisible = feed.link != null && IntentUtils.isCallable(requireContext(), Intent(Intent.ACTION_VIEW, Uri.parse(feed.link))) + toolbar.menu?.findItem(R.id.edit_feed_url_item)?.isVisible = !feed.isLocalFeed } override fun onMenuItemClick(item: MenuItem): Boolean { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt index 8d6c18d6..bec674d3 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt @@ -104,7 +104,7 @@ class FeedSettingsFragment : Fragment() { Spacer(modifier = Modifier.width(20.dp)) Text(text = stringResource(R.string.keep_updated), style = MaterialTheme.typography.titleLarge, color = textColor) Spacer(modifier = Modifier.weight(1f)) - var checked by remember { mutableStateOf(feed?.preferences?.keepUpdated ?: true) } + var checked by remember { mutableStateOf(feed?.preferences?.keepUpdated != false) } Switch(checked = checked, modifier = Modifier.height(24.dp), onCheckedChange = { checked = it @@ -138,7 +138,7 @@ class FeedSettingsFragment : Fragment() { Spacer(modifier = Modifier.width(20.dp)) Text(text = stringResource(R.string.pref_stream_over_download_title), style = MaterialTheme.typography.titleLarge, color = textColor) Spacer(modifier = Modifier.weight(1f)) - var checked by remember { mutableStateOf(feed?.preferences?.prefStreamOverDownload ?: false) } + var checked by remember { mutableStateOf(feed?.preferences?.prefStreamOverDownload == true) } Switch(checked = checked, modifier = Modifier.height(24.dp), onCheckedChange = { checked = it @@ -213,7 +213,7 @@ class FeedSettingsFragment : Fragment() { Spacer(modifier = Modifier.width(20.dp)) Text(text = stringResource(R.string.audo_add_new_queue), style = MaterialTheme.typography.titleLarge, color = textColor) Spacer(modifier = Modifier.weight(1f)) - var checked by remember { mutableStateOf(feed?.preferences?.autoAddNewToQueue ?: true) } + var checked by remember { mutableStateOf(feed?.preferences?.autoAddNewToQueue != false) } Switch(checked = checked, modifier = Modifier.height(24.dp), onCheckedChange = { checked = it @@ -302,7 +302,7 @@ class FeedSettingsFragment : Fragment() { } if (isEnableAutodownload && feed?.type != Feed.FeedType.YOUTUBE.name) { // auto download - var audoDownloadChecked by remember { mutableStateOf(feed?.preferences?.autoDownload ?: false) } + var audoDownloadChecked by remember { mutableStateOf(feed?.preferences?.autoDownload == true) } Column { Row(Modifier.fillMaxWidth()) { Text(text = stringResource(R.string.auto_download_label), style = MaterialTheme.typography.titleLarge, color = textColor) @@ -341,7 +341,7 @@ class FeedSettingsFragment : Fragment() { Row(Modifier.fillMaxWidth()) { Text(text = stringResource(R.string.pref_auto_download_counting_played_title), style = MaterialTheme.typography.titleLarge, color = textColor) Spacer(modifier = Modifier.weight(1f)) - var checked by remember { mutableStateOf(feed?.preferences?.countingPlayed ?: true) } + var checked by remember { mutableStateOf(feed?.preferences?.countingPlayed != false) } Switch(checked = checked, modifier = Modifier.height(24.dp), onCheckedChange = { checked = it @@ -800,9 +800,7 @@ class FeedSettingsFragment : Fragment() { .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> val newSpeed = if (binding.useGlobalCheckbox.isChecked) FeedPreferences.SPEED_USE_GLOBAL else binding.seekBar.currentSpeed - feed = upsertBlk(feed!!) { - it.preferences?.playSpeed = newSpeed - } + feed = upsertBlk(feed!!) { it.preferences?.playSpeed = newSpeed } } .setNegativeButton(R.string.cancel_label, null) .create() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt index 39841a61..8e0729fc 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/HistoryFragment.kt @@ -96,9 +96,9 @@ class HistoryFragment : BaseEpisodesFragment() { override fun updateToolbar() { // Not calling super, as we do not have a refresh button that could be updated - toolbar.menu.findItem(R.id.episodes_sort).setVisible(episodes.isNotEmpty()) - toolbar.menu.findItem(R.id.filter_items).setVisible(episodes.isNotEmpty()) - toolbar.menu.findItem(R.id.clear_history_item).setVisible(episodes.isNotEmpty()) + toolbar.menu.findItem(R.id.episodes_sort).isVisible = episodes.isNotEmpty() + toolbar.menu.findItem(R.id.filter_items).isVisible = episodes.isNotEmpty() + toolbar.menu.findItem(R.id.clear_history_item).isVisible = episodes.isNotEmpty() swipeActions.setFilter(getFilter()) var info = "${episodes.size} episodes" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/LogsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/LogsFragment.kt index 8c3d2caf..50113d21 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/LogsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/LogsFragment.kt @@ -5,11 +5,11 @@ import ac.mdiq.podcini.databinding.ComposeFragmentBinding import ac.mdiq.podcini.net.feed.FeedUpdateManager import ac.mdiq.podcini.storage.database.Feeds.getFeed import ac.mdiq.podcini.storage.database.Feeds.getFeedByTitleAndAuthor +import ac.mdiq.podcini.storage.database.LogsAndStats.DownloadResultComparator import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope import ac.mdiq.podcini.storage.model.* import ac.mdiq.podcini.storage.model.Rating.Companion.fromCode -import ac.mdiq.podcini.storage.utils.DownloadResultComparator import ac.mdiq.podcini.ui.actions.DownloadActionButton import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.activity.ShareReceiverActivity.Companion.receiveShared @@ -301,7 +301,7 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener { @Deprecated("Deprecated in Java") override fun onPrepareOptionsMenu(menu: Menu) { - menu.findItem(R.id.clear_logs_item).setVisible(shareLogs.isNotEmpty()) + menu.findItem(R.id.clear_logs_item).isVisible = shareLogs.isNotEmpty() } private fun clearAllLogs() { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt index bd6dba3e..ce5d4a41 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt @@ -59,6 +59,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.math.max +import kotlin.math.roundToInt class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener { val TAG = this::class.simpleName ?: "Anonymous" @@ -68,13 +69,7 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener { super.onCreateView(inflater, container, savedInstanceState) checkHiddenItems() getRecentPodcasts() - val composeView = ComposeView(requireContext()).apply { - setContent { - CustomTheme(requireContext()) { - MainView() - } - } - } + val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { MainView() } } } Logd(TAG, "fragment onCreateView") setupDrawerRoundBackground(composeView) @@ -87,7 +82,7 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener { navigationBarHeight = if (requireActivity().window.navigationBarDividerColor == Color.TRANSPARENT) 0f else 1 * resources.displayMetrics.density // Assuming the divider is 1dp in height } - val bottomInset = max(0.0, Math.round(bars.bottom - navigationBarHeight).toDouble()).toFloat() + val bottomInset = max(0.0, (bars.bottom - navigationBarHeight).roundToInt().toDouble()).toFloat() (view.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = bottomInset.toInt() insets } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt index 243e21b1..d1ab0d1e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt @@ -41,8 +41,11 @@ import android.view.View import android.view.ViewGroup import androidx.annotation.UiThread import androidx.collection.ArrayMap -import androidx.compose.foundation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -68,7 +71,6 @@ import org.jsoup.nodes.Document import java.io.File import java.io.IOException import java.util.* -import kotlin.concurrent.Volatile /** * Downloads a feed from a feed URL and parses it. Subclasses can display the @@ -736,11 +738,11 @@ class OnlineFeedFragment : Fragment() { return PREF_NAME } override fun updateToolbar() { - binding.toolbar.menu.findItem(R.id.episodes_sort).setVisible(false) + binding.toolbar.menu.findItem(R.id.episodes_sort).isVisible = false // binding.toolbar.menu.findItem(R.id.refresh_item).setVisible(false) - binding.toolbar.menu.findItem(R.id.action_search).setVisible(false) + binding.toolbar.menu.findItem(R.id.action_search).isVisible = false // binding.toolbar.menu.findItem(R.id.action_favorites).setVisible(false) - binding.toolbar.menu.findItem(R.id.filter_items).setVisible(false) + binding.toolbar.menu.findItem(R.id.filter_items).isVisible = false infoBarText.value = "${episodes.size} episodes" } override fun onMenuItemClick(item: MenuItem): Boolean { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineSearchFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineSearchFragment.kt index fb532c54..8bc0ad93 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineSearchFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineSearchFragment.kt @@ -36,10 +36,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -/** - * Provides actions for adding new podcast subscriptions. - */ - class OnlineSearchFragment : Fragment() { private var _binding: AddfeedBinding? = null @@ -56,11 +52,11 @@ class OnlineSearchFragment : Fragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { super.onCreateView(inflater, container, savedInstanceState) _binding = AddfeedBinding.inflate(inflater) - activity = getActivity() as? MainActivity +// activity = activity Logd(TAG, "fragment onCreateView") displayUpArrow = parentFragmentManager.backStackEntryCount != 0 if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW) - (getActivity() as MainActivity).setupToolbarToggle(binding.toolbar, displayUpArrow) + (activity as MainActivity).setupToolbarToggle(binding.toolbar, displayUpArrow) binding.searchButton.setOnClickListener { performSearch() } binding.searchVistaGuideButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(VistaGuidePodcastSearcher::class.java)) } @@ -172,12 +168,12 @@ class OnlineSearchFragment : Fragment() { withContext(Dispatchers.Main) { if (feed != null) { val fragment: Fragment = FeedEpisodesFragment.newInstance(feed.id) - (getActivity() as MainActivity).loadChildFragment(fragment) + (activity as MainActivity).loadChildFragment(fragment) } } } catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) - (getActivity() as MainActivity).showSnackbarAbovePlayer(e.localizedMessage?: "No messaage", Snackbar.LENGTH_LONG) + (activity as MainActivity).showSnackbarAbovePlayer(e.localizedMessage?: "No messaage", Snackbar.LENGTH_LONG) } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt index 89cb5ef1..c0577911 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt @@ -143,7 +143,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener { queueNames = queues.map { it.name }.toTypedArray() spinnerTexts.clear() spinnerTexts.addAll(queues.map { "${it.name} : ${it.size()}" }) - curIndex = queues.indexOf(curQueue) +// curIndex = queues.indexOf(curQueue) (activity as MainActivity).setupToolbarToggle(toolbar, displayUpArrow) toolbar.inflateMenu(R.menu.queue) @@ -156,7 +156,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener { Logd(TAG, "Queue selected: $queues[index].name") val prevQueueSize = curQueue.size() curQueue = upsertBlk(queues[index]) { it.update() } - toolbar.menu?.findItem(R.id.rename_queue)?.setVisible(curQueue.name != "Default") + toolbar.menu?.findItem(R.id.rename_queue)?.isVisible = curQueue.name != "Default" loadCurQueue(true) playbackService?.notifyCurQueueItemsChanged(max(prevQueueSize, curQueue.size())) } @@ -246,7 +246,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener { items(feedsAssociated.size, key = {index -> feedsAssociated[index].id}) { index -> val feed by remember { mutableStateOf(feedsAssociated[index]) } ConstraintLayout { - val (coverImage, episodeCount, rating, error) = createRefs() + val (coverImage, episodeCount, rating, _) = createRefs() val imgLoc = remember(feed) { feed.imageUrl } AsyncImage(model = ImageRequest.Builder(context).data(imgLoc) .memoryCachePolicy(CachePolicy.ENABLED).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).build(), @@ -374,7 +374,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener { } queues = realm.query(PlayQueue::class).find() queueNames = queues.map { it.name }.toTypedArray() - curIndex = queues.indexOf(curQueue) + curIndex = queues.indexOfFirst { it.id == curQueue.id } spinnerTexts.clear() spinnerTexts.addAll(queues.map { "${it.name} : ${it.size()}" }) refreshMenuItems() @@ -451,20 +451,20 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener { private fun refreshMenuItems() { if (showBin) { - toolbar.menu?.findItem(R.id.queue_sort)?.setVisible(false) - toolbar.menu?.findItem(R.id.rename_queue)?.setVisible(false) - toolbar.menu?.findItem(R.id.associated_feed)?.setVisible(false) - toolbar.menu?.findItem(R.id.add_queue)?.setVisible(false) - toolbar.menu?.findItem(R.id.queue_lock)?.setVisible(false) - toolbar.menu?.findItem(R.id.action_search)?.setVisible(false) + toolbar.menu?.findItem(R.id.queue_sort)?.isVisible = false + toolbar.menu?.findItem(R.id.rename_queue)?.isVisible = false + toolbar.menu?.findItem(R.id.associated_feed)?.isVisible = false + toolbar.menu?.findItem(R.id.add_queue)?.isVisible = false + toolbar.menu?.findItem(R.id.queue_lock)?.isVisible = false + toolbar.menu?.findItem(R.id.action_search)?.isVisible = false } else { - toolbar.menu?.findItem(R.id.action_search)?.setVisible(true) - toolbar.menu?.findItem(R.id.queue_sort)?.setVisible(true) - toolbar.menu?.findItem(R.id.associated_feed)?.setVisible(true) - toolbar.menu?.findItem(R.id.queue_lock)?.setChecked(isQueueLocked) - toolbar.menu?.findItem(R.id.queue_lock)?.setVisible(!isQueueKeepSorted) - toolbar.menu?.findItem(R.id.rename_queue)?.setVisible(curQueue.name != "Default") - toolbar.menu?.findItem(R.id.add_queue)?.setVisible(queueNames.size < 9) + toolbar.menu?.findItem(R.id.action_search)?.isVisible = true + toolbar.menu?.findItem(R.id.queue_sort)?.isVisible = true + toolbar.menu?.findItem(R.id.associated_feed)?.isVisible = true + toolbar.menu?.findItem(R.id.queue_lock)?.isChecked = isQueueLocked + toolbar.menu?.findItem(R.id.queue_lock)?.isVisible = !isQueueKeepSorted + toolbar.menu?.findItem(R.id.rename_queue)?.isVisible = curQueue.name != "Default" + toolbar.menu?.findItem(R.id.add_queue)?.isVisible = queueNames.size < 9 } } @@ -584,7 +584,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener { upsertBlk(newQueue) {} queues = realm.query(PlayQueue::class).find() queueNames = queues.map { it.name }.toTypedArray() - curIndex = queues.indexOf(curQueue) + curIndex = queues.indexOfFirst { it.id == curQueue.id } spinnerTexts.clear() spinnerTexts.addAll(queues.map { "${it.name} : ${it.episodeIds.size}" }) onDismiss() @@ -625,7 +625,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener { appPrefs.edit().putBoolean(UserPreferences.Prefs.prefQueueLocked.name, locked).apply() dragDropEnabled = !(isQueueKeepSorted || isQueueLocked) refreshMenuItems() - if (queueItems.size == 0) { + if (queueItems.isEmpty()) { if (locked) (activity as MainActivity).showSnackbarAbovePlayer(R.string.queue_locked, Snackbar.LENGTH_SHORT) else (activity as MainActivity).showSnackbarAbovePlayer(R.string.queue_unlocked, Snackbar.LENGTH_SHORT) } @@ -673,7 +673,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener { for (e in queueItems) vms.add(EpisodeVM(e)) Logd(TAG, "loadCurQueue() curQueue.episodes: ${curQueue.episodes.size}") queues = realm.query(PlayQueue::class).find() - curIndex = queues.indexOf(curQueue) + curIndex = queues.indexOfFirst { it.id == curQueue.id } spinnerTexts.clear() spinnerTexts.addAll(queues.map { "${it.name} : ${it.size()}" }) refreshInfoBar() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QuickDiscoveryFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QuickDiscoveryFragment.kt index 6a6f2132..38b21be9 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QuickDiscoveryFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QuickDiscoveryFragment.kt @@ -207,7 +207,7 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener { val podcast: PodcastSearchResult? = adapter.getItem(position) if (podcast?.feedUrl.isNullOrEmpty()) return - val fragment: Fragment = OnlineFeedFragment.newInstance(podcast!!.feedUrl!!) + val fragment: Fragment = OnlineFeedFragment.newInstance(podcast.feedUrl) (activity as MainActivity).loadChildFragment(fragment) } @@ -368,7 +368,7 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener { if (result.isNotEmpty()) { searchResults.addAll(result) noResultText = "" - } else noResultText = getString(R.string.no_results_for_query, "") + } else noResultText = getString(R.string.no_results_for_query) showProgress = false } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt index 3ca2586d..6e837f1d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt @@ -62,9 +62,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.text.NumberFormat -/** - * Performs a search operation on all feeds or one specific feed and displays the search result. - */ class SearchFragment : Fragment() { private var _binding: SearchFragmentBinding? = null private val binding get() = _binding!! @@ -76,7 +73,7 @@ class SearchFragment : Fragment() { private val resultFeeds = mutableStateListOf() private val results = mutableListOf() private val vms = mutableStateListOf() - protected var infoBarText = mutableStateOf("") + private var infoBarText = mutableStateOf("") private var leftActionState = mutableStateOf(NoActionSwipeAction()) private var rightActionState = mutableStateOf(NoActionSwipeAction()) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchResultsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchResultsFragment.kt index e19fa572..23e5ece4 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchResultsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchResultsFragment.kt @@ -119,7 +119,7 @@ class SearchResultsFragment : Fragment() { } if (searchResults.isEmpty()) Text(noResultText, color = textColor, modifier = Modifier.constrainAs(empty) { centerTo(parent) }) if (errorText.isNotEmpty()) Text(errorText, color = textColor, modifier = Modifier.constrainAs(txtvError) { centerTo(parent) }) - if (retryQerry.isNotEmpty()) Button(modifier = Modifier.padding(16.dp).constrainAs(butRetry) { top.linkTo(txtvError.bottom)}, onClick = { search(retryQerry) }, ) { + if (retryQerry.isNotEmpty()) Button(modifier = Modifier.padding(16.dp).constrainAs(butRetry) { top.linkTo(txtvError.bottom) }, onClick = { search(retryQerry) }) { Text(stringResource(id = R.string.retry_label)) } Text( getString(R.string.search_powered_by, searchProvider!!.name), color = Color.Black, style = MaterialTheme.typography.labelSmall, modifier = Modifier.background(Color.LightGray) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/StatisticsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/StatisticsFragment.kt index 265d490b..0044aa8d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/StatisticsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/StatisticsFragment.kt @@ -367,16 +367,16 @@ class StatisticsFragment : Fragment(), Toolbar.OnMenuItemClickListener { private fun refreshToolbarState() { when (selectedTabIndex.value) { 0 -> { - toolbar.menu?.findItem(R.id.statistics_reset)?.setVisible(true) - toolbar.menu?.findItem(R.id.statistics_filter)?.setVisible(true) + toolbar.menu?.findItem(R.id.statistics_reset)?.isVisible = true + toolbar.menu?.findItem(R.id.statistics_filter)?.isVisible = true } 1 -> { - toolbar.menu?.findItem(R.id.statistics_reset)?.setVisible(true) - toolbar.menu?.findItem(R.id.statistics_filter)?.setVisible(false) + toolbar.menu?.findItem(R.id.statistics_reset)?.isVisible = true + toolbar.menu?.findItem(R.id.statistics_filter)?.isVisible = false } else -> { - toolbar.menu?.findItem(R.id.statistics_reset)?.setVisible(false) - toolbar.menu?.findItem(R.id.statistics_filter)?.setVisible(false) + toolbar.menu?.findItem(R.id.statistics_reset)?.isVisible = false + toolbar.menu?.findItem(R.id.statistics_filter)?.isVisible = false } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt index 5aedcb14..e765b694 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt @@ -32,6 +32,7 @@ import android.content.ActivityNotFoundException import android.content.Context import android.content.DialogInterface import android.content.Intent +import android.content.SharedPreferences import android.net.Uri import android.os.Bundle import android.util.Log @@ -89,6 +90,7 @@ import java.text.SimpleDateFormat import java.util.* class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { + val prefs: SharedPreferences by lazy { requireContext().getSharedPreferences("SubscriptionsFragmentPrefs", Context.MODE_PRIVATE) } private var _binding: ComposeFragmentBinding? = null private val binding get() = _binding!! @@ -97,8 +99,38 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { private val tags: MutableList = mutableListOf() private val queueIds: MutableList = mutableListOf() - private var tagFilterIndex = 1 - private var queueFilterIndex = 0 + + private var _feedsFilter: String? = null + private var feedsFilter: String + get() { + if (_feedsFilter == null) _feedsFilter = prefs.getString("feedsFilter", "") ?: "" + return _feedsFilter ?: "" + } + set(filter) { + _feedsFilter = filter + prefs.edit().putString("feedsFilter", filter).apply() + } + + private var _tagFilterIndex: Int = -1 + private var tagFilterIndex: Int + get() { + if (_tagFilterIndex < 0) _tagFilterIndex = prefs.getInt("tagFilterIndex", 0) + return _tagFilterIndex + } + set(index) { + _tagFilterIndex = index + prefs.edit().putInt("tagFilterIndex", index).apply() + } + private var _queueFilterIndex: Int = -1 + private var queueFilterIndex: Int + get() { + if (_queueFilterIndex < 0) _queueFilterIndex = prefs.getInt("queueFilterIndex", 0) + return _queueFilterIndex + } + set(index) { + _queueFilterIndex = index + prefs.edit().putInt("queueFilterIndex", index).apply() + } private var infoTextFiltered = "" private var infoTextUpdate = "" @@ -176,12 +208,12 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { Column { InforBar() Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 20.dp, end = 20.dp)) { - Spinner(items = spinnerTexts, selectedItem = spinnerTexts[0]) { index: Int -> + Spinner(items = spinnerTexts, selectedIndex = queueFilterIndex) { index: Int -> queueFilterIndex = index loadSubscriptions() } Spacer(Modifier.weight(1f)) - Spinner(items = tags, selectedItem = tags[0]) { index: Int -> + Spinner(items = tags, selectedIndex = tagFilterIndex) { index: Int -> tagFilterIndex = index loadSubscriptions() } @@ -439,7 +471,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { } if (selectedOption == "Custom") { val queues = realm.query(PlayQueue::class).find() - Spinner(items = queues.map { it.name }, selectedItem = "Default") { index -> + Spinner(items = queues.map { it.name }, selectedIndex = 0) { index -> Logd(TAG, "Queue selected: ${queues[index]}") saveFeedPreferences { it: FeedPreferences -> it.queueId = queues[index].id } onDismissRequest() @@ -793,18 +825,14 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_arrow_upward_24), tint = Color.Black, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp).padding(end = 10.dp).clickable(onClick = { selected.clear() - for (i in 0..longPressIndex) { - selected.add(feedListFiltered[i]) - } + for (i in 0..longPressIndex) selected.add(feedListFiltered[i]) selectedSize = selected.size Logd(TAG, "selectedIds: ${selected.size}") })) Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_arrow_downward_24), tint = Color.Black, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp).padding(end = 10.dp).clickable(onClick = { selected.clear() - for (i in longPressIndex..) { - val localItems: MutableList = mutableListOf() - for (item in items) { - if (item.feed?.isLocalFeed == true) localItems.add(item) - else deleteEpisodeMedia(context, item) - } - - if (localItems.isNotEmpty()) { - MaterialAlertDialogBuilder(context) - .setTitle(R.string.delete_episode_label) - .setMessage(R.string.delete_local_feed_warning_body) - .setPositiveButton(R.string.delete_label) { dialog: DialogInterface?, which: Int -> - for (item in localItems) { - deleteEpisodeMedia(context, item) - } - } - .setNegativeButton(R.string.cancel_label, null) - .show() - } - } -} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/PictureInPictureUtil.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/PictureInPictureUtil.kt deleted file mode 100644 index ceed1e16..00000000 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/PictureInPictureUtil.kt +++ /dev/null @@ -1,19 +0,0 @@ -package ac.mdiq.podcini.ui.utils - -import android.app.Activity -import android.content.pm.PackageManager -import android.os.Build - -object PictureInPictureUtil { - fun supportsPictureInPicture(activity: Activity): Boolean { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - val packageManager = activity.packageManager - return packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) - } else return false - } - - fun isInPictureInPictureMode(activity: Activity): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && supportsPictureInPicture(activity)) activity.isInPictureInPictureMode - else false - } -} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/ShownotesCleaner.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/ShownotesCleaner.kt index 162693e1..7c4c138f 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/ShownotesCleaner.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/utils/ShownotesCleaner.kt @@ -79,7 +79,7 @@ class ShownotesCleaner(context: Context) { private fun addTimecodes(document: Document, playableDuration: Int) { val elementsWithTimeCodes = document.body().getElementsMatchingOwnText(TIMECODE_REGEX) Logd(TAG, "Recognized " + elementsWithTimeCodes.size + " timecodes") - if (elementsWithTimeCodes.size == 0) return // No elements with timecodes + if (elementsWithTimeCodes.isEmpty()) return // No elements with timecodes var useHourFormat = true if (playableDuration != Int.MAX_VALUE) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/view/NoRelayoutTextView.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/view/NoRelayoutTextView.kt deleted file mode 100644 index fa5e854d..00000000 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/view/NoRelayoutTextView.kt +++ /dev/null @@ -1,30 +0,0 @@ -package ac.mdiq.podcini.ui.view - -import android.content.Context -import android.util.AttributeSet -import androidx.appcompat.widget.AppCompatTextView - -class NoRelayoutTextView : AppCompatTextView { - private var requestLayoutEnabled = false - private var maxTextLength = 0f - - constructor(context: Context) : super(context) - - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) - - override fun requestLayout() { - if (requestLayoutEnabled) super.requestLayout() - requestLayoutEnabled = false - } - - override fun setText(text: CharSequence, type: BufferType) { - val textLength = paint.measureText(text.toString()) - if (textLength > maxTextLength) { - maxTextLength = textLength - requestLayoutEnabled = true - } - super.setText(text, type) - } -} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/view/PlaybackSpeedSeekBar.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/view/PlaybackSpeedSeekBar.kt index f161471b..5bf9f514 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/view/PlaybackSpeedSeekBar.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/view/PlaybackSpeedSeekBar.kt @@ -8,6 +8,7 @@ import android.widget.FrameLayout import android.widget.SeekBar import android.widget.SeekBar.OnSeekBarChangeListener import androidx.core.util.Consumer +import kotlin.math.roundToInt class PlaybackSpeedSeekBar : FrameLayout { private var _binding: PlaybackSpeedSeekBarBinding? = null @@ -16,6 +17,9 @@ class PlaybackSpeedSeekBar : FrameLayout { private lateinit var seekBar: SeekBar private var progressChangedListener: Consumer? = null + val currentSpeed: Float + get() = (seekBar.progress + 10) / 20.0f + constructor(context: Context) : super(context) { setup() } @@ -39,24 +43,19 @@ class PlaybackSpeedSeekBar : FrameLayout { val playbackSpeed = (progress + 10) / 20.0f progressChangedListener?.accept(playbackSpeed) } - override fun onStartTrackingTouch(seekBar: SeekBar) {} - override fun onStopTrackingTouch(seekBar: SeekBar) {} }) } fun updateSpeed(speedMultiplier: Float) { - seekBar.progress = Math.round((20 * speedMultiplier) - 10) + seekBar.progress = ((20 * speedMultiplier) - 10).roundToInt() } fun setProgressChangedListener(progressChangedListener: Consumer?) { this.progressChangedListener = progressChangedListener } - val currentSpeed: Float - get() = (seekBar.progress + 10) / 20.0f - override fun setEnabled(enabled: Boolean) { super.setEnabled(enabled) seekBar.isEnabled = enabled diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/view/ShownotesWebView.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/view/ShownotesWebView.kt index c2d57de2..61d87602 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/view/ShownotesWebView.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/view/ShownotesWebView.kt @@ -3,7 +3,6 @@ package ac.mdiq.podcini.ui.view import ac.mdiq.podcini.R import ac.mdiq.podcini.net.utils.NetworkUtils import ac.mdiq.podcini.storage.utils.DurationConverter -import ac.mdiq.podcini.ui.actions.MenuItemUtils import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.utils.ShownotesCleaner import ac.mdiq.podcini.util.* @@ -142,7 +141,23 @@ class ShownotesWebView : WebView, View.OnLongClickListener { menu.add(Menu.NONE, R.id.share_url_item, Menu.NONE, R.string.share_url_label) menu.setHeaderTitle(selectedUrl) } - MenuItemUtils.setOnClickListeners(menu) { item: MenuItem -> this.onContextItemSelected(item) } + setOnClickListeners(menu) { item: MenuItem -> this.onContextItemSelected(item) } + } + + /** + * When pressing a context menu item, Android calls onContextItemSelected + * for ALL fragments in arbitrary order, not just for the fragment that the + * context menu was created from. This assigns the listener to every menu item, + * so that the correct fragment is always called first and can consume the click. + * + * Note that Android still calls the onContextItemSelected methods of all fragments + * when the passed listener returns false. + */ + fun setOnClickListeners(menu: Menu?, listener: MenuItem.OnMenuItemClickListener?) { + for (i in 0 until menu!!.size()) { + if (menu.getItem(i).subMenu != null) setOnClickListeners(menu.getItem(i).subMenu, listener) + menu.getItem(i).setOnMenuItemClickListener(listener) + } } fun setTimecodeSelectedListener(timecodeSelectedListener: Consumer?) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/view/TriangleLabelView.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/view/TriangleLabelView.kt deleted file mode 100644 index 3992f74b..00000000 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/view/TriangleLabelView.kt +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (C) 2016 Shota Saito - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Source: https://github.com/shts/TriangleLabelView - * Modified for our need; see Podcini #5925 for context - */ -package ac.mdiq.podcini.ui.view - - -import ac.mdiq.podcini.R -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.* -import android.util.AttributeSet -import android.view.View -import kotlin.math.sqrt - -class TriangleLabelView : View { - private val primary = PaintHolder() - private var topPadding = 0f - private var bottomPadding = 0f - private var centerPadding = 0f - private var trianglePaint: Paint? = null - private var width = 0 - private var height = 0 - private var corner: Corner? = null - - @JvmOverloads - constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : super(context, attrs, defStyleAttr) { - init(context, attrs) - } - - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { - init(context, attrs) - } - - private fun init(context: Context, attrs: AttributeSet?) { - val ta = context.obtainStyledAttributes(attrs, R.styleable.TriangleLabelView) - - this.topPadding = ta.getDimension(R.styleable.TriangleLabelView_labelTopPadding, dp2px(7f).toFloat()) - this.centerPadding = ta.getDimension(R.styleable.TriangleLabelView_labelCenterPadding, dp2px(3f).toFloat()) - this.bottomPadding = ta.getDimension(R.styleable.TriangleLabelView_labelBottomPadding, dp2px(3f).toFloat()) - - val backgroundColor = ta.getColor(R.styleable.TriangleLabelView_backgroundColor, Color.parseColor("#66000000")) - primary.color = ta.getColor(R.styleable.TriangleLabelView_primaryTextColor, Color.WHITE) - - primary.size = ta.getDimension(R.styleable.TriangleLabelView_primaryTextSize, sp2px(11f)) - - val primary = ta.getString(R.styleable.TriangleLabelView_primaryText) - if (primary != null) this.primary.text = primary - - this.corner = Corner.from(ta.getInt(R.styleable.TriangleLabelView_corner, 1)) - - ta.recycle() - - this.primary.initPaint() - - trianglePaint = Paint(Paint.ANTI_ALIAS_FLAG) - trianglePaint!!.color = backgroundColor - - this.primary.resetStatus() - } - - fun setPrimaryText(text: String) { - primary.text = text - primary.resetStatus() - relayout() - } - - fun getCorner(): Corner? { - return corner - } - - fun setCorner(corner: Corner?) { - this.corner = corner - relayout() - } - - override fun onDraw(canvas: Canvas) { - super.onDraw(canvas) - canvas.save() - - // translate - canvas.translate(0f, ((height * sqrt(2.0)) - height).toFloat()) - - // rotate - if (corner!!.left()) canvas.rotate(DEGREES_LEFT.toFloat(), 0f, height.toFloat()) - else canvas.rotate(DEGREES_RIGHT.toFloat(), width.toFloat(), height.toFloat()) - - // draw triangle - @SuppressLint("DrawAllocation") val path = Path() - path.moveTo(0f, height.toFloat()) - path.lineTo(width / 2f, 0f) - path.lineTo(width.toFloat(), height.toFloat()) - path.close() - canvas.drawPath(path, trianglePaint!!) - - // draw primaryText - canvas.drawText(primary.text, (width) / 2f, (topPadding + centerPadding + primary.height), primary.paint!!) - canvas.restore() - } - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - height = (topPadding + centerPadding + bottomPadding + primary.height).toInt() - width = 2 * height - val realHeight = (height * sqrt(2.0)).toInt() - setMeasuredDimension(width, realHeight) - } - - fun dp2px(dpValue: Float): Int { - val scale = context.resources.displayMetrics.density - return (dpValue * scale + 0.5f).toInt() - } - - fun sp2px(spValue: Float): Float { - val scale = context.resources.displayMetrics.scaledDensity - return spValue * scale - } - - /** - * Should be called whenever what we're displaying could have changed. - */ - private fun relayout() { - invalidate() - requestLayout() - } - - enum class Corner(private val type: Int) { - TOP_LEFT(1), - TOP_RIGHT(2); - - fun left(): Boolean { - return this == TOP_LEFT - } - - companion object { - internal fun from(type: Int): Corner { - for (c in entries) { - if (c.type == type) return c - } - return TOP_LEFT - } - } - } - - private class PaintHolder { - var text: String = "" - var paint: Paint? = null - var color: Int = 0 - var size: Float = 0f - var height: Float = 0f - var width: Float = 0f - - fun initPaint() { - paint = Paint(Paint.ANTI_ALIAS_FLAG) - paint!!.color = color - paint!!.textAlign = Paint.Align.CENTER - paint!!.textSize = size - paint!!.setTypeface(Typeface.DEFAULT_BOLD) - } - - fun resetStatus() { - val rectText = Rect() - paint!!.getTextBounds(text, 0, text.length, rectText) - width = rectText.width().toFloat() - height = rectText.height().toFloat() - } - } - - companion object { - private const val DEGREES_LEFT = -45 - private const val DEGREES_RIGHT = 45 - } -} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/widget/WidgetUpdater.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/widget/WidgetUpdater.kt index f91c2404..24f20ec6 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/widget/WidgetUpdater.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/widget/WidgetUpdater.kt @@ -36,9 +36,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.math.max -/** - * Updates the state of the player widget. - */ object WidgetUpdater { private val TAG: String = WidgetUpdater::class.simpleName ?: "Anonymous" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/widget/WidgetUpdaterWorker.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/widget/WidgetUpdaterWorker.kt index 36f96ad3..91c85a74 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/widget/WidgetUpdaterWorker.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/widget/WidgetUpdaterWorker.kt @@ -11,8 +11,7 @@ import androidx.work.* class WidgetUpdaterWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) { override fun doWork(): Result { - try { - updateWidget() + try { updateWidget() } catch (e: Exception) { Logd(TAG, "Failed to update Podcini widget: $e") return Result.failure() diff --git a/changelog.md b/changelog.md index 90854288..8d6757bc 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,12 @@ +# 6.13.11 + +* created private shared preferences for Subscriptions view and moved related properties there from the apps prefs +* persisted settings of tag spinner and queue spinner in Subscriptions view +* fixed again the incorrect initial text on Spinner in Queues view +* save played duration and time spent when playback of an episode is completed +* some code cleaning and restructuring +* gradle update + # 6.13.10 * fixed Spinner in Subscriptions: All tags vs Untagged irregularity diff --git a/fastlane/metadata/android/en-US/changelogs/3020298.txt b/fastlane/metadata/android/en-US/changelogs/3020298.txt new file mode 100644 index 00000000..74e77072 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020298.txt @@ -0,0 +1,8 @@ + Version 6.13.11 + +* created private shared preferences for Subscriptions view and moved related properties there from the apps prefs +* persisted settings of tag spinner and queue spinner in Subscriptions view +* fixed again the incorrect initial text on Spinner in Queues view +* save played duration and time spent when playback of an episode is completed +* some code cleaning and restructuring +* gradle update diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6e486e10..ce303c9d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ balloon = "1.6.6" coil = "2.7.0" commonsLang3 = "3.15.0" commonsIo = "2.16.1" -composeBom = "2024.10.01" +composeBom = "2024.11.00" conscryptAndroid = "2.5.2" constraintlayoutCompose = "1.1.0" coordinatorlayout = "1.2.0" @@ -19,7 +19,7 @@ documentfile = "1.0.1" fyydlin = "v0.5.0" googleMaterialTypeface = "4.0.0.3-kotlin" googleMaterialTypefaceOutlined = "4.0.0.2-kotlin" -gradle = "8.5.2" +gradle = "8.6.1" gridlayout = "1.0.0" groovyXml = "3.0.19" iconicsCore = "5.5.0-b01" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 09523c0e..bb45641c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ +#Fri Nov 15 08:33:12 WEST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME