6.13.11 commit

This commit is contained in:
Xilin Jia 2024-11-15 14:56:39 +01:00
parent 8eb74458bd
commit 5598ad630f
61 changed files with 585 additions and 1036 deletions

View File

@ -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 = ""

View File

@ -114,10 +114,10 @@
website="https://github.com/ByteHamster/SearchPreference"
license="MIT"
licenseText="LICENSE_SEARCHPREFERENCE.txt" />
<library
name="Triangle Label View"
author="Shota Saito"
website="https://github.com/shts/TriangleLabelView"
license="Apache 2.0"
licenseText="LICENSE_TRIANGLE_LABEL_VIEW.txt" />
<!-- <library-->
<!-- name="Triangle Label View"-->
<!-- author="Shota Saito"-->
<!-- website="https://github.com/shts/TriangleLabelView"-->
<!-- license="Apache 2.0"-->
<!-- licenseText="LICENSE_TRIANGLE_LABEL_VIEW.txt" />-->
</libraries>

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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<String>? = null
private var bufferingUpdateListener: Consumer<Int>? = null
private var loudnessEnhancer: LoudnessEnhancer? = null

View File

@ -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
/*

View File

@ -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<CommandButton>,
@ -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() {

View File

@ -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()

View File

@ -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 {

View File

@ -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 {

View File

@ -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<DownloadResult> {
override fun compare(lhs: DownloadResult, rhs: DownloadResult): Int {
return rhs.getCompletionDate().compareTo(lhs.getCompletionDate())
}
}
}

View File

@ -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<Episode>): Int {

View File

@ -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"

View File

@ -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<DownloadResult> {
override fun compare(lhs: DownloadResult, rhs: DownloadResult): Int {
return rhs.getCompletionDate().compareTo(lhs.getCompletionDate())
}
}

View File

@ -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()
}
}
}

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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<Episode>) {
val localItems: MutableList<Episode> = 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 {

View File

@ -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!!

View File

@ -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

View File

@ -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()

View File

@ -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
}
}
}

View File

@ -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<String> = ArrayAdapter<String>(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() {

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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<String>, selectedItem: String, modifier: Modifier = Modi
@Composable
fun Spinner(items: List<String>, 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<String>, 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<String>, 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)) {

View File

@ -112,24 +112,16 @@ fun InforBar(text: MutableState<String>, leftAction: MutableState<SwipeAction>,
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<Episode>, onDismissRequest: () -> Unit) {
if (removeChecked) {
val toRemove = mutableSetOf<Long>()
val toRemoveCur = mutableListOf<Episode>()
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<Episode>, 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<Episode>, 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<Episode>, 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<Episode>, 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<EpisodeVM>, feed:
val showConfirmYoutubeDialog = remember { mutableStateOf(false) }
val youtubeUrls = remember { mutableListOf<String>() }
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<EpisodeVM>, 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<EpisodeVM>, 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<EpisodeVM>, 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))
}
}

View File

@ -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

View File

@ -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

View File

@ -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)
// }
}
}

View File

@ -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)

View File

@ -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) {

View File

@ -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 {

View File

@ -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()

View File

@ -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"

View File

@ -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() {

View File

@ -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
}

View File

@ -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 {

View File

@ -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)
}
}
}

View File

@ -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()

View File

@ -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
}

View File

@ -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<Feed>()
private val results = mutableListOf<Episode>()
private val vms = mutableStateListOf<EpisodeVM>()
protected var infoBarText = mutableStateOf("")
private var infoBarText = mutableStateOf("")
private var leftActionState = mutableStateOf<SwipeAction>(NoActionSwipeAction())
private var rightActionState = mutableStateOf<SwipeAction>(NoActionSwipeAction())

View File

@ -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)

View File

@ -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
}
}
}

View File

@ -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<String> = mutableListOf()
private val queueIds: MutableList<Long> = 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..<feedListFiltered.size) {
selected.add(feedListFiltered[i])
}
for (i in longPressIndex..<feedListFiltered.size) selected.add(feedListFiltered[i])
selectedSize = selected.size
Logd(TAG, "selectedIds: ${selected.size}")
}))
@ -853,47 +881,37 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
}
private fun sortArraysFromCodeSet() {
for (i in playStateSort.indices) {
playStateSort[i].value = false
}
for (c in playStateCodeSet) {
val e = PlayState.fromCode(c.toInt())
playStateSort[e.ordinal].value = true
}
for (i in ratingSort.indices) {
ratingSort[i].value = false
}
for (c in ratingCodeSet) {
val e = Rating.fromCode(c.toInt())
ratingSort[e.ordinal].value = true
}
for (i in playStateSort.indices) playStateSort[i].value = false
for (c in playStateCodeSet) playStateSort[PlayState.fromCode(c.toInt()).ordinal].value = true
for (i in ratingSort.indices) ratingSort[i].value = false
for (c in ratingCodeSet) ratingSort[Rating.fromCode(c.toInt()).ordinal].value = true
}
private fun saveSortingPrefs() {
appPrefs.edit().putInt("sortIndex", sortIndex).apply()
appPrefs.edit().putBoolean("titleAscending", titleAscending).apply()
appPrefs.edit().putBoolean("dateAscending", dateAscending).apply()
appPrefs.edit().putBoolean("countAscending", countAscending).apply()
appPrefs.edit().putInt("dateSortIndex", dateSortIndex).apply()
appPrefs.edit().putInt("downlaodedSortIndex", downlaodedSortIndex).apply()
appPrefs.edit().putInt("commentedSortIndex", commentedSortIndex).apply()
prefs.edit().putInt("sortIndex", sortIndex).apply()
prefs.edit().putBoolean("titleAscending", titleAscending).apply()
prefs.edit().putBoolean("dateAscending", dateAscending).apply()
prefs.edit().putBoolean("countAscending", countAscending).apply()
prefs.edit().putInt("dateSortIndex", dateSortIndex).apply()
prefs.edit().putInt("downlaodedSortIndex", downlaodedSortIndex).apply()
prefs.edit().putInt("commentedSortIndex", commentedSortIndex).apply()
sortArrays2CodeSet()
appPrefs.edit().putStringSet("playStateCodeSet", playStateCodeSet).apply()
appPrefs.edit().putStringSet("ratingCodeSet", ratingCodeSet).apply()
prefs.edit().putStringSet("playStateCodeSet", playStateCodeSet).apply()
prefs.edit().putStringSet("ratingCodeSet", ratingCodeSet).apply()
}
private fun getSortingPrefs() {
sortIndex = appPrefs.getInt("sortIndex", 0)
titleAscending = appPrefs.getBoolean("titleAscending", true)
dateAscending = appPrefs.getBoolean("dateAscending", true)
countAscending = appPrefs.getBoolean("countAscending", true)
dateSortIndex = appPrefs.getInt("dateSortIndex", 0)
downlaodedSortIndex = appPrefs.getInt("downlaodedSortIndex", -1)
commentedSortIndex = appPrefs.getInt("commentedSortIndex", -1)
sortIndex = prefs.getInt("sortIndex", 0)
titleAscending = prefs.getBoolean("titleAscending", true)
dateAscending = prefs.getBoolean("dateAscending", true)
countAscending = prefs.getBoolean("countAscending", true)
dateSortIndex = prefs.getInt("dateSortIndex", 0)
downlaodedSortIndex = prefs.getInt("downlaodedSortIndex", -1)
commentedSortIndex = prefs.getInt("commentedSortIndex", -1)
playStateCodeSet.clear()
playStateCodeSet.addAll(appPrefs.getStringSet("playStateCodeSet", setOf())!!)
playStateCodeSet.addAll(prefs.getStringSet("playStateCodeSet", setOf())!!)
ratingCodeSet.clear()
ratingCodeSet.addAll(appPrefs.getStringSet("ratingCodeSet", setOf())!!)
ratingCodeSet.addAll(prefs.getStringSet("ratingCodeSet", setOf())!!)
sortArraysFromCodeSet()
}
@ -1487,24 +1505,6 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
private var prevFeedUpdatingEvent: FlowEvent.FeedUpdatingEvent? = null
// val feedOrderBy: Int
// get() {
// val value = appPrefs.getString(UserPreferences.Prefs.prefDrawerFeedOrder.name, "" + FeedSortOrder.UNPLAYED_NEW_OLD.index)
// return value!!.toInt()
// }
//
// val feedOrderDir: Int
// get() {
// val value = appPrefs.getInt(UserPreferences.Prefs.prefDrawerFeedOrderDir.name, 0)
// return value
// }
var feedsFilter: String
get() = appPrefs.getString(UserPreferences.Prefs.prefFeedFilter.name, "")?:""
set(filter) {
appPrefs.edit().putString(UserPreferences.Prefs.prefFeedFilter.name, filter).apply()
}
fun newInstance(folderTitle: String?): SubscriptionsFragment {
val fragment = SubscriptionsFragment()
val args = Bundle()

View File

@ -1,32 +0,0 @@
package ac.mdiq.podcini.ui.utils
import ac.mdiq.podcini.R
import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodeMedia
import android.content.Context
import android.content.DialogInterface
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import ac.mdiq.podcini.storage.model.Episode
object LocalDeleteModal {
fun deleteEpisodesWarnLocal(context: Context, items: Iterable<Episode>) {
val localItems: MutableList<Episode> = 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()
}
}
}

View File

@ -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
}
}

View File

@ -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) {

View File

@ -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)
}
}

View File

@ -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<Float>? = 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<Float>?) {
this.progressChangedListener = progressChangedListener
}
val currentSpeed: Float
get() = (seekBar.progress + 10) / 20.0f
override fun setEnabled(enabled: Boolean) {
super.setEnabled(enabled)
seekBar.isEnabled = enabled

View File

@ -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<Int>?) {

View File

@ -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
}
}

View File

@ -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"

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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