mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
player: draw the app's own like, shuffle and repeat buttons, lit when their icon says they're on (Tidal showed every song liked and shuffle off)
This commit is contained in:
1 parent
e587ea97e1
commit
10dfb46a96
2 files changed
+85
-28
No files matched your search
@@ -138,29 +138,47 @@ internal fun PlayModeRow(
|
|||||||
val repeatOn = repeatKnown && modes.repeat != PlaybackStateCompat.REPEAT_MODE_NONE
|
val repeatOn = repeatKnown && modes.repeat != PlaybackStateCompat.REPEAT_MODE_NONE
|
||||||
val icon = size * 0.52f
|
val icon = size * 0.52f
|
||||||
Row(modifier, horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically) {
|
Row(modifier, horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically) {
|
||||||
GsIconButton(
|
// An app's own like, shuffle or repeat button shows whether it's on only through its icon,
|
||||||
if (liked == true) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
|
// so that icon is drawn, as Android Auto does, and lit when its name says it's on.
|
||||||
if (liked == true) "Unlike" else "Like",
|
// Gearslip's own stand in for apps that report a heart rating or shuffle and repeat modes.
|
||||||
media::toggleLike,
|
val likeAction = now.likeAction
|
||||||
enabled = liked != null,
|
if (likeAction != null) {
|
||||||
tone = if (liked == true) GsTone.Primary else GsTone.Neutral,
|
CustomActionButton(likeAction, media, size, toggle = true)
|
||||||
size = size, iconSize = icon,
|
} else {
|
||||||
)
|
GsIconButton(
|
||||||
GsIconButton(
|
if (liked == true) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
|
||||||
MediaIcons.Shuffle, if (shuffleOn) "Shuffle off" else "Shuffle on",
|
if (liked == true) "Unlike" else "Like",
|
||||||
media::toggleShuffle,
|
media::toggleLike,
|
||||||
enabled = shuffleKnown || now.shuffleAction != null,
|
enabled = liked != null,
|
||||||
tone = if (shuffleOn) GsTone.Primary else GsTone.Neutral,
|
tone = if (liked == true) GsTone.Primary else GsTone.Neutral,
|
||||||
size = size, iconSize = icon,
|
size = size, iconSize = icon,
|
||||||
)
|
)
|
||||||
GsIconButton(
|
}
|
||||||
if (modes.repeat == PlaybackStateCompat.REPEAT_MODE_ONE) MediaIcons.RepeatOne else MediaIcons.Repeat,
|
val shuffleAction = now.shuffleAction
|
||||||
"Repeat",
|
if (shuffleAction != null) {
|
||||||
media::cycleRepeat,
|
CustomActionButton(shuffleAction, media, size, toggle = true)
|
||||||
enabled = repeatKnown || now.repeatAction != null,
|
} else {
|
||||||
tone = if (repeatOn) GsTone.Primary else GsTone.Neutral,
|
GsIconButton(
|
||||||
size = size, iconSize = icon,
|
MediaIcons.Shuffle, if (shuffleOn) "Shuffle off" else "Shuffle on",
|
||||||
)
|
media::toggleShuffle,
|
||||||
|
enabled = shuffleKnown,
|
||||||
|
tone = if (shuffleOn) GsTone.Primary else GsTone.Neutral,
|
||||||
|
size = size, iconSize = icon,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val repeatAction = now.repeatAction
|
||||||
|
if (repeatAction != null) {
|
||||||
|
CustomActionButton(repeatAction, media, size, toggle = true)
|
||||||
|
} else {
|
||||||
|
GsIconButton(
|
||||||
|
if (modes.repeat == PlaybackStateCompat.REPEAT_MODE_ONE) MediaIcons.RepeatOne else MediaIcons.Repeat,
|
||||||
|
"Repeat",
|
||||||
|
media::cycleRepeat,
|
||||||
|
enabled = repeatKnown,
|
||||||
|
tone = if (repeatOn) GsTone.Primary else GsTone.Neutral,
|
||||||
|
size = size, iconSize = icon,
|
||||||
|
)
|
||||||
|
}
|
||||||
now.otherActions.take(maxOther).forEach { CustomActionButton(it, media, size) }
|
now.otherActions.take(maxOther).forEach { CustomActionButton(it, media, size) }
|
||||||
trailing()
|
trailing()
|
||||||
}
|
}
|
||||||
@@ -171,12 +189,12 @@ internal fun PlayModeRow(
|
|||||||
* not Gearslip's, so it's loaded from there. Its name stands in if the icon won't load.
|
* not Gearslip's, so it's loaded from there. Its name stands in if the icon won't load.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun CustomActionButton(action: CustomAction, media: CarMedia, size: Dp) {
|
private fun CustomActionButton(action: CustomAction, media: CarMedia, size: Dp, toggle: Boolean = false) {
|
||||||
val icon = rememberActionIcon(action)
|
val icon = rememberActionIcon(action)
|
||||||
GsIconBox(
|
GsIconBox(
|
||||||
onClick = { media.custom(action) },
|
onClick = { media.custom(action) },
|
||||||
modifier = Modifier.size(size),
|
modifier = Modifier.size(size),
|
||||||
colors = gsColors(GsTone.Neutral),
|
colors = gsColors(if (toggle && action.on) GsTone.Primary else GsTone.Neutral),
|
||||||
) {
|
) {
|
||||||
if (icon != null) {
|
if (icon != null) {
|
||||||
Icon(icon, action.name, Modifier.size(size * 0.52f))
|
Icon(icon, action.name, Modifier.size(size * 0.52f))
|
||||||
|
|||||||
@@ -40,7 +40,19 @@ class MediaEntry(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/** [icon] is a drawable id inside the media app's own package, not Gearslip's; 0 for none. */
|
/** [icon] is a drawable id inside the media app's own package, not Gearslip's; 0 for none. */
|
||||||
class CustomAction(val id: String, val name: String, val extras: Bundle?, val icon: Int = 0)
|
class CustomAction(val id: String, val name: String, val extras: Bundle?, val icon: Int = 0, val iconName: String = "") {
|
||||||
|
/**
|
||||||
|
* Whether a toggle like heart or shuffle is on, read from the icon's name - Tidal swaps
|
||||||
|
* ic_favorite for ic_favorite_filled and ic_shuffle for ic_shuffle_active, and keeps the
|
||||||
|
* button's name the same. Whole words only, so "inactive" doesn't count as "active".
|
||||||
|
*/
|
||||||
|
val on: Boolean
|
||||||
|
get() = iconName.lowercase().split('_', '.', '-').any { it in ON_WORDS }
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
val ON_WORDS = setOf("filled", "fill", "active", "on", "selected", "checked", "enabled", "solid", "one", "all")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** One entry in the app's own queue - not its library, whatever it has lined up to play next. */
|
/** One entry in the app's own queue - not its library, whatever it has lined up to play next. */
|
||||||
class QueueTrack(
|
class QueueTrack(
|
||||||
@@ -228,7 +240,11 @@ class CarMedia(private val context: Context) {
|
|||||||
|
|
||||||
private fun refreshModes() {
|
private fun refreshModes() {
|
||||||
val c = compat
|
val c = compat
|
||||||
_modes.value = if (c == null) Modes() else Modes(shuffle = c.shuffleMode, repeat = c.repeatMode)
|
val modes = if (c == null) Modes() else Modes(shuffle = c.shuffleMode, repeat = c.repeatMode)
|
||||||
|
if (modes.shuffle != _modes.value.shuffle || modes.repeat != _modes.value.repeat) {
|
||||||
|
GearslipLog.i("media: shuffle mode ${modes.shuffle}, repeat mode ${modes.repeat}, rating type ${controller?.ratingType}")
|
||||||
|
}
|
||||||
|
_modes.value = modes
|
||||||
}
|
}
|
||||||
private var subscribed: String? = null
|
private var subscribed: String? = null
|
||||||
private var rootId: String? = null
|
private var rootId: String? = null
|
||||||
@@ -366,16 +382,39 @@ class CarMedia(private val context: Context) {
|
|||||||
speed = state?.playbackSpeed ?: 1f,
|
speed = state?.playbackSpeed ?: 1f,
|
||||||
updatedAt = state?.lastPositionUpdateTime ?: 0,
|
updatedAt = state?.lastPositionUpdateTime ?: 0,
|
||||||
actions = state?.actions ?: 0,
|
actions = state?.actions ?: 0,
|
||||||
custom = state?.customActions.orEmpty().map { CustomAction(it.action, it.name.toString(), it.extras, it.icon) },
|
custom = state?.customActions.orEmpty().map { CustomAction(it.action, it.name.toString(), it.extras, it.icon, iconName(it.icon)) },
|
||||||
error = state?.errorMessage?.toString(),
|
error = state?.errorMessage?.toString(),
|
||||||
activeQueueItemId = state?.activeQueueItemId ?: MediaSession.QueueItem.UNKNOWN_ID.toLong(),
|
activeQueueItemId = state?.activeQueueItemId ?: MediaSession.QueueItem.UNKNOWN_ID.toLong(),
|
||||||
heart = if (controller?.ratingType == Rating.RATING_HEART) {
|
heart = if (controller?.ratingType == Rating.RATING_HEART) {
|
||||||
metadata?.getRating(MediaMetadata.METADATA_KEY_USER_RATING)?.takeIf { it.isRated }?.hasHeart() ?: false
|
metadata?.getRating(MediaMetadata.METADATA_KEY_USER_RATING)?.takeIf { it.isRated }?.hasHeart() ?: false
|
||||||
} else null,
|
} else null,
|
||||||
)
|
)
|
||||||
|
logButtons()
|
||||||
checkSearch()
|
checkSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var appResources: Pair<String, android.content.res.Resources?>? = null
|
||||||
|
|
||||||
|
/** The icon's resource name in the app's package, or "" when it can't be read. */
|
||||||
|
private fun iconName(icon: Int): String {
|
||||||
|
val pkg = controller?.packageName ?: return ""
|
||||||
|
if (appResources?.first != pkg) {
|
||||||
|
appResources = pkg to runCatching { context.packageManager.getResourcesForApplication(pkg) }.getOrNull()
|
||||||
|
}
|
||||||
|
val res = appResources?.second ?: return ""
|
||||||
|
return runCatching { res.getResourceEntryName(icon) }.getOrDefault("")
|
||||||
|
}
|
||||||
|
|
||||||
|
private var loggedButtons: String? = null
|
||||||
|
|
||||||
|
/** The app's own buttons, by id, name and icon, each time they change. */
|
||||||
|
private fun logButtons() {
|
||||||
|
val text = _now.value.custom.joinToString { a -> "${a.id} \"${a.name}\" icon=${a.iconName.ifEmpty { a.icon.toString() }}" }
|
||||||
|
if (text == loggedButtons) return
|
||||||
|
loggedButtons = text
|
||||||
|
GearslipLog.i("media: app buttons: ${text.ifEmpty { "none" }}")
|
||||||
|
}
|
||||||
|
|
||||||
// --- browsing ------------------------------------------------------------------------
|
// --- browsing ------------------------------------------------------------------------
|
||||||
|
|
||||||
private fun open(parentId: String) {
|
private fun open(parentId: String) {
|
||||||
|
|||||||
Reference in new issue
Block a user