From a9f59905bd80e4b6e2a1897d247e37fca06c882b Mon Sep 17 00:00:00 2001 From: Xilin Jia <6257601+XilinJia@users.noreply.github.com> Date: Sun, 20 Oct 2024 17:30:30 +0100 Subject: [PATCH] 6.11.7 commit --- app/build.gradle | 4 +- .../ac/mdiq/podcini/storage/database/Feeds.kt | 22 +- .../mdiq/podcini/storage/database/RealmDB.kt | 2 +- .../podcini/storage/model/EpisodeMedia.kt | 11 +- .../ac/mdiq/podcini/storage/model/Feed.kt | 2 + .../ac/mdiq/podcini/storage/model/ShareLog.kt | 10 + .../storage/utils/ImageResourceUtils.kt | 2 + .../podcini/ui/actions/EpisodeActionButton.kt | 12 +- .../mdiq/podcini/ui/actions/SwipeActions.kt | 10 +- .../mdiq/podcini/ui/activity/MainActivity.kt | 7 +- .../ui/activity/ShareReceiverActivity.kt | 8 +- .../mdiq/podcini/ui/compose/ChaptersDialog.kt | 4 +- .../ac/mdiq/podcini/ui/compose/EpisodesVM.kt | 464 ++++++++++-------- .../ac/mdiq/podcini/ui/compose/Feeds.kt | 11 +- .../podcini/ui/dialog/CustomFeedNameDialog.kt | 1 - .../ui/fragment/AudioPlayerFragment.kt | 312 ++++++------ .../ui/fragment/EpisodeInfoFragment.kt | 14 +- .../ui/fragment/FeedEpisodesFragment.kt | 10 +- .../podcini/ui/fragment/FeedInfoFragment.kt | 10 +- .../mdiq/podcini/ui/fragment/LogsFragment.kt | 35 +- .../podcini/ui/fragment/NavDrawerFragment.kt | 6 +- .../podcini/ui/fragment/OnlineFeedFragment.kt | 53 +- .../podcini/ui/fragment/QueuesFragment.kt | 2 +- .../ui/fragment/SubscriptionsFragment.kt | 39 +- app/src/main/res/layout/queue_fragment.xml | 2 +- changelog.md | 9 + .../android/en-US/changelogs/3020276.txt | 2 +- .../android/en-US/changelogs/3020277.txt | 8 + 28 files changed, 631 insertions(+), 441 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/3020277.txt diff --git a/app/build.gradle b/app/build.gradle index 609e435b..ef76a654 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,8 +31,8 @@ android { testApplicationId "ac.mdiq.podcini.tests" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - versionCode 3020276 - versionName "6.11.6" + versionCode 3020277 + versionName "6.11.7" applicationId "ac.mdiq.podcini.R" def commit = "" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt index 70aebfa5..647a3219 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt @@ -184,6 +184,15 @@ object Feeds { return null } + fun isSubscribed(feed: Feed): Boolean { + val f = realm.query(Feed::class, "eigenTitle == $0 && author == $1", feed.eigenTitle, feed.author).first().find() + return f != null + } + + fun getFeedByTitleAndAuthor(title: String, author: String): Feed? { + return realm.query(Feed::class, "eigenTitle == $0 && author == $1", title, author).first().find() + } + /** * Adds new Feeds to the database or updates the old versions if they already exists. If another Feed with the same * identifying value already exists, this method will add new FeedItems from the new Feed to the existing Feed. @@ -271,6 +280,7 @@ object Feeds { } } } + if (oldItem != null) oldItem.updateFromOther(episode) else { Logd(TAG, "Found new episode: ${episode.title}") @@ -316,6 +326,11 @@ object Feeds { savedFeed.type = newFeed.type savedFeed.lastUpdateFailed = false resultFeed = savedFeed + + savedFeed.totleDuration = 0 + for (e in savedFeed.episodes) { + savedFeed.totleDuration += e.media?.duration ?: 0 + } try { upsertBlk(savedFeed) {} if (removeUnlistedItems && unlistedItems.isNotEmpty()) runBlocking { deleteEpisodes(context, unlistedItems).join() } @@ -357,13 +372,13 @@ object Feeds { feed.preferences = FeedPreferences(feed.id, false, AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, "", "") else feed.preferences!!.feedID = feed.id + feed.totleDuration = 0 Logd(TAG, "feed.episodes count: ${feed.episodes.size}") for (episode in feed.episodes) { episode.id = idLong++ episode.feedId = feed.id if (episode.media != null) episode.media!!.id = episode.id -// copyToRealm(episode) // no need if episodes is a relation of feed, otherwise yes. -// idLong += 1 + feed.totleDuration += episode.media?.duration ?: 0 } copyToRealm(feed) } @@ -458,7 +473,7 @@ object Feeds { return 1 } - fun createSynthetic(feedId: Long, name: String): Feed { + fun createSynthetic(feedId: Long, name: String, video: Boolean = false): Feed { val feed = Feed() var feedId_ = feedId if (feedId_ <= 0) { @@ -473,6 +488,7 @@ object Feeds { feed.title = name feed.author = "Yours Truly" 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!!.keepUpdated = false diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt index b23ec554..e6ac311a 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt @@ -40,7 +40,7 @@ object RealmDB { SubscriptionLog::class, Chapter::class)) .name("Podcini.realm") - .schemaVersion(26) + .schemaVersion(27) .migration({ mContext -> val oldRealm = mContext.oldRealm // old realm using the previous schema val newRealm = mContext.newRealm // new realm using the new schema diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt index ad58bf65..40b5e1f2 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/EpisodeMedia.kt @@ -69,6 +69,8 @@ class EpisodeMedia: EmbeddedRealmObject, Playable { private set // if null: unknown, will be checked + // TODO: what to do with this? can be expensive + @Ignore var hasEmbeddedPicture: Boolean? = null @Ignore @@ -78,10 +80,6 @@ class EpisodeMedia: EmbeddedRealmObject, Playable { // var episodeId: Long = 0 // private set -// @Ignore -// val isInProgress: Boolean -// get() = (this.position > 0) - constructor() {} constructor(i: Episode?, downloadUrl: String?, size: Long, mimeType: String?) { @@ -215,14 +213,13 @@ class EpisodeMedia: EmbeddedRealmObject, Playable { fun hasEmbeddedPicture(): Boolean { // TODO: checkEmbeddedPicture needs to update current copy - if (hasEmbeddedPicture == null) unmanaged(this).checkEmbeddedPicture() + if (hasEmbeddedPicture == null) checkEmbeddedPicture() return hasEmbeddedPicture ?: false } override fun writeToParcel(dest: Parcel, flags: Int) { dest.writeString(id.toString()) dest.writeString(if (episode != null) episode!!.id.toString() else "") - dest.writeInt(duration) dest.writeInt(position) dest.writeLong(size) @@ -329,7 +326,7 @@ class EpisodeMedia: EmbeddedRealmObject, Playable { hasEmbeddedPicture = false } } - if (persist && episode != null) upsertBlk(episode!!) {} +// if (persist && episode != null) upsertBlk(episode!!) {} } fun episodeOrFetch(): Episode? { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt index 85fce98d..c14331d6 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt @@ -89,6 +89,8 @@ class Feed : RealmObject { var preferences: FeedPreferences? = null + var totleDuration: Long = 0L + // TODO: this might not be needed var measures: FeedMeasures? = null diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/ShareLog.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/ShareLog.kt index e8ddb1bc..76e0b930 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/ShareLog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/ShareLog.kt @@ -10,6 +10,10 @@ class ShareLog : RealmObject { var url: String? = null + var title: String? = null + + var author: String? = null + var type: String? = null var status: Int = Status.ERROR.ordinal @@ -23,6 +27,12 @@ class ShareLog : RealmObject { this.url = url } + enum class Type { + Text, + YTMedia, + Podcast + } + enum class Status { ERROR, SUCCESS, diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageResourceUtils.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageResourceUtils.kt index 41ab89d0..9d999622 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageResourceUtils.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageResourceUtils.kt @@ -6,6 +6,7 @@ import ac.mdiq.podcini.storage.model.Playable import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.Prefs.prefEpisodeCover import ac.mdiq.podcini.preferences.UserPreferences.appPrefs +import ac.mdiq.podcini.util.Logd /** * Utility class to use the appropriate image resource based on [UserPreferences]. @@ -31,6 +32,7 @@ object ImageResourceUtils { */ @JvmStatic fun getEpisodeListImageLocation(episode: Episode): String? { + Logd("ImageResourceUtils", "getEpisodeListImageLocation called") return if (useEpisodeCoverSetting) episode.imageLocation else getFallbackImageLocation(episode) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeActionButton.kt index 9211ae13..6f06addb 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeActionButton.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/EpisodeActionButton.kt @@ -43,7 +43,9 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.core.text.HtmlCompat @@ -87,31 +89,31 @@ abstract class EpisodeActionButton internal constructor(@JvmField var item: Epis IconButton(onClick = { PlayActionButton(item).onClick(context) onDismiss() - }) { Image(painter = painterResource(R.drawable.ic_play_24dp), contentDescription = "Play") } + }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_play_24dp), contentDescription = "Play") } } if (label != R.string.stream_label && label != R.string.play_label && label != R.string.pause_label && label != R.string.delete_label) { IconButton(onClick = { StreamActionButton(item).onClick(context) onDismiss() - }) { Image(painter = painterResource(R.drawable.ic_stream), contentDescription = "Stream") } + }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_stream), contentDescription = "Stream") } } if (label != R.string.download_label && label != R.string.play_label && label != R.string.delete_label) { IconButton(onClick = { DownloadActionButton(item).onClick(context) onDismiss() - }) { Image(painter = painterResource(R.drawable.ic_download), contentDescription = "Download") } + }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_download), contentDescription = "Download") } } if (label != R.string.delete_label && label != R.string.download_label && label != R.string.stream_label) { IconButton(onClick = { DeleteActionButton(item).onClick(context) onDismiss() - }) { Image(painter = painterResource(R.drawable.ic_delete), contentDescription = "Delete") } + }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_delete), contentDescription = "Delete") } } if (label != R.string.visit_website_label) { IconButton(onClick = { VisitWebsiteActionButton(item).onClick(context) onDismiss() - }) { Image(painter = painterResource(R.drawable.ic_web), contentDescription = "Web") } + }) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_web), contentDescription = "Web") } } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/SwipeActions.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/SwipeActions.kt index ac4b1363..6e4ab9e4 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/SwipeActions.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/SwipeActions.kt @@ -600,7 +600,7 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String) } showPickerDialog = false }) { - Icon(painter = painterResource(keys[index].getActionIcon()), tint = textColor, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp)) + Icon(imageVector = ImageVector.vectorResource(keys[index].getActionIcon()), tint = textColor, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp)) Text(keys[index].getTitle(context), color = textColor, textAlign = TextAlign.Center) } } @@ -644,22 +644,22 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String) Text(stringResource(R.string.swipe_left)) Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 10.dp, end = 10.dp)) { Spacer(Modifier.weight(0.1f)) - Icon(painter = painterResource(leftAction.value[0].getActionIcon()), tint = textColor, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp) + Icon(imageVector = ImageVector.vectorResource(leftAction.value[0].getActionIcon()), tint = textColor, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp) .clickable(onClick = { direction = -1 showPickerDialog = true }) ) Spacer(Modifier.weight(0.1f)) - Icon(painter = painterResource(R.drawable.baseline_arrow_left_alt_24), tint = textColor, contentDescription = "right_arrow", modifier = Modifier.width(50.dp).height(35.dp)) + Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_arrow_left_alt_24), tint = textColor, contentDescription = "right_arrow", modifier = Modifier.width(50.dp).height(35.dp)) Spacer(Modifier.weight(0.5f)) } Text(stringResource(R.string.swipe_right)) Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 10.dp, end = 10.dp)) { Spacer(Modifier.weight(0.5f)) - Icon(painter = painterResource(R.drawable.baseline_arrow_right_alt_24), tint = textColor, contentDescription = "right_arrow", modifier = Modifier.width(50.dp).height(35.dp)) + Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_arrow_right_alt_24), tint = textColor, contentDescription = "right_arrow", modifier = Modifier.width(50.dp).height(35.dp)) Spacer(Modifier.weight(0.1f)) - Icon(painter = painterResource(rightAction.value[0].getActionIcon()), tint = textColor, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp) + Icon(imageVector = ImageVector.vectorResource(rightAction.value[0].getActionIcon()), tint = textColor, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp) .clickable(onClick = { direction = 1 showPickerDialog = true diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt index ebb169e0..4107c485 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt @@ -681,7 +681,8 @@ class MainActivity : CastEnabledActivity() { } intent.hasExtra(Extras.fragment_feed_url.name) -> { val feedurl = intent.getStringExtra(Extras.fragment_feed_url.name) - if (feedurl != null) loadChildFragment(OnlineFeedFragment.newInstance(feedurl)) + val isShared = intent.getBooleanExtra(Extras.isShared.name, false) + if (feedurl != null) loadChildFragment(OnlineFeedFragment.newInstance(feedurl, isShared)) } intent.hasExtra(Extras.search_string.name) -> { val query = intent.getStringExtra(Extras.search_string.name) @@ -810,6 +811,7 @@ class MainActivity : CastEnabledActivity() { add_to_back_stack, generated_view_id, search_string, + isShared } companion object { @@ -829,9 +831,10 @@ class MainActivity : CastEnabledActivity() { } @JvmStatic - fun showOnlineFeed(context: Context, feedUrl: String): Intent { + fun showOnlineFeed(context: Context, feedUrl: String, isShared: Boolean = false): Intent { val intent = Intent(context.applicationContext, MainActivity::class.java) intent.putExtra(Extras.fragment_feed_url.name, feedUrl) + intent.putExtra(Extras.isShared.name, isShared) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) return intent } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt index 708e42e0..cb800ede 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/ShareReceiverActivity.kt @@ -111,22 +111,22 @@ class ShareReceiverActivity : AppCompatActivity() { when { // plain text sharedUrl.matches(Regex("^[^\\s<>/]+\$")) -> { - if (log != null) upsertBlk(log) {it.type = "text" } + if (log != null) upsertBlk(log) {it.type = ShareLog.Type.Text.name } val intent = MainActivity.showOnlineSearch(activity, sharedUrl) activity.startActivity(intent) if (finish) activity.finish() } // Youtube media (isYoutubeURL(url) && (url.path.startsWith("/watch") || url.path.startsWith("/live"))) || isYoutubeServiceURL(url) -> { - if (log != null) upsertBlk(log) {it.type = "youtube media" } + if (log != null) upsertBlk(log) {it.type = ShareLog.Type.YTMedia.name } Logd(TAG, "got youtube media") mediaCB() } // podcast or Youtube channel, Youtube playlist, or other? else -> { - if (log != null) upsertBlk(log) {it.type = "podcast" } + if (log != null) upsertBlk(log) {it.type = ShareLog.Type.Podcast.name } Logd(TAG, "Activity was started with url $sharedUrl") - val intent = MainActivity.showOnlineFeed(activity, sharedUrl) + val intent = MainActivity.showOnlineFeed(activity, sharedUrl, true) activity.startActivity(intent) if (finish) activity.finish() } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/ChaptersDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/ChaptersDialog.kt index 73b29336..787f1c26 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/ChaptersDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/ChaptersDialog.kt @@ -20,9 +20,11 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +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 import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -58,7 +60,7 @@ fun ChaptersDialog(media: Playable, onDismissRequest: () -> Unit) { 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 - Icon(painter = painterResource(playRes), tint = textColor, contentDescription = "play button", + Icon(imageVector = ImageVector.vectorResource(playRes), tint = textColor, contentDescription = "play button", modifier = Modifier.width(28.dp).height(32.dp).clickable { if (MediaPlayerBase.status != PlayerStatus.PLAYING) playPause() seekTo(ch.start.toInt()) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt index f9d34da5..84cb9584 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/EpisodesVM.kt @@ -24,6 +24,7 @@ import ac.mdiq.podcini.storage.model.Feed.Companion.newId import ac.mdiq.podcini.storage.utils.DurationConverter import ac.mdiq.podcini.storage.utils.ImageResourceUtils import ac.mdiq.podcini.ui.actions.EpisodeActionButton +import ac.mdiq.podcini.ui.actions.EpisodeActionButton.Companion.forItem import ac.mdiq.podcini.ui.actions.SwipeAction import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.fragment.EpisodeInfoFragment @@ -58,7 +59,6 @@ import androidx.compose.material.icons.filled.Edit import androidx.compose.material3.* import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.* -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -67,7 +67,6 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.util.VelocityTracker 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.TextStyle @@ -79,7 +78,7 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.constraintlayout.compose.ConstraintLayout import androidx.documentfile.provider.DocumentFile -import coil.compose.rememberAsyncImagePainter +import coil.compose.AsyncImage import coil.request.CachePolicy import coil.request.ImageRequest import io.realm.kotlin.notifications.SingleQueryChange @@ -95,15 +94,25 @@ fun InforBar(text: MutableState, leftAction: MutableState, val textColor = MaterialTheme.colorScheme.onSurface Logd("InforBar", "textState: ${text.value}") Row { - Icon(painter = painterResource(leftAction.value.getActionIcon()), tint = textColor, contentDescription = "left_action_icon", - modifier = Modifier.width(24.dp).height(24.dp).clickable(onClick = actionConfig)) - Icon(painter = painterResource(R.drawable.baseline_arrow_left_alt_24), tint = textColor, contentDescription = "left_arrow", modifier = Modifier.width(24.dp).height(24.dp)) + 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)) Spacer(modifier = Modifier.weight(1f)) Text(text.value, color = textColor, style = MaterialTheme.typography.bodyMedium) Spacer(modifier = Modifier.weight(1f)) - Icon(painter = painterResource(R.drawable.baseline_arrow_right_alt_24), tint = textColor, contentDescription = "right_arrow", modifier = Modifier.width(24.dp).height(24.dp)) - Icon(painter = painterResource(rightAction.value.getActionIcon()), tint = textColor, contentDescription = "right_action_icon", - modifier = Modifier.width(24.dp).height(24.dp).clickable(onClick = actionConfig)) + 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)) } } @@ -117,8 +126,8 @@ class EpisodeVM(var episode: Episode) { var ratingState by mutableIntStateOf(episode.rating) var inProgressState by mutableStateOf(episode.isInProgress) var downloadState by mutableIntStateOf(if (episode.media?.downloaded == true) DownloadStatus.State.COMPLETED.ordinal else DownloadStatus.State.UNKNOWN.ordinal) - var actionButton by mutableStateOf(null) - var actionRes by mutableIntStateOf(R.drawable.ic_questionmark) + var actionButton by mutableStateOf(forItem(episode)) + var actionRes by mutableIntStateOf(actionButton.getDrawable()) var showAltActionsDialog by mutableStateOf(false) var dlPercent by mutableIntStateOf(0) var inQueueState by mutableStateOf(curQueue.contains(episode)) @@ -150,7 +159,7 @@ class EpisodeVM(var episode: Episode) { withContext(Dispatchers.Main) { playedState = changes.obj.isPlayed() ratingState = changes.obj.rating - episode = changes.obj // direct assignment doesn't update member like media?? +// episode = changes.obj // direct assignment doesn't update member like media?? } Logd("EpisodeVM", "episodeMonitor $playedState $playedState ") } else Logd("EpisodeVM", "episodeMonitor index out bound") @@ -174,7 +183,7 @@ class EpisodeVM(var episode: Episode) { positionState = changes.obj.media?.position ?: 0 inProgressState = changes.obj.isInProgress Logd("EpisodeVM", "mediaMonitor $positionState $inProgressState ${episode.title}") - episode = changes.obj +// episode = changes.obj // Logd("EpisodeVM", "mediaMonitor downloaded: ${changes.obj.media?.downloaded} ${episode.media?.downloaded}") } } else Logd("EpisodeVM", "mediaMonitor index out bound") @@ -185,6 +194,20 @@ 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 @@ -193,10 +216,12 @@ fun ChooseRatingDialog(selected: List, onDismissRequest: () -> Unit) { Surface(shape = RoundedCornerShape(16.dp)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { for (rating in Rating.entries.reversed()) { - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(4.dp).clickable { - for (item in selected) Episodes.setRating(item, rating.code) - onDismissRequest() - }) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier + .padding(4.dp) + .clickable { + for (item in selected) Episodes.setRating(item, rating.code) + onDismissRequest() + }) { Icon(imageVector = ImageVector.vectorResource(id = rating.res), "") Text(rating.name, Modifier.padding(start = 4.dp)) } @@ -264,7 +289,9 @@ fun ShelveDialog(selected: List, onDismissRequest: () -> Unit) { Dialog(onDismissRequest = onDismissRequest) { Surface(shape = RoundedCornerShape(16.dp)) { val scrollState = rememberScrollState() - Column(modifier = Modifier.verticalScroll(scrollState).padding(16.dp), verticalArrangement = Arrangement.spacedBy(1.dp)) { + Column(modifier = Modifier + .verticalScroll(scrollState) + .padding(16.dp), verticalArrangement = Arrangement.spacedBy(1.dp)) { var removeChecked by remember { mutableStateOf(false) } var toFeed by remember { mutableStateOf(null) } for (f in synthetics) { @@ -317,7 +344,7 @@ fun ShelveDialog(selected: List, onDismissRequest: () -> Unit) { @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable -fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList, feed: Feed? = null, +fun EpisodeLazyColumn(activity: MainActivity, vms: List, feed: Feed? = null, refreshCB: (()->Unit)? = null, leftSwipeCB: ((Episode) -> Unit)? = null, rightSwipeCB: ((Episode) -> Unit)? = null, actionButton_: ((Episode)-> EpisodeActionButton)? = null) { val TAG = "EpisodeLazyColumn" @@ -350,7 +377,6 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList, val message = stringResource(R.string.erase_episodes_confirmation_msg) val textColor = MaterialTheme.colorScheme.onSurface var textState by remember { mutableStateOf(TextFieldValue("")) } - val context = LocalContext.current Dialog(onDismissRequest = onDismissRequest) { Surface(shape = RoundedCornerShape(16.dp)) { @@ -359,7 +385,11 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList, Text(stringResource(R.string.feed_delete_reason_msg)) BasicTextField(value = textState, onValueChange = { textState = it }, textStyle = TextStyle(fontSize = 16.sp, color = textColor), - modifier = Modifier.fillMaxWidth().height(100.dp).padding(start = 10.dp, end = 10.dp, bottom = 10.dp).border(1.dp, MaterialTheme.colorScheme.primary, MaterialTheme.shapes.small) + modifier = Modifier + .fillMaxWidth() + .height(100.dp) + .padding(start = 10.dp, end = 10.dp, bottom = 10.dp) + .border(1.dp, MaterialTheme.colorScheme.primary, MaterialTheme.shapes.small) ) Button(onClick = { CoroutineScope(Dispatchers.IO).launch { @@ -403,7 +433,8 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList, fun EpisodeSpeedDial(modifier: Modifier = Modifier) { var isExpanded by remember { mutableStateOf(false) } val options = mutableListOf<@Composable () -> Unit>( - { Row(modifier = Modifier.padding(horizontal = 16.dp) + { Row(modifier = Modifier + .padding(horizontal = 16.dp) .clickable { isExpanded = false selectMode = false @@ -412,19 +443,22 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList, }, 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) + { 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() + 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) + { Row(modifier = Modifier + .padding(horizontal = 16.dp) .clickable { isExpanded = false selectMode = false @@ -433,7 +467,8 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList, }, verticalAlignment = Alignment.CenterVertically) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_mark_played), "Toggle played state") Text(stringResource(id = R.string.toggle_played_label)) } }, - { Row(modifier = Modifier.padding(horizontal = 16.dp) + { Row(modifier = Modifier + .padding(horizontal = 16.dp) .clickable { isExpanded = false selectMode = false @@ -442,7 +477,8 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList, }, 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) + { Row(modifier = Modifier + .padding(horizontal = 16.dp) .clickable { isExpanded = false selectMode = false @@ -451,7 +487,8 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList, }, 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) + { Row(modifier = Modifier + .padding(horizontal = 16.dp) .clickable { isExpanded = false selectMode = false @@ -460,7 +497,8 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList, }, 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) + { Row(modifier = Modifier + .padding(horizontal = 16.dp) .clickable { isExpanded = false selectMode = false @@ -469,7 +507,8 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList, }, 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) + { Row(modifier = Modifier + .padding(horizontal = 16.dp) .clickable { selectMode = false Logd(TAG, "ic_star: ${selected.size}") @@ -481,7 +520,8 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList, ) if (selected.isNotEmpty() && selected[0].isRemote.value) options.add { - Row(modifier = Modifier.padding(horizontal = 16.dp) + Row(modifier = Modifier + .padding(horizontal = 16.dp) .clickable { isExpanded = false selectMode = false @@ -507,7 +547,8 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList, } if (feed != null && feed.id <= MAX_SYNTHETIC_ID) { options.add { - Row(modifier = Modifier.padding(horizontal = 16.dp) + Row(modifier = Modifier + .padding(horizontal = 16.dp) .clickable { isExpanded = false selectMode = false @@ -523,7 +564,9 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList, 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), + FloatingActionButton(modifier = Modifier + .padding(start = 4.dp, bottom = 6.dp) + .height(40.dp), containerColor = Color.LightGray, onClick = {}) { button() } } @@ -533,14 +576,146 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList, } } + @Composable + fun MainRow(vm: EpisodeVM, index: Int) { + val textColor = MaterialTheme.colorScheme.onSurface + fun toggleSelected() { + vm.isSelected = !vm.isSelected + if (vm.isSelected) selected.add(vms[index].episode) + else selected.remove(vms[index].episode) + } + Row(Modifier.background(if (vm.isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)) { + if (false) { + val typedValue = TypedValue() + LocalContext.current.theme.resolveAttribute(R.attr.dragview_background, typedValue, true) + Icon(imageVector = ImageVector.vectorResource(typedValue.resourceId), tint = textColor, contentDescription = "drag handle", + modifier = Modifier.width(16.dp).align(Alignment.CenterVertically)) + } + ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) { + val (imgvCover, checkMark) = createRefs() + val imgLoc = remember(vm) { ImageResourceUtils.getEpisodeListImageLocation(vm.episode) } + Logd(TAG, "imgLoc: $imgLoc") + AsyncImage(model = ImageRequest.Builder(context).data(imgLoc) + .memoryCachePolicy(CachePolicy.ENABLED).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).build(), + contentDescription = "imgvCover", + modifier = Modifier.width(56.dp).height(56.dp) + .constrainAs(imgvCover) { + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + } + .clickable(onClick = { + Logd(TAG, "icon clicked!") + if (selectMode) toggleSelected() + else if (vm.episode.feed != null) activity.loadChildFragment(FeedInfoFragment.newInstance(vm.episode.feed!!)) + })) + val alpha = if (vm.playedState) 1.0f else 0f + if (vm.playedState) Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_check), tint = textColor, contentDescription = "played_mark", + modifier = Modifier.background(Color.Green).alpha(alpha) + .constrainAs(checkMark) { + bottom.linkTo(parent.bottom) + end.linkTo(parent.end) + }) + } + Column(Modifier.weight(1f).padding(start = 6.dp, end = 6.dp) + .combinedClickable(onClick = { + Logd(TAG, "clicked: ${vm.episode.title}") + if (selectMode) toggleSelected() + else activity.loadChildFragment(EpisodeInfoFragment.newInstance(vm.episode)) + }, onLongClick = { + selectMode = !selectMode + vm.isSelected = selectMode + selected.clear() + if (selectMode) { + selected.add(vms[index].episode) + longPressIndex = index + } else { + selectedSize = 0 + longPressIndex = -1 + } + Logd(TAG, "long clicked: ${vm.episode.title}") + })) { + LaunchedEffect(key1 = queueChanged) { + if (index >= vms.size) return@LaunchedEffect + vms[index].inQueueState = curQueue.contains(vms[index].episode) + } + Row(verticalAlignment = Alignment.CenterVertically) { + Logd(TAG, "info row") + if (vm.episode.media?.getMediaType() == MediaType.VIDEO) + Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_videocam), tint = textColor, contentDescription = "isVideo", modifier = Modifier.width(14.dp).height(14.dp)) + val ratingIconRes = Rating.fromCode(vm.ratingState).res + if (vm.ratingState != Rating.UNRATED.code) + Icon(imageVector = ImageVector.vectorResource(ratingIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating", modifier = Modifier.background(MaterialTheme.colorScheme.tertiaryContainer).width(14.dp).height(14.dp)) + if (vm.inQueueState) + Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_playlist_play), tint = textColor, contentDescription = "ivInPlaylist", modifier = Modifier.width(14.dp).height(14.dp)) + val curContext = LocalContext.current + val dur = remember { vm.episode.media?.getDuration() ?: 0 } + val durText = remember { DurationConverter.getDurationStringLong(dur) } + val dateSizeText = " · " + formatAbbrev(curContext, vm.episode.getPubDate()) + " · " + durText + " · " + + if ((vm.episode.media?.size ?: 0) > 0) Formatter.formatShortFileSize(curContext, vm.episode.media?.size ?: 0) else "" + Text(dateSizeText, color = textColor, style = MaterialTheme.typography.bodyMedium) + } + Text(vm.episode.title ?: "", color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis) + } + fun isDownloading(): Boolean { + return vms[index].downloadState > DownloadStatus.State.UNKNOWN.ordinal && vms[index].downloadState < DownloadStatus.State.COMPLETED.ordinal + } + if (actionButton_ == null) { + LaunchedEffect(key1 = status, key2 = vm.downloadState) { + if (index >= vms.size) return@LaunchedEffect + if (isDownloading()) vm.dlPercent = dls?.getProgress(vms[index].episode.media?.downloadUrl ?: "") ?: 0 + Logd(TAG, "LaunchedEffect $index isPlayingState: ${vms[index].isPlayingState} ${vms[index].episode.title}") + Logd(TAG, "LaunchedEffect $index downloadState: ${vms[index].downloadState} ${vm.episode.media?.downloaded} ${vm.dlPercent}") + vm.actionButton = forItem(vm.episode) +// vm.actionRes = vm.actionButton!!.getDrawable() + } + } else { + LaunchedEffect(vm.actionButton) { + Logd(TAG, "LaunchedEffect init actionButton") + vm.actionButton = actionButton_(vm.episode) +// vm.actionRes = vm.actionButton!!.getDrawable() + } + } + Box(contentAlignment = Alignment.Center, modifier = Modifier.width(40.dp).height(40.dp).padding(end = 10.dp).align(Alignment.CenterVertically) + .pointerInput(Unit) { + detectTapGestures(onLongPress = { vms[index].showAltActionsDialog = true }, + onTap = { vms[index].actionButton.onClick(activity) }) + }, ) { + Logd(TAG, "button box") + vm.actionRes = vm.actionButton.getDrawable() + Icon(imageVector = ImageVector.vectorResource(vm.actionRes), tint = textColor, contentDescription = null, modifier = Modifier.width(28.dp).height(32.dp)) + 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) vm.actionButton.AltActionsDialog(activity, vm.showAltActionsDialog, + onDismiss = { vm.showAltActionsDialog = false }) + } + } + + @Composable + fun ProgressRow(vm: EpisodeVM, index: Int) { + val textColor = MaterialTheme.colorScheme.onSurface + if (vm.inProgressState || InTheatre.isCurMedia(vm.episode.media)) { + val pos = vm.positionState + val dur = remember { vm.episode.media?.getDuration() ?: 0 } + val durText = remember { DurationConverter.getDurationStringLong(dur) } + vm.prog = if (dur > 0 && pos >= 0 && dur >= pos) 1.0f * pos / dur else 0f + Logd(TAG, "$index vm.prog: ${vm.prog}") + Row { + Text(DurationConverter.getDurationStringLong(vm.positionState), color = textColor, style = MaterialTheme.typography.bodySmall) + LinearProgressIndicator(progress = { vm.prog }, modifier = Modifier.weight(1f).height(4.dp).align(Alignment.CenterVertically)) + Text(durText, color = textColor, style = MaterialTheme.typography.bodySmall) + } + } + } + var refreshing by remember { mutableStateOf(false)} PullToRefreshBox(modifier = Modifier.fillMaxWidth(), isRefreshing = refreshing, indicator = {}, onRefresh = { refreshing = true refreshCB?.invoke() refreshing = false }) { - LazyColumn(state = lazyListState, modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp), - verticalArrangement = Arrangement.spacedBy(8.dp)) { + LazyColumn(state = lazyListState, modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { itemsIndexed(vms, key = {index, vm -> vm.episode.id}) { index, vm -> vm.startMonitoring() DisposableEffect(Unit) { @@ -549,180 +724,48 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList, vm.stopMonitoring() } } - LaunchedEffect(vm.actionButton) { - Logd(TAG, "LaunchedEffect init actionButton") - if (vm.actionButton == null) { - vm.actionButton = if (actionButton_ != null) actionButton_(vm.episode) else EpisodeActionButton.forItem(vm.episode) - vm.actionRes = vm.actionButton!!.getDrawable() - } - } val velocityTracker = remember { VelocityTracker() } val offsetX = remember { Animatable(0f) } Box(modifier = Modifier.fillMaxWidth().pointerInput(Unit) { - detectHorizontalDragGestures(onDragStart = { velocityTracker.resetTracking() }, - onHorizontalDrag = { change, dragAmount -> - velocityTracker.addPosition(change.uptimeMillis, change.position) - coroutineScope.launch { offsetX.snapTo(offsetX.value + dragAmount) } - }, - onDragEnd = { - coroutineScope.launch { - val velocity = velocityTracker.calculateVelocity().x - if (velocity > 1000f || velocity < -1000f) { - Logd(TAG, "velocity: $velocity") + Logd(TAG, "top box") + detectHorizontalDragGestures(onDragStart = { velocityTracker.resetTracking() }, + onHorizontalDrag = { change, dragAmount -> + Logd(TAG, "onHorizontalDrag $dragAmount") + velocityTracker.addPosition(change.uptimeMillis, change.position) + coroutineScope.launch { offsetX.snapTo(offsetX.value + dragAmount) } + }, + onDragEnd = { + coroutineScope.launch { + val velocity = velocityTracker.calculateVelocity().x + Logd(TAG, "velocity: $velocity") + if (velocity > 1000f || velocity < -1000f) { +// Logd(TAG, "velocity: $velocity") // if (velocity > 0) rightSwipeCB?.invoke(vms[index].episode) // else leftSwipeCB?.invoke(vms[index].episode) - if (velocity > 0) rightSwipeCB?.invoke(vm.episode) - else leftSwipeCB?.invoke(vm.episode) - } - offsetX.animateTo(targetValue = 0f, animationSpec = tween(500)) + if (velocity > 0) rightSwipeCB?.invoke(vm.episode) + else leftSwipeCB?.invoke(vm.episode) } + offsetX.animateTo(targetValue = 0f, animationSpec = tween(500)) } - ) - }.offset { IntOffset(offsetX.value.roundToInt(), 0) } - ) { + } + ) + }.offset { IntOffset(offsetX.value.roundToInt(), 0) }) { LaunchedEffect(key1 = selectMode, key2 = selectedSize) { vm.isSelected = selectMode && vm.episode in selected -// Logd(TAG, "LaunchedEffect $index $isSelected ${selected.size}") + Logd(TAG, "LaunchedEffect $index ${vm.isSelected} ${selected.size}") } - fun toggleSelected() { - vm.isSelected = !vm.isSelected - if (vm.isSelected) selected.add(vms[index].episode) - else selected.remove(vms[index].episode) - } - val textColor = MaterialTheme.colorScheme.onSurface Column { - val dur = remember { vm.episode.media?.getDuration() ?: 0 } - val durText = remember { DurationConverter.getDurationStringLong(dur) } - Row(Modifier.background(if (vm.isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)) { - if (false) { - val typedValue = TypedValue() - LocalContext.current.theme.resolveAttribute(R.attr.dragview_background, typedValue, true) - Icon(painter = painterResource(typedValue.resourceId), tint = textColor, contentDescription = "drag handle", - modifier = Modifier.width(16.dp).align(Alignment.CenterVertically)) - } - Logd(TAG, "episode.imageUrl: ${vm.episode.imageUrl}") - ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) { - val (imgvCover, checkMark) = createRefs() - val imgLoc = remember { ImageResourceUtils.getEpisodeListImageLocation(vm.episode) } - Logd(TAG, "imgLoc: $imgLoc") - val painter = rememberAsyncImagePainter(model = ImageRequest.Builder(context).data(imgLoc) - .memoryCachePolicy(CachePolicy.ENABLED) - .placeholder(R.mipmap.ic_launcher) - .error(R.mipmap.ic_launcher).build()) - Image(painter = painter, contentDescription = "imgvCover", - modifier = Modifier.width(56.dp).height(56.dp) - .constrainAs(imgvCover) { - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - start.linkTo(parent.start) - }.clickable(onClick = { - Logd(TAG, "icon clicked!") - if (selectMode) toggleSelected() - else if (vm.episode.feed != null) activity.loadChildFragment(FeedInfoFragment.newInstance(vm.episode.feed!!)) - }) - ) - val alpha = if (vm.playedState) 1.0f else 0f - if (vm.playedState) Icon(painter = painterResource(R.drawable.ic_check), tint = textColor, contentDescription = "played_mark", - modifier = Modifier.background(Color.Green).alpha(alpha).constrainAs(checkMark) { - bottom.linkTo(parent.bottom) - end.linkTo(parent.end) - }) - } - Column(Modifier.weight(1f).padding(start = 6.dp, end = 6.dp) - .combinedClickable(onClick = { - Logd(TAG, "clicked: ${vm.episode.title}") - if (selectMode) toggleSelected() - else activity.loadChildFragment(EpisodeInfoFragment.newInstance(vm.episode)) - }, onLongClick = { - selectMode = !selectMode - vm.isSelected = selectMode - selected.clear() - if (selectMode) { - selected.add(vms[index].episode) - longPressIndex = index - } else { - selectedSize = 0 - longPressIndex = -1 - } - Logd(TAG, "long clicked: ${vm.episode.title}") - })) { - LaunchedEffect(key1 = queueChanged) { - if (index >= vms.size) return@LaunchedEffect - vms[index].inQueueState = curQueue.contains(vms[index].episode) - } - Row(verticalAlignment = Alignment.CenterVertically) { - if (vm.episode.media?.getMediaType() == MediaType.VIDEO) - Icon(painter = painterResource(R.drawable.ic_videocam), tint = textColor, contentDescription = "isVideo", - modifier = Modifier.width(14.dp).height(14.dp)) - val ratingIconRes = Rating.fromCode(vm.ratingState).res - if (vm.ratingState != Rating.UNRATED.code) - Icon(painter = painterResource(ratingIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating", - modifier = Modifier.background(MaterialTheme.colorScheme.tertiaryContainer).width(14.dp).height(14.dp)) - if (vm.inQueueState) - Icon(painter = painterResource(R.drawable.ic_playlist_play), tint = textColor, contentDescription = "ivInPlaylist", - modifier = Modifier.width(14.dp).height(14.dp)) - val curContext = LocalContext.current - val dateSizeText = " · " + formatAbbrev(curContext, vm.episode.getPubDate()) + " · " + durText + " · " + - if ((vm.episode.media?.size ?: 0) > 0) Formatter.formatShortFileSize(curContext, vm.episode.media?.size ?: 0) else "" - Text(dateSizeText, color = textColor, style = MaterialTheme.typography.bodyMedium) - } - Text(vm.episode.title ?: "", color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis) - } - fun isDownloading(): Boolean { - return vms[index].downloadState > DownloadStatus.State.UNKNOWN.ordinal && vms[index].downloadState < DownloadStatus.State.COMPLETED.ordinal - } - if (actionButton_ == null) { - LaunchedEffect(vms[index].downloadState) { - if (index >= vms.size) return@LaunchedEffect - if (isDownloading()) vm.dlPercent = dls?.getProgress(vms[index].episode.media?.downloadUrl ?: "") ?: 0 - Logd(TAG, "LaunchedEffect $index downloadState: ${vms[index].downloadState} ${vm.episode.media?.downloaded} ${vm.dlPercent}") - vm.actionButton = EpisodeActionButton.forItem(vm.episode) - vm.actionRes = vm.actionButton!!.getDrawable() - } - LaunchedEffect(key1 = status) { - if (index >= vms.size) return@LaunchedEffect - Logd(TAG, "LaunchedEffect $index isPlayingState: ${vms[index].isPlayingState} ${vms[index].episode.title}") - vm.actionButton = EpisodeActionButton.forItem(vm.episode) - Logd(TAG, "LaunchedEffect vm.actionButton: ${vm.actionButton?.getLabel()}") - vm.actionRes = vm.actionButton!!.getDrawable() - } -// LaunchedEffect(vm.isPlayingState) { -// Logd(TAG, "LaunchedEffect isPlayingState: $index ${vms[index].isPlayingState} ${vm.isPlayingState}") -// vms[index].actionButton = EpisodeActionButton.forItem(vms[index].episode) -// vms[index].actionRes = vm.actionButton.getDrawable() -// } - } - Box(modifier = Modifier.width(40.dp).height(40.dp).padding(end = 10.dp) - .align(Alignment.CenterVertically).pointerInput(Unit) { - detectTapGestures(onLongPress = { vm.showAltActionsDialog = true }, onTap = { - vms[index].actionButton?.onClick(activity) - }) - }, contentAlignment = Alignment.Center) { -// actionRes = actionButton.getDrawable() - Icon(painter = painterResource(vm.actionRes), tint = textColor, contentDescription = null, modifier = Modifier.width(28.dp).height(32.dp)) - 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) vm.actionButton?.AltActionsDialog(activity, vm.showAltActionsDialog, - onDismiss = { vm.showAltActionsDialog = false }) - } - if (vm.inProgressState || InTheatre.isCurMedia(vm.episode.media)) { - val pos = vm.positionState - vm.prog = if (dur > 0 && pos >= 0 && dur >= pos) 1.0f * pos / dur else 0f - Logd(TAG, "$index vm.prog: ${vm.prog}") - Row { - Text(DurationConverter.getDurationStringLong(vm.positionState), color = textColor, style = MaterialTheme.typography.bodySmall) - LinearProgressIndicator(progress = { vm.prog }, modifier = Modifier.weight(1f).height(4.dp).align(Alignment.CenterVertically)) - Text(durText, color = textColor, style = MaterialTheme.typography.bodySmall) - } - } + MainRow(vm, index) + ProgressRow(vm, index) } } } } if (selectMode) { - Row(modifier = Modifier.align(Alignment.TopEnd).width(150.dp).height(45.dp).background(Color.LightGray), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) { - Icon(painter = painterResource(R.drawable.baseline_arrow_upward_24), tint = Color.Black, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp).padding(end = 10.dp) + Row(modifier = Modifier.align(Alignment.TopEnd).width(150.dp).height(45.dp) + .background(Color.LightGray), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) { + 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) { @@ -731,7 +774,8 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList, selectedSize = selected.size Logd(TAG, "selectedIds: ${selected.size}") })) - Icon(painter = painterResource(R.drawable.baseline_arrow_downward_24), tint = Color.Black, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp).padding(end = 10.dp) + 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.., Logd(TAG, "selectedIds: ${selected.size}") })) var selectAllRes by remember { mutableIntStateOf(R.drawable.ic_select_all) } - Icon(painter = painterResource(selectAllRes), tint = Color.Black, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp) + Icon(imageVector = ImageVector.vectorResource(selectAllRes), tint = Color.Black, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp) .clickable(onClick = { - if (selectedSize != vms.size) { - selected.clear() - for (vm in vms) { - selected.add(vm.episode) - } - selectAllRes = R.drawable.ic_select_none - } else { - selected.clear() - longPressIndex = -1 - selectAllRes = R.drawable.ic_select_all - } + if (selectedSize != vms.size) { + selected.clear() + for (vm in vms) { + selected.add(vm.episode) + } + selectAllRes = R.drawable.ic_select_none + } else { + selected.clear() + longPressIndex = -1 + selectAllRes = R.drawable.ic_select_all + } selectedSize = selected.size Logd(TAG, "selectedIds: ${selected.size}") })) @@ -790,7 +834,10 @@ fun ConfirmAddYoutubeEpisode(sharedUrls: List, showDialog: Boolean, onDi val info = StreamInfo.getInfo(Vista.getService(0), url) val episode = episodeFromStreamInfo(info) val status = addToYoutubeSyndicate(episode, !audioOnly) - if (log != null) upsert(log) { it.status = status } + if (log != null) upsert(log) { + it.title = episode.title + it.status = status + } } catch (e: Throwable) { toastMassege = "Receive share error: ${e.message}" Log.e(TAG, toastMassege) @@ -803,7 +850,8 @@ fun ConfirmAddYoutubeEpisode(sharedUrls: List, showDialog: Boolean, onDi }) { Text("Confirm") } - } else CircularProgressIndicator(progress = { 0.6f }, strokeWidth = 4.dp, modifier = Modifier.padding(start = 20.dp, end = 20.dp).width(30.dp).height(30.dp)) + } else CircularProgressIndicator(progress = { 0.6f }, strokeWidth = 4.dp, + modifier = Modifier.padding(start = 20.dp, end = 20.dp).width(30.dp).height(30.dp)) } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Feeds.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Feeds.kt index 0ba2526e..1dcc7fb8 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Feeds.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/compose/Feeds.kt @@ -30,7 +30,6 @@ import androidx.compose.ui.draw.alpha 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.TextStyle @@ -41,6 +40,8 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.constraintlayout.compose.ConstraintLayout import coil.compose.AsyncImage +import coil.request.CachePolicy +import coil.request.ImageRequest import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -160,6 +161,7 @@ fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult, log: Subsc showSubscribeDialog.value = false }) } + val context = LocalContext.current Column(Modifier.padding(start = 10.dp, end = 10.dp, top = 4.dp, bottom = 4.dp).combinedClickable( onClick = { if (feed.feedUrl != null) { @@ -182,8 +184,9 @@ fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult, log: Subsc Row { ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) { val (imgvCover, checkMark) = createRefs() - AsyncImage(model = feed.imageUrl, contentDescription = "imgvCover", - placeholder = painterResource(R.mipmap.ic_launcher), error = painterResource(R.mipmap.ic_launcher), + 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(), contentDescription = "imgvCover", modifier = Modifier.width(65.dp).height(65.dp).constrainAs(imgvCover) { top.linkTo(parent.top) bottom.linkTo(parent.bottom) @@ -194,7 +197,7 @@ fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult, log: Subsc Logd("OnlineFeedItem", "${feed.feedId} $log") val alpha = 1.0f val iRes = if (feed.feedId > 0) R.drawable.ic_check else R.drawable.baseline_clear_24 - Icon(painter = painterResource(iRes), tint = textColor, contentDescription = "played_mark", + Icon(imageVector = ImageVector.vectorResource(iRes), tint = textColor, contentDescription = "played_mark", modifier = Modifier.background(Color.Green).alpha(alpha).constrainAs(checkMark) { bottom.linkTo(parent.bottom) end.linkTo(parent.end) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/CustomFeedNameDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/CustomFeedNameDialog.kt index 95710dbc..3535e685 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/CustomFeedNameDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/CustomFeedNameDialog.kt @@ -20,7 +20,6 @@ class CustomFeedNameDialog(activity: Activity, private var feed: Feed) { fun show() { val activity = activityRef.get() ?: return - val binding = EditTextDialogBinding.inflate(LayoutInflater.from(activity)) val title = feed.title diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt index 5deb339b..51c7cf83 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt @@ -63,8 +63,11 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -78,6 +81,8 @@ import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import androidx.media3.session.MediaController import coil.compose.AsyncImage +import coil.request.CachePolicy +import coil.request.ImageRequest import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.Dispatchers @@ -108,6 +113,7 @@ class AudioPlayerFragment : Fragment() { private var showTimeLeft = false private var titleText by mutableStateOf("") private var imgLoc by mutableStateOf(null) + private var imgLocLarge by mutableStateOf(null) private var txtvPlaybackSpeed by mutableStateOf("") private var remainingTime by mutableIntStateOf(0) private var isVideoScreen = false @@ -173,141 +179,152 @@ class AudioPlayerFragment : Fragment() { } @OptIn(ExperimentalFoundationApi::class) + @Composable + fun ControlUI() { + val textColor = MaterialTheme.colorScheme.onSurface + val context = LocalContext.current + Row { + fun ensureService() { + if (curMedia == null) return + if (playbackService == null) PlaybackServiceStarter(requireContext(), curMedia!!).start() + } + val imgLoc_ = remember(currentItem) { imgLoc } + AsyncImage(model = ImageRequest.Builder(context).data(imgLoc_) + .memoryCachePolicy(CachePolicy.ENABLED).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).build(), + contentDescription = "imgvCover", + modifier = Modifier.width(65.dp).height(65.dp).padding(start = 5.dp) + .clickable(onClick = { + Logd(TAG, "playerUiFragment icon was clicked") + if (isCollapsed) { + val media = curMedia + if (media != null) { + val mediaType = media.getMediaType() + if (mediaType == MediaType.AUDIO || videoPlayMode == VideoMode.AUDIO_ONLY.code || videoMode == VideoMode.AUDIO_ONLY + || (media is EpisodeMedia && media.episode?.feed?.preferences?.videoModePolicy == VideoMode.AUDIO_ONLY)) { + Logd(TAG, "popping as audio episode") + ensureService() + (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED) + } else { + Logd(TAG, "popping video activity") + val intent = getPlayerActivityIntent(requireContext(), mediaType) + startActivity(intent) + } + } + } else (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED) + })) + Spacer(Modifier.weight(0.1f)) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_playback_speed), tint = textColor, + contentDescription = "speed", + modifier = Modifier.width(43.dp).height(43.dp).clickable(onClick = { + VariableSpeedDialog.newInstance(booleanArrayOf(true, true, true), null)?.show(childFragmentManager, null) + })) + Text(txtvPlaybackSpeed, color = textColor, style = MaterialTheme.typography.bodySmall) + } + Spacer(Modifier.weight(0.1f)) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_fast_rewind), tint = textColor, + contentDescription = "rewind", + modifier = Modifier.width(43.dp).height(43.dp).combinedClickable(onClick = { + if (controller != null && playbackService?.isServiceReady() == true) + playbackService?.mPlayer?.seekDelta(-UserPreferences.rewindSecs * 1000) + }, onLongClick = { + SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND) + })) + val rewindSecs = remember { NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong()) } + Text(rewindSecs, color = textColor, style = MaterialTheme.typography.bodySmall) + } + Spacer(Modifier.weight(0.1f)) + Icon(imageVector = ImageVector.vectorResource(playButRes), tint = textColor, contentDescription = "play", + modifier = Modifier.width(64.dp).height(64.dp).combinedClickable(onClick = { + if (controller == null) return@combinedClickable + if (curMedia != null) { + val media = curMedia!! + setIsShowPlay(!isShowPlay) + if (media.getMediaType() == MediaType.VIDEO && status != PlayerStatus.PLAYING && + (media is EpisodeMedia && media.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY)) { + playPause() + requireContext().startActivity(getPlayerActivityIntent(requireContext(), curMedia!!.getMediaType())) + } else playPause() + } + }, onLongClick = { + if (controller != null && status == PlayerStatus.PLAYING) { + val fallbackSpeed = UserPreferences.fallbackSpeed + if (fallbackSpeed > 0.1f) toggleFallbackSpeed(fallbackSpeed) + } + })) + Spacer(Modifier.weight(0.1f)) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_fast_forward), tint = textColor, + contentDescription = "forward", + modifier = Modifier.width(43.dp).height(43.dp).combinedClickable(onClick = { + if (controller != null && playbackService?.isServiceReady() == true) + playbackService?.mPlayer?.seekDelta(UserPreferences.fastForwardSecs * 1000) + }, onLongClick = { + SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_FORWARD) + })) + val fastForwardSecs = remember { NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong()) } + Text(fastForwardSecs, color = textColor, style = MaterialTheme.typography.bodySmall) + } + Spacer(Modifier.weight(0.1f)) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + fun speedForward(speed: Float) { + if (playbackService?.mPlayer == null || playbackService?.isFallbackSpeed == true) return + if (playbackService?.isSpeedForward == false) { + playbackService?.normalSpeed = playbackService?.mPlayer!!.getPlaybackSpeed() + playbackService?.mPlayer!!.setPlaybackParams(speed, isSkipSilence) + } else playbackService?.mPlayer?.setPlaybackParams(playbackService!!.normalSpeed, isSkipSilence) + playbackService!!.isSpeedForward = !playbackService!!.isSpeedForward + } + Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_skip_48dp), tint = textColor, + contentDescription = "rewind", + modifier = Modifier.width(43.dp).height(43.dp).combinedClickable(onClick = { + if (controller != null && status == PlayerStatus.PLAYING) { + val speedForward = UserPreferences.speedforwardSpeed + if (speedForward > 0.1f) speedForward(speedForward) + } + }, onLongClick = { + activity?.sendBroadcast(MediaButtonReceiver.createIntent(requireContext(), KeyEvent.KEYCODE_MEDIA_NEXT)) + })) + if (UserPreferences.speedforwardSpeed > 0.1f) Text(NumberFormat.getInstance().format(UserPreferences.speedforwardSpeed), color = textColor, style = MaterialTheme.typography.bodySmall) + } + Spacer(Modifier.weight(0.1f)) + } + } + + @Composable + fun ProgressBar() { + val textColor = MaterialTheme.colorScheme.onSurface + Slider(value = sliderValue, valueRange = 0f..duration.toFloat(), + modifier = Modifier.height(12.dp).padding(top = 2.dp, bottom = 2.dp), + onValueChange = { + Logd(TAG, "Slider onValueChange: $it") + sliderValue = it + }, onValueChangeFinished = { + Logd(TAG, "Slider onValueChangeFinished: $sliderValue") + currentPosition = sliderValue.toInt() + if (playbackService?.isServiceReady() == true) seekTo(currentPosition) + }) + Row { + Text(DurationConverter.getDurationStringLong(currentPosition), color = textColor, style = MaterialTheme.typography.bodySmall) + Spacer(Modifier.weight(1f)) + showTimeLeft = UserPreferences.shouldShowRemainingTime() + Text(txtvLengtTexth, color = textColor, style = MaterialTheme.typography.bodySmall, modifier = Modifier.clickable { + if (controller == null) return@clickable + showTimeLeft = !showTimeLeft + UserPreferences.setShowRemainTimeSetting(showTimeLeft) + onPositionUpdate(FlowEvent.PlaybackPositionEvent(curMedia, curPositionFB, curDurationFB)) + }) + } + } + @Composable fun PlayerUI(modifier: Modifier) { val textColor = MaterialTheme.colorScheme.onSurface Column(modifier = modifier.fillMaxWidth().background(MaterialTheme.colorScheme.surface)) { Text(titleText, maxLines = 1, color = textColor, style = MaterialTheme.typography.bodyMedium) - Slider(value = sliderValue, valueRange = 0f..duration.toFloat(), -// colors = SliderDefaults.colors( -// thumbColor = MaterialTheme.colorScheme.secondary, -// activeTrackColor = MaterialTheme.colorScheme.secondary, -// inactiveTrackColor = Color.Gray, -// ), - modifier = Modifier.height(12.dp).padding(top = 2.dp, bottom = 2.dp), - onValueChange = { - Logd(TAG, "Slider onValueChange: $it") - sliderValue = it - }, onValueChangeFinished = { - Logd(TAG, "Slider onValueChangeFinished: $sliderValue") - currentPosition = sliderValue.toInt() - if (playbackService?.isServiceReady() == true) seekTo(currentPosition) - }) - Row { - Text(DurationConverter.getDurationStringLong(currentPosition), color = textColor, style = MaterialTheme.typography.bodySmall) - Spacer(Modifier.weight(1f)) - showTimeLeft = UserPreferences.shouldShowRemainingTime() - Text(txtvLengtTexth, color = textColor, style = MaterialTheme.typography.bodySmall, modifier = Modifier.clickable { - if (controller == null) return@clickable - showTimeLeft = !showTimeLeft - UserPreferences.setShowRemainTimeSetting(showTimeLeft) - onPositionUpdate(FlowEvent.PlaybackPositionEvent(curMedia, curPositionFB, curDurationFB)) - }) - } - Row { - fun ensureService() { - if (curMedia == null) return - if (playbackService == null) PlaybackServiceStarter(requireContext(), curMedia!!).start() - } - AsyncImage(model = imgLoc, contentDescription = "imgvCover", placeholder = painterResource(R.mipmap.ic_launcher), error = painterResource(R.mipmap.ic_launcher), - modifier = Modifier.width(65.dp).height(65.dp).padding(start = 5.dp) - .clickable(onClick = { - Logd(TAG, "playerUiFragment icon was clicked") - if (isCollapsed) { - val media = curMedia - if (media != null) { - val mediaType = media.getMediaType() - if (mediaType == MediaType.AUDIO || videoPlayMode == VideoMode.AUDIO_ONLY.code || videoMode == VideoMode.AUDIO_ONLY - || (media is EpisodeMedia && media.episode?.feed?.preferences?.videoModePolicy == VideoMode.AUDIO_ONLY)) { - Logd(TAG, "popping as audio episode") - ensureService() - (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED) - } else { - Logd(TAG, "popping video activity") - val intent = getPlayerActivityIntent(requireContext(), mediaType) - startActivity(intent) - } - } - } else (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED) - })) - Spacer(Modifier.weight(0.1f)) - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Icon(painter = painterResource(R.drawable.ic_playback_speed), tint = textColor, - contentDescription = "speed", - modifier = Modifier.width(43.dp).height(43.dp).clickable(onClick = { - VariableSpeedDialog.newInstance(booleanArrayOf(true, true, true), null)?.show(childFragmentManager, null) - })) - Text(txtvPlaybackSpeed, color = textColor, style = MaterialTheme.typography.bodySmall) - } - Spacer(Modifier.weight(0.1f)) - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Icon(painter = painterResource(R.drawable.ic_fast_rewind), tint = textColor, - contentDescription = "rewind", - modifier = Modifier.width(43.dp).height(43.dp).combinedClickable(onClick = { - if (controller != null && playbackService?.isServiceReady() == true) { - playbackService?.mPlayer?.seekDelta(-UserPreferences.rewindSecs * 1000) - } - }, onLongClick = { - SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND) - })) - Text(NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong()), color = textColor, style = MaterialTheme.typography.bodySmall) - } - Spacer(Modifier.weight(0.1f)) - Icon(painter = painterResource(playButRes), tint = textColor, contentDescription = "play", - modifier = Modifier.width(64.dp).height(64.dp).combinedClickable(onClick = { - if (controller == null) return@combinedClickable - if (curMedia != null) { - val media = curMedia!! - setIsShowPlay(!isShowPlay) - if (media.getMediaType() == MediaType.VIDEO && status != PlayerStatus.PLAYING && - (media is EpisodeMedia && media.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY)) { - playPause() - requireContext().startActivity(getPlayerActivityIntent(requireContext(), curMedia!!.getMediaType())) - } else playPause() - } - }, onLongClick = { - if (controller != null && status == PlayerStatus.PLAYING) { - val fallbackSpeed = UserPreferences.fallbackSpeed - if (fallbackSpeed > 0.1f) toggleFallbackSpeed(fallbackSpeed) - } - })) - Spacer(Modifier.weight(0.1f)) - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Icon(painter = painterResource(R.drawable.ic_fast_forward), tint = textColor, - contentDescription = "forward", - modifier = Modifier.width(43.dp).height(43.dp).combinedClickable(onClick = { - if (controller != null && playbackService?.isServiceReady() == true) { - playbackService?.mPlayer?.seekDelta(UserPreferences.fastForwardSecs * 1000) - } - }, onLongClick = { - SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_FORWARD) - })) - Text(NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong()), color = textColor, style = MaterialTheme.typography.bodySmall) - } - Spacer(Modifier.weight(0.1f)) - Column(horizontalAlignment = Alignment.CenterHorizontally) { - fun speedForward(speed: Float) { - if (playbackService?.mPlayer == null || playbackService?.isFallbackSpeed == true) return - if (playbackService?.isSpeedForward == false) { - playbackService?.normalSpeed = playbackService?.mPlayer!!.getPlaybackSpeed() - playbackService?.mPlayer!!.setPlaybackParams(speed, isSkipSilence) - } else playbackService?.mPlayer?.setPlaybackParams(playbackService!!.normalSpeed, isSkipSilence) - playbackService!!.isSpeedForward = !playbackService!!.isSpeedForward - } - Icon(painter = painterResource(R.drawable.ic_skip_48dp), tint = textColor, - contentDescription = "rewind", - modifier = Modifier.width(43.dp).height(43.dp).combinedClickable(onClick = { - if (controller != null && status == PlayerStatus.PLAYING) { - val speedForward = UserPreferences.speedforwardSpeed - if (speedForward > 0.1f) speedForward(speedForward) - } - }, onLongClick = { - activity?.sendBroadcast(MediaButtonReceiver.createIntent(requireContext(), KeyEvent.KEYCODE_MEDIA_NEXT)) - })) - if (UserPreferences.speedforwardSpeed > 0.1f) Text(NumberFormat.getInstance().format(UserPreferences.speedforwardSpeed), color = textColor, style = MaterialTheme.typography.bodySmall) - } - Spacer(Modifier.weight(0.1f)) - } + ProgressBar() + ControlUI() } } @@ -319,19 +336,17 @@ class AudioPlayerFragment : Fragment() { val mediaType = curMedia?.getMediaType() val notAudioOnly = (curMedia as? EpisodeMedia)?.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY Row(modifier = Modifier.fillMaxWidth().padding(10.dp), horizontalArrangement = Arrangement.SpaceBetween) { - Icon(painter = painterResource(R.drawable.ic_arrow_down), tint = textColor, contentDescription = "Collapse", modifier = Modifier.clickable { + Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_arrow_down), tint = textColor, contentDescription = "Collapse", modifier = Modifier.clickable { (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED) }) var homeIcon by remember { mutableIntStateOf(R.drawable.baseline_home_24)} - Icon(painter = painterResource(homeIcon), tint = textColor, contentDescription = "Home", modifier = Modifier.clickable { + Icon(imageVector = ImageVector.vectorResource(homeIcon), tint = textColor, contentDescription = "Home", modifier = Modifier.clickable { homeIcon = if (showHomeText) R.drawable.ic_home else R.drawable.outline_home_24 buildHomeReaderText() }) - if (mediaType == MediaType.VIDEO) Icon(painter = painterResource(R.drawable.baseline_fullscreen_24), tint = textColor, contentDescription = "Play video", + if (mediaType == MediaType.VIDEO) Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_fullscreen_24), tint = textColor, contentDescription = "Play video", modifier = Modifier.clickable { - if (notAudioOnly || (curMedia as? EpisodeMedia)?.forceVideo == true) { -// playPause() - } else { + if (!notAudioOnly && (curMedia as? EpisodeMedia)?.forceVideo != true) { (curMedia as? EpisodeMedia)?.forceVideo = true status = PlayerStatus.STOPPED playbackService?.mPlayer?.pause(true, reinit = true) @@ -341,24 +356,24 @@ class AudioPlayerFragment : Fragment() { }) if (controller != null) { val sleepRes = if (sleepTimerActive) R.drawable.ic_sleep_off else R.drawable.ic_sleep - Icon(painter = painterResource(sleepRes), tint = textColor, contentDescription = "Sleep timer", modifier = Modifier.clickable { + Icon(imageVector = ImageVector.vectorResource(sleepRes), tint = textColor, contentDescription = "Sleep timer", modifier = Modifier.clickable { SleepTimerDialog().show(childFragmentManager, "SleepTimerDialog") }) } - if (currentMedia is EpisodeMedia) Icon(painter = painterResource(R.drawable.ic_feed), tint = textColor, contentDescription = "Open podcast", + if (currentMedia is EpisodeMedia) Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_feed), tint = textColor, contentDescription = "Open podcast", modifier = Modifier.clickable { if (feedItem?.feedId != null) { val intent: Intent = MainActivity.getIntentToOpenFeed(requireContext(), feedItem.feedId!!) startActivity(intent) } }) - Icon(painter = painterResource(R.drawable.ic_share), tint = textColor, contentDescription = "Share", modifier = Modifier.clickable { + Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_share), tint = textColor, contentDescription = "Share", modifier = Modifier.clickable { if (currentItem != null) { val shareDialog: ShareDialog = ShareDialog.newInstance(currentItem!!) shareDialog.show((requireActivity().supportFragmentManager), "ShareEpisodeDialog") } }) - Icon(painter = painterResource(R.drawable.baseline_offline_share_24), tint = textColor, contentDescription = "Share Note", modifier = Modifier.clickable { + Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_offline_share_24), tint = textColor, contentDescription = "Share Note", modifier = Modifier.clickable { val notes = if (showHomeText) readerhtml else feedItem?.description if (!notes.isNullOrEmpty()) { val shareText = HtmlCompat.fromHtml(notes, HtmlCompat.FROM_HTML_MODE_COMPACT).toString() @@ -407,7 +422,7 @@ class AudioPlayerFragment : Fragment() { Row(modifier = Modifier.fillMaxWidth().padding(top = 2.dp, bottom = 2.dp)) { Spacer(modifier = Modifier.weight(0.2f)) val ratingIconRes = Rating.fromCode(rating).res - Icon(painter = painterResource(ratingIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating", + Icon(imageVector = ImageVector.vectorResource(ratingIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating", modifier = Modifier.background(MaterialTheme.colorScheme.tertiaryContainer).width(24.dp).height(24.dp).clickable(onClick = { showChooseRatingDialog = true })) @@ -453,7 +468,7 @@ class AudioPlayerFragment : Fragment() { if (displayedChapterIndex >= 0) { Row(modifier = Modifier.padding(start = 20.dp, end = 20.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) { - Icon(painter = painterResource(R.drawable.ic_chapter_prev), tint = textColor, contentDescription = "prev_chapter", + Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_chapter_prev), tint = textColor, contentDescription = "prev_chapter", modifier = Modifier.width(36.dp).height(36.dp).clickable(onClick = { seekToPrevChapter() })) Text("Ch " + displayedChapterIndex.toString() + ": " + currentChapter?.title, color = textColor, style = MaterialTheme.typography.bodyMedium, @@ -461,11 +476,11 @@ class AudioPlayerFragment : Fragment() { modifier = Modifier.weight(1f).padding(start = 10.dp, end = 10.dp) // .clickable(onClick = { ChaptersFragment().show(childFragmentManager, ChaptersFragment.TAG) })) .clickable(onClick = { showChaptersDialog = true })) - if (hasNextChapter) Icon(painter = painterResource(R.drawable.ic_chapter_next), tint = textColor, contentDescription = "next_chapter", + if (hasNextChapter) Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_chapter_next), tint = textColor, contentDescription = "next_chapter", modifier = Modifier.width(36.dp).height(36.dp).clickable(onClick = { seekToNextChapter() })) } } - AsyncImage(model = imgLoc, contentDescription = "imgvCover", placeholder = painterResource(R.mipmap.ic_launcher), error = painterResource(R.mipmap.ic_launcher), + AsyncImage(model = imgLocLarge, contentDescription = "imgvCover", placeholder = painterResource(R.mipmap.ic_launcher), error = painterResource(R.mipmap.ic_launcher), modifier = Modifier.fillMaxWidth().padding(start = 32.dp, end = 32.dp, top = 10.dp).clickable(onClick = { })) } @@ -489,6 +504,7 @@ class AudioPlayerFragment : Fragment() { } @UnstableApi fun onPositionUpdate(event: FlowEvent.PlaybackPositionEvent) { + Logd(TAG, "onPositionUpdate") if (curMedia?.getIdentifier() != event.media?.getIdentifier() || controller == null || curPositionFB == Playable.INVALID_TIME || curDurationFB == Playable.INVALID_TIME) return val converter = TimeSpeedConverter(curSpeedFB) currentPosition = converter.convert(event.position) @@ -499,11 +515,9 @@ class AudioPlayerFragment : Fragment() { return } showTimeLeft = UserPreferences.shouldShowRemainingTime() - txtvLengtTexth = if (showTimeLeft) { - (if (remainingTime > 0) "-" else "") + DurationConverter.getDurationStringLong(remainingTime) - } else DurationConverter.getDurationStringLong(duration) + txtvLengtTexth = if (showTimeLeft) (if (remainingTime > 0) "-" else "") + DurationConverter.getDurationStringLong(remainingTime) + else DurationConverter.getDurationStringLong(duration) -// val progress: Float = (event.position.toFloat()) / event.duration sliderValue = event.position.toFloat() } private fun onPlaybackServiceChanged(event: FlowEvent.PlaybackServiceEvent) { @@ -645,7 +659,7 @@ class AudioPlayerFragment : Fragment() { private fun displayCoverImage() { if (currentMedia == null) return - imgLoc = if (displayedChapterIndex == -1 || currentMedia!!.getChapters().isEmpty() || currentMedia!!.getChapters()[displayedChapterIndex].imageUrl.isNullOrEmpty()) + imgLocLarge = if (displayedChapterIndex == -1 || currentMedia!!.getChapters().isEmpty() || currentMedia!!.getChapters()[displayedChapterIndex].imageUrl.isNullOrEmpty()) currentMedia!!.getImageLocation() else EmbeddedChapterImage.getModelFor(currentMedia!!, displayedChapterIndex)?.toString() Logd(TAG, "displayCoverImage: imgLoc: $imgLoc") } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt index 6b9bef1a..578304b7 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt @@ -71,8 +71,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector 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 import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextOverflow @@ -199,7 +201,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { Row(modifier = Modifier.padding(top = 4.dp), verticalAlignment = Alignment.CenterVertically) { Spacer(modifier = Modifier.weight(0.4f)) val playedIconRes = if (!isPlayed) R.drawable.ic_mark_unplayed else R.drawable.ic_mark_played - Icon(painter = painterResource(playedIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "isPlayed", + Icon(imageVector = ImageVector.vectorResource(playedIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "isPlayed", modifier = Modifier.background(MaterialTheme.colorScheme.tertiaryContainer).width(24.dp).height(24.dp) .clickable(onClick = { if (isPlayed) { @@ -228,7 +230,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { if (episode?.media != null) { Spacer(modifier = Modifier.weight(0.2f)) val inQueueIconRes = if (inQueue) R.drawable.ic_playlist_play else R.drawable.ic_playlist_remove - Icon(painter = painterResource(inQueueIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "inQueue", + Icon(imageVector = ImageVector.vectorResource(inQueueIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "inQueue", modifier = Modifier.background(MaterialTheme.colorScheme.tertiaryContainer).width(24.dp).height(24.dp).clickable(onClick = { if (inQueue) removeFromQueue(episode!!) else addToQueue(true, episode!!) })) @@ -236,12 +238,12 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { Spacer(modifier = Modifier.weight(0.2f)) Logd(TAG, "ratingIconRes rating: $rating") val ratingIconRes = Rating.fromCode(rating).res - Icon(painter = painterResource(ratingIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating", + Icon(imageVector = ImageVector.vectorResource(ratingIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating", modifier = Modifier.background(MaterialTheme.colorScheme.tertiaryContainer).width(24.dp).height(24.dp).clickable(onClick = { showChooseRatingDialog = true })) Spacer(modifier = Modifier.weight(0.2f)) - if (hasMedia) Icon(painter = painterResource(actionButton1?.getDrawable()?: R.drawable.ic_questionmark), tint = textColor, contentDescription = "butAction1", + if (hasMedia) Icon(imageVector = ImageVector.vectorResource(actionButton1?.getDrawable()?: R.drawable.ic_questionmark), tint = textColor, contentDescription = "butAction1", modifier = Modifier.width(24.dp).height(24.dp).clickable(onClick = { when { actionButton1 is StreamActionButton && !UserPreferences.isStreamOverDownload @@ -254,7 +256,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { } })) Spacer(modifier = Modifier.weight(0.2f)) - Icon(painter = painterResource(R.drawable.baseline_home_work_24), tint = textColor, contentDescription = "homeButton", + Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_home_work_24), tint = textColor, contentDescription = "homeButton", modifier = Modifier.width(24.dp).height(24.dp).clickable(onClick = { if (!episode?.link.isNullOrEmpty()) { homeFragment = EpisodeHomeFragment.newInstance(episode!!) @@ -263,7 +265,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { })) Spacer(modifier = Modifier.weight(0.2f)) Box(modifier = Modifier.width(40.dp).height(40.dp).align(Alignment.CenterVertically), contentAlignment = Alignment.Center) { - Icon(painter = painterResource(actionButton2?.getDrawable()?: R.drawable.ic_questionmark), tint = textColor, contentDescription = "butAction2", modifier = Modifier.width(24.dp).height(24.dp).clickable { + Icon(imageVector = ImageVector.vectorResource(actionButton2?.getDrawable()?: R.drawable.ic_questionmark), tint = textColor, contentDescription = "butAction2", modifier = Modifier.width(24.dp).height(24.dp).clickable { when { actionButton2 is DownloadActionButton && UserPreferences.isStreamOverDownload && UsageStatistics.hasSignificantBiasTo(UsageStatistics.ACTION_DOWNLOAD) -> { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt index a107ff4c..cf4e3c87 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedEpisodesFragment.kt @@ -46,8 +46,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.blur import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -249,10 +251,10 @@ import java.util.concurrent.Semaphore start.linkTo(parent.start) }, verticalAlignment = Alignment.CenterVertically) { Spacer(modifier = Modifier.weight(0.7f)) - Icon(painter = painterResource(R.drawable.ic_filter_white), tint = if (filterButColor == Color.White) textColor else filterButColor, contentDescription = "butFilter", + Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_filter_white), tint = if (filterButColor == Color.White) textColor else filterButColor, contentDescription = "butFilter", modifier = Modifier.width(40.dp).height(40.dp).padding(3.dp).combinedClickable(onClick = filterClickCB, onLongClick = filterLongClickCB)) Spacer(modifier = Modifier.width(15.dp)) - Icon(painter = painterResource(R.drawable.ic_settings_white), tint = textColor, contentDescription = "butShowSettings", + Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_settings_white), tint = textColor, contentDescription = "butShowSettings", modifier = Modifier.width(40.dp).height(40.dp).padding(3.dp).clickable(onClick = { if (feed != null) { val fragment = FeedSettingsFragment.newInstance(feed) @@ -262,12 +264,12 @@ import java.util.concurrent.Semaphore Spacer(modifier = Modifier.weight(0.5f)) Text(episodes.size.toString() + " / " + feed?.episodes?.size?.toString(), textAlign = TextAlign.Center, color = Color.White, style = MaterialTheme.typography.bodyLarge) } -// Image(painter = painterResource(R.drawable.ic_rounded_corner_left), contentDescription = "left_corner", +// Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_rounded_corner_left), contentDescription = "left_corner", // Modifier.width(12.dp).height(12.dp).constrainAs(image1) { // bottom.linkTo(parent.bottom) // start.linkTo(parent.start) // }) -// Image(painter = painterResource(R.drawable.ic_rounded_corner_right), contentDescription = "right_corner", +// Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_rounded_corner_right), contentDescription = "right_corner", // Modifier.width(12.dp).height(12.dp).constrainAs(image2) { // bottom.linkTo(parent.bottom) // end.linkTo(parent.end) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt index 751a6668..cd9ecee7 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt @@ -52,9 +52,11 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.blur +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale 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 import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextOverflow @@ -173,12 +175,12 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { }, verticalAlignment = Alignment.CenterVertically) { Spacer(modifier = Modifier.weight(1f)) val ratingIconRes = Rating.fromCode(rating).res - Icon(painter = painterResource(ratingIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating", + Icon(imageVector = ImageVector.vectorResource(ratingIconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating", modifier = Modifier.background(MaterialTheme.colorScheme.tertiaryContainer).width(30.dp).height(30.dp).clickable(onClick = { showChooseRatingDialog = true })) Spacer(modifier = Modifier.weight(0.2f)) - Icon(painter = painterResource(R.drawable.ic_settings_white), tint = textColor, contentDescription = "butShowSettings", + Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_settings_white), tint = textColor, contentDescription = "butShowSettings", modifier = Modifier.width(40.dp).height(40.dp).padding(3.dp).clickable(onClick = { (activity as MainActivity).loadChildFragment(FeedSettingsFragment.newInstance(feed), TransitionEffect.SLIDE) })) @@ -188,12 +190,12 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { } Spacer(modifier = Modifier.width(15.dp)) } -// Image(painter = painterResource(R.drawable.ic_rounded_corner_left), contentDescription = "left_corner", +// Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_rounded_corner_left), contentDescription = "left_corner", // Modifier.width(12.dp).height(12.dp).constrainAs(image1) { // bottom.linkTo(parent.bottom) // start.linkTo(parent.start) // }) -// Image(painter = painterResource(R.drawable.ic_rounded_corner_right), contentDescription = "right_corner", +// Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_rounded_corner_right), contentDescription = "right_corner", // Modifier.width(12.dp).height(12.dp).constrainAs(image2) { // bottom.linkTo(parent.bottom) // end.linkTo(parent.end) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/LogsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/LogsFragment.kt index 1ed40388..5a2f6c5f 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/LogsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/LogsFragment.kt @@ -5,6 +5,7 @@ import ac.mdiq.podcini.databinding.LogsFragmentBinding import ac.mdiq.podcini.net.feed.FeedUpdateManager import ac.mdiq.podcini.storage.database.Episodes.getEpisodeByGuidOrUrl import ac.mdiq.podcini.storage.database.Feeds.getFeed +import ac.mdiq.podcini.storage.database.Feeds.getFeedByTitleAndAuthor import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope import ac.mdiq.podcini.storage.model.* @@ -49,8 +50,10 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -124,12 +127,27 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener { } } else { Logd(TAG, "shared log url: ${log.url}") -// val episode = getEpisodeByGuidOrUrl(null, log.url!!, false) -// if (episode != null) (activity as MainActivity).loadChildFragment(EpisodeInfoFragment.newInstance(episode)) -// else { + var hasError = false + when(log.type) { + ShareLog.Type.YTMedia.name, "youtube media" -> { + val episode = realm.query(Episode::class).query("title == $0", log.title).first().find() + if (episode != null) (activity as MainActivity).loadChildFragment(EpisodeInfoFragment.newInstance(episode)) + else hasError = true + } + ShareLog.Type.Podcast.name, "podcast" -> { + val feed = getFeedByTitleAndAuthor(log.title?:"", log.author?:"") + if (feed != null ) (activity as MainActivity).loadChildFragment(FeedInfoFragment.newInstance(feed)) + else hasError = true + } + else -> { + showSharedDialog.value = true + sharedlogState.value = log + } + } + if (hasError) { showSharedDialog.value = true sharedlogState.value = log -// } + } } }) { Column { @@ -141,12 +159,13 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener { Spacer(Modifier.weight(1f)) var showAction by remember { mutableStateOf(log.status < ShareLog.Status.SUCCESS.ordinal) } if (true || showAction) { - Icon(painter = painterResource(R.drawable.ic_delete), tint = textColor, contentDescription = null, + Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_delete), tint = textColor, contentDescription = null, modifier = Modifier.width(25.dp).height(25.dp).clickable { }) } } - Text(log.url?:"unknown", color = textColor) + Text(log.title?:"unknown title", color = textColor) + Text(log.url?:"unknown url", color = textColor) val statusText = when (log.status) { ShareLog.Status.ERROR.ordinal -> ShareLog.Status.ERROR.name ShareLog.Status.SUCCESS.ordinal -> ShareLog.Status.SUCCESS.name @@ -185,7 +204,7 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener { showDialog.value = true }) { val iconRes = remember { fromCode(log.rating).res } - Icon(painter = painterResource(iconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating", + Icon(imageVector = ImageVector.vectorResource(iconRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating", modifier = Modifier.background(MaterialTheme.colorScheme.tertiaryContainer).width(40.dp).height(40.dp).padding(end = 15.dp)) Column { Text(log.type + ": " + formatDateTimeFlex(Date(log.id)) + " -- " + formatDateTimeFlex(Date(log.cancelDate)), color = textColor) @@ -245,7 +264,7 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener { } var showAction by remember { mutableStateOf(!status.isSuccessful && !newerWasSuccessful(position, status.feedfileType, status.feedfileId)) } if (showAction) { - Icon(painter = painterResource(R.drawable.ic_refresh), + Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_refresh), tint = textColor, contentDescription = null, modifier = Modifier.width(28.dp).height(32.dp).clickable { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt index cf85e7d7..205495b4 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt @@ -41,8 +41,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.core.graphics.Insets @@ -120,7 +122,7 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener { (activity as MainActivity).loadFragment(nav.tag, null) (activity as MainActivity).bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED }) { - Icon(painter = painterResource(nav.iconRes), tint = textColor, contentDescription = nav.tag, modifier = Modifier.padding(start = 10.dp)) + Icon(imageVector = ImageVector.vectorResource(nav.iconRes), tint = textColor, contentDescription = nav.tag, modifier = Modifier.padding(start = 10.dp)) Text(stringResource(nav.nameRes), color = textColor, style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(start = 20.dp)) Spacer(Modifier.weight(1f)) if (nav.count > 0) Text(nav.count.toString(), color = textColor, modifier = Modifier.padding(end = 10.dp)) @@ -146,7 +148,7 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().clickable { startActivity(Intent(activity, PreferenceActivity::class.java)) }) { - Icon(painter = painterResource(R.drawable.ic_settings), tint = textColor, contentDescription = "settings", modifier = Modifier.padding(start = 10.dp)) + Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_settings), tint = textColor, contentDescription = "settings", modifier = Modifier.padding(start = 10.dp)) Text(stringResource(R.string.settings_label), color = textColor, style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(start = 20.dp)) } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt index 8fe29459..c0ba7767 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt @@ -10,8 +10,12 @@ import ac.mdiq.podcini.net.feed.discovery.PodcastSearcherRegistry import ac.mdiq.podcini.net.utils.HtmlToPlainText import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload import ac.mdiq.podcini.storage.database.Feeds.getFeed +import ac.mdiq.podcini.storage.database.Feeds.getFeedByTitleAndAuthor import ac.mdiq.podcini.storage.database.Feeds.getFeedList +import ac.mdiq.podcini.storage.database.Feeds.isSubscribed import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences +import ac.mdiq.podcini.storage.database.RealmDB.realm +import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.model.* import ac.mdiq.podcini.storage.model.Rating.Companion.fromCode import ac.mdiq.podcini.storage.model.SubscriptionLog.Companion.feedLogsMap @@ -44,9 +48,11 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +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.input.TextFieldValue import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -70,7 +76,6 @@ import kotlin.concurrent.Volatile * Downloads a feed from a feed URL and parses it. Subclasses can display the * feed object that was parsed. This activity MUST be started with a given URL * or an Exception will be thrown. - * * If the feed cannot be downloaded or parsed, an error dialog will be displayed * and the activity will finish as soon as the error dialog is closed. */ @@ -85,6 +90,8 @@ class OnlineFeedFragment : Fragment() { private var feedUrl: String = "" private lateinit var feedBuilder: FeedBuilder + private var isShared: Boolean = false + private var showFeedDisplay by mutableStateOf(false) private var showProgress by mutableStateOf(true) private var autoDownloadChecked by mutableStateOf(false) @@ -123,6 +130,7 @@ class OnlineFeedFragment : Fragment() { (activity as MainActivity).setupToolbarToggle(binding.toolbar, displayUpArrow) feedUrl = requireArguments().getString(ARG_FEEDURL) ?: "" + isShared = requireArguments().getBoolean("isShared") Logd(TAG, "feedUrl: $feedUrl") feedBuilder = FeedBuilder(requireContext()) { message, details -> showErrorDialog(message, details) } @@ -183,6 +191,13 @@ class OnlineFeedFragment : Fragment() { feedBuilder.startFeedBuilding(urlString, username, password) { feed_, map -> selectedDownloadUrl = feedBuilder.selectedDownloadUrl feed = feed_ + if (isShared) { + val log = realm.query(ShareLog::class).query("url == $0", url).first().find() + if (log != null) upsertBlk(log) { + it.title = feed_.title + it.author = feed_.author + } + } showFeedInformation(feed_, map) } } catch (e: FeedUrlNotFoundException) { tryToRetrieveFeedUrlBySearch(e) @@ -216,6 +231,13 @@ class OnlineFeedFragment : Fragment() { feedBuilder.startFeedBuilding(url, username, password) { feed_, map -> selectedDownloadUrl = feedBuilder.selectedDownloadUrl feed = feed_ + if (isShared) { + val log = realm.query(ShareLog::class).query("url == $0", url).first().find() + if (log != null) upsertBlk(log) { + it.title = feed_.title + it.author = feed_.author + } + } showFeedInformation(feed_, map) } } else { @@ -340,7 +362,7 @@ class OnlineFeedFragment : Fragment() { }) { if (showFeedDisplay) ConstraintLayout(modifier = Modifier.fillMaxWidth().height(120.dp).background(MaterialTheme.colorScheme.surface)) { val (backgroundImage, coverImage, taColumn, buttons, closeButton) = createRefs() - if (false) Image(painter = painterResource(R.drawable.ic_settings_white), contentDescription = "background", + if (false) Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_settings_white), contentDescription = "background", Modifier.fillMaxWidth().height(120.dp).constrainAs(backgroundImage) { top.linkTo(parent.top) bottom.linkTo(parent.bottom) @@ -364,12 +386,28 @@ class OnlineFeedFragment : Fragment() { }) { Spacer(modifier = Modifier.weight(0.2f)) if (enableSubscribe) Button(onClick = { - if (feedInFeedlist()) (activity as MainActivity).loadFeedFragmentById(feedId, null) + if (feedInFeedlist() || isSubscribed(feed!!)) { + if (isShared) { + val log = realm.query(ShareLog::class).query("url == $0", feedUrl).first().find() + if (log != null) upsertBlk(log) { + it.status = ShareLog.Status.EXISTING.ordinal + } + } + val feed = getFeedByTitleAndAuthor(feed?.eigenTitle?:"", feed?.author?:"") + if (feed != null ) (activity as MainActivity).loadChildFragment(FeedInfoFragment.newInstance(feed)) +// (activity as MainActivity).loadFeedFragmentById(feedId, null) + } else { enableSubscribe = false enableEpisodes = false CoroutineScope(Dispatchers.IO).launch { feedBuilder.subscribe(feed!!) + if (isShared) { + val log = realm.query(ShareLog::class).query("url == $0", feedUrl).first().find() + if (log != null) upsertBlk(log) { + it.status = ShareLog.Status.SUCCESS.ordinal + } + } withContext(Dispatchers.Main) { enableSubscribe = true didPressSubscribe = true @@ -386,7 +424,7 @@ class OnlineFeedFragment : Fragment() { } Spacer(modifier = Modifier.weight(0.2f)) } - if (false) Icon(painter = painterResource(R.drawable.ic_close_white), contentDescription = null, modifier = Modifier + if (false) Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_close_white), contentDescription = null, modifier = Modifier .constrainAs(closeButton) { top.linkTo(parent.top) end.linkTo(parent.end) @@ -418,14 +456,14 @@ class OnlineFeedFragment : Fragment() { Text(HtmlToPlainText.getPlainText(feed?.description ?: ""), color = textColor, style = MaterialTheme.typography.bodyMedium) val sLog = remember {feedLogsMap_[feed?.downloadUrl?:""] } if (sLog != null) { - val commentTextState by remember { mutableStateOf(TextFieldValue(sLog.comment ?: "")) } + val commentTextState by remember { mutableStateOf(TextFieldValue(sLog.comment)) } val context = LocalContext.current val cancelDate = remember { formatAbbrev(context, Date(sLog.cancelDate)) } val ratingRes = remember { fromCode(sLog.rating).res } if (commentTextState.text.isNotEmpty()) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 15.dp, top = 10.dp, bottom = 5.dp)) { Text(stringResource(R.string.my_opinion_label), color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.titleMedium) - Icon(painter = painterResource(ratingRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = null, modifier = Modifier.padding(start = 5.dp)) + Icon(imageVector = ImageVector.vectorResource(ratingRes), tint = MaterialTheme.colorScheme.tertiary, contentDescription = null, modifier = Modifier.padding(start = 5.dp)) } Text(commentTextState.text, color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(start = 15.dp, bottom = 10.dp)) @@ -765,10 +803,11 @@ class OnlineFeedFragment : Fragment() { if (prefs == null) prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) } - fun newInstance(feedUrl: String): OnlineFeedFragment { + fun newInstance(feedUrl: String, isShared: Boolean = false): OnlineFeedFragment { val fragment = OnlineFeedFragment() val b = Bundle() b.putString(ARG_FEEDURL, feedUrl) + b.putBoolean("isShared", isShared) fragment.arguments = b return fragment } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt index 99f77d45..2e874b3b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt @@ -184,7 +184,7 @@ import kotlin.math.max swipeActionsBin = SwipeActions(this, "$TAG.Bin") swipeActionsBin.setFilter(EpisodeFilter(EpisodeFilter.States.queued.name)) - binding.lazyColumn.setContent { + binding.mainView.setContent { CustomTheme(requireContext()) { if (showBin) { Column { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt index 74539529..c0fb25dd 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SubscriptionsFragment.kt @@ -18,6 +18,7 @@ import ac.mdiq.podcini.storage.database.RealmDB.upsert import ac.mdiq.podcini.storage.model.* import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction import ac.mdiq.podcini.storage.model.FeedPreferences.Companion.FeedAutoDeleteOptions +import ac.mdiq.podcini.storage.utils.DurationConverter import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.compose.CustomTheme import ac.mdiq.podcini.ui.compose.RemoveFeedDialog @@ -71,6 +72,7 @@ import androidx.compose.ui.Alignment 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 @@ -84,6 +86,8 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import coil.compose.AsyncImage +import coil.request.CachePolicy +import coil.request.ImageRequest import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.button.MaterialButtonToggleGroup @@ -314,9 +318,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { CustomFeedNameDialog(activity as Activity, feed).show() } R.id.new_synth_yt -> { - val feed = createSynthetic(0, "") + val feed = createSynthetic(0, "", true) feed.type = Feed.FeedType.YOUTUBE.name - feed.hasVideoMedia = true feed.preferences!!.videoModePolicy = VideoMode.WINDOW_VIEW CustomFeedNameDialog(activity as Activity, feed).show() } @@ -498,7 +501,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { fun InforBar() { Row(Modifier.padding(start = 20.dp, end = 20.dp)) { val textColor = MaterialTheme.colorScheme.onSurface - Icon(painter = painterResource(R.drawable.ic_info), contentDescription = "info", tint = textColor) + Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_info), contentDescription = "info", tint = textColor) Spacer(Modifier.weight(1f)) Text(txtvInformation, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.clickable { if (feedsFilter.isNotEmpty()) { @@ -839,6 +842,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { refreshing = false // } }) { + val context = LocalContext.current if (if (useGrid == null) useGridLayout else useGrid!!) { val lazyGridState = rememberLazyGridState() LazyVerticalGrid(state = lazyGridState, columns = GridCells.Adaptive(80.dp), @@ -881,8 +885,10 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { val textColor = MaterialTheme.colorScheme.onSurface ConstraintLayout(Modifier.fillMaxSize()) { val (coverImage, episodeCount, rating, error) = createRefs() - AsyncImage(model = feed.imageUrl, contentDescription = "coverImage", - placeholder = painterResource(R.mipmap.ic_launcher), error = painterResource(R.mipmap.ic_launcher), + 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(), + contentDescription = "coverImage", modifier = Modifier.fillMaxWidth().aspectRatio(1f) .constrainAs(coverImage) { top.linkTo(parent.top) @@ -895,13 +901,13 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { top.linkTo(coverImage.top) }) if (feed.rating != Rating.UNRATED.code) - Icon(painter = painterResource(Rating.fromCode(feed.rating).res), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating", + Icon(imageVector = ImageVector.vectorResource(Rating.fromCode(feed.rating).res), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating", modifier = Modifier.background(MaterialTheme.colorScheme.tertiaryContainer).constrainAs(rating) { start.linkTo(parent.start) centerVerticallyTo(coverImage) }) // TODO: need to use state - if (feed.lastUpdateFailed) Icon(painter = painterResource(R.drawable.ic_error), tint = Color.Red, contentDescription = "error", + if (feed.lastUpdateFailed) Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_error), tint = Color.Red, contentDescription = "error", modifier = Modifier.background(Color.Gray).constrainAs(error) { end.linkTo(parent.end) bottom.linkTo(coverImage.bottom) @@ -929,7 +935,9 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { Row(Modifier.background(if (isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)) { ConstraintLayout { val (coverImage, rating) = createRefs() - AsyncImage(model = feed.imageUrl, + 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(), contentDescription = "imgvCover", placeholder = painterResource(R.mipmap.ic_launcher), error = painterResource(R.mipmap.ic_launcher), @@ -947,7 +955,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { }) ) if (feed.rating != Rating.UNRATED.code) - Icon(painter = painterResource(Rating.fromCode(feed.rating).res), tint = MaterialTheme.colorScheme.tertiary, + Icon(imageVector = ImageVector.vectorResource(Rating.fromCode(feed.rating).res), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating", modifier = Modifier.background(MaterialTheme.colorScheme.tertiaryContainer).constrainAs(rating) { start.linkTo(parent.start) @@ -980,8 +988,9 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold)) Text(feed.author ?: "No author", color = textColor, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodyMedium) Row(Modifier.padding(top = 5.dp)) { - Text(NumberFormat.getInstance().format(feed.episodes.size.toLong()) + " episodes", - color = textColor, style = MaterialTheme.typography.bodyMedium) + val measureString = remember { NumberFormat.getInstance().format(feed.episodes.size.toLong()) + " : " + + DurationConverter.shortLocalizedDuration(requireActivity(), feed.totleDuration/1000) } + Text(measureString, color = textColor, style = MaterialTheme.typography.bodyMedium) Spacer(modifier = Modifier.weight(1f)) var feedSortInfo by remember { mutableStateOf(feed.sortInfo) } LaunchedEffect(feedSorted) { feedSortInfo = feed.sortInfo } @@ -989,7 +998,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { } } // TODO: need to use state - if (feed.lastUpdateFailed) Icon(painter = painterResource(R.drawable.ic_error), tint = Color.Red, contentDescription = "error") + if (feed.lastUpdateFailed) Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_error), tint = Color.Red, contentDescription = "error") } } } @@ -997,7 +1006,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { if (selectMode) { Row(modifier = Modifier.align(Alignment.TopEnd).width(150.dp).height(45.dp).background(Color.LightGray), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) { - Icon(painter = painterResource(R.drawable.baseline_arrow_upward_24), tint = Color.Black, contentDescription = null, + 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() @@ -1007,7 +1016,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { selectedSize = selected.size Logd(TAG, "selectedIds: ${selected.size}") })) - Icon(painter = painterResource(R.drawable.baseline_arrow_downward_24), tint = Color.Black, contentDescription = null, + 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() @@ -1018,7 +1027,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { Logd(TAG, "selectedIds: ${selected.size}") })) var selectAllRes by remember { mutableIntStateOf(R.drawable.ic_select_all) } - Icon(painter = painterResource(selectAllRes), tint = Color.Black, contentDescription = null, + Icon(imageVector = ImageVector.vectorResource(selectAllRes), tint = Color.Black, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp) .clickable(onClick = { if (selectedSize != feedListFiltered.size) { diff --git a/app/src/main/res/layout/queue_fragment.xml b/app/src/main/res/layout/queue_fragment.xml index 46e3f4d0..09f13f67 100644 --- a/app/src/main/res/layout/queue_fragment.xml +++ b/app/src/main/res/layout/queue_fragment.xml @@ -29,7 +29,7 @@ diff --git a/changelog.md b/changelog.md index 352e686a..fed7f72a 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,12 @@ +# 6.11.7 + +* added author and title info in SharedLog +* when shared channel, playlist or podcast from Youtube, double checks if existing and records SharedLog accordingly +* in Shared LogsFragment, tap on a successful or existing item (media or feed) opens the corresponding fragment +* in Subscriptions view, added total duration for every feed +* hasEmbeddedPicture in EpisodeMedia is set to not persist for now +* tuned Compose routines ti reduce recomposition and improve efficiency + # 6.11.6 * fixed a serious performance issue when scrolling list of episode having no defined image url diff --git a/fastlane/metadata/android/en-US/changelogs/3020276.txt b/fastlane/metadata/android/en-US/changelogs/3020276.txt index 0a10fe9f..9e0ee7b2 100644 --- a/fastlane/metadata/android/en-US/changelogs/3020276.txt +++ b/fastlane/metadata/android/en-US/changelogs/3020276.txt @@ -1,3 +1,3 @@ - Version 6.11.5 + Version 6.11.6 * fixed a serious performance issue when scrolling list of episode having no defined image url diff --git a/fastlane/metadata/android/en-US/changelogs/3020277.txt b/fastlane/metadata/android/en-US/changelogs/3020277.txt new file mode 100644 index 00000000..bec8f82e --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020277.txt @@ -0,0 +1,8 @@ + Version 6.11.7 + +* added author and title info in SharedLog +* when shared channel, playlist or podcast from Youtube, double checks if existing and records SharedLog accordingly +* in Shared LogsFragment, tap on a successful or existing item (media or feed) opens the corresponding fragment +* in Subscriptions view, added total duration for every feed +* hasEmbeddedPicture in EpisodeMedia is set to not persist for now +* tuned Compose routines ti reduce recomposition and improve efficiency