6.11.7 commit

This commit is contained in:
Xilin Jia 2024-10-20 17:30:30 +01:00
parent 29f7c6ce87
commit a9f59905bd
28 changed files with 631 additions and 441 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String>, leftAction: MutableState<SwipeAction>,
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<EpisodeActionButton?>(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<Episode>, 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<Episode>, 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<Feed?>(null) }
for (f in synthetics) {
@ -317,7 +344,7 @@ fun ShelveDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>, feed: Feed? = null,
fun EpisodeLazyColumn(activity: MainActivity, vms: List<EpisodeVM>, 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<EpisodeVM>,
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<EpisodeVM>,
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<EpisodeVM>,
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<EpisodeVM>,
}, 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<EpisodeVM>,
}, 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<EpisodeVM>,
}, 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<EpisodeVM>,
}, 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<EpisodeVM>,
}, 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<EpisodeVM>,
}, 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<EpisodeVM>,
)
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<EpisodeVM>,
}
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<EpisodeVM>,
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<EpisodeVM>,
}
}
@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<EpisodeVM>,
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<EpisodeVM>,
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..<vms.size) {
@ -741,19 +785,19 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
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<String>, 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<String>, 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))
}
}
}

View File

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

View File

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

View File

@ -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<String?>(null)
private var imgLocLarge by mutableStateOf<String?>(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")
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.compose.ui.platform.ComposeView
android:id="@+id/lazyColumn"
android:id="@+id/mainView"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

View File

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

View File

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

View File

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