mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
An app's own like button lights the whole button when liked, like shuffle and repeat
Metrolist swaps ic_heart_outline for ic_heart and keeps the name, so only the heart filled in. A plain icon that has an outline twin in the app now counts as on.
This commit is contained in:
1 parent
42b151034a
commit
03f27c4dda
2 files changed
+42
-12
No files matched your search
@@ -145,7 +145,7 @@ internal fun PlayModeRow(
|
|||||||
// Gearslip's own stand in for apps that report a heart rating or shuffle and repeat modes.
|
// Gearslip's own stand in for apps that report a heart rating or shuffle and repeat modes.
|
||||||
val likeAction = now.likeAction
|
val likeAction = now.likeAction
|
||||||
if (likeAction != null) {
|
if (likeAction != null) {
|
||||||
CustomActionButton(likeAction, media, size, toggle = true)
|
CustomActionButton(likeAction, media, size, on = liked == true)
|
||||||
} else {
|
} else {
|
||||||
GsIconButton(
|
GsIconButton(
|
||||||
if (liked == true) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
|
if (liked == true) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
|
||||||
@@ -158,7 +158,7 @@ internal fun PlayModeRow(
|
|||||||
}
|
}
|
||||||
val shuffleAction = now.shuffleAction
|
val shuffleAction = now.shuffleAction
|
||||||
if (shuffleAction != null) {
|
if (shuffleAction != null) {
|
||||||
CustomActionButton(shuffleAction, media, size, toggle = true)
|
CustomActionButton(shuffleAction, media, size, on = shuffleAction.on)
|
||||||
} else {
|
} else {
|
||||||
GsIconButton(
|
GsIconButton(
|
||||||
MediaIcons.Shuffle, stringResource(if (shuffleOn) R.string.player_shuffle_off else R.string.player_shuffle_on),
|
MediaIcons.Shuffle, stringResource(if (shuffleOn) R.string.player_shuffle_off else R.string.player_shuffle_on),
|
||||||
@@ -170,7 +170,7 @@ internal fun PlayModeRow(
|
|||||||
}
|
}
|
||||||
val repeatAction = now.repeatAction
|
val repeatAction = now.repeatAction
|
||||||
if (repeatAction != null) {
|
if (repeatAction != null) {
|
||||||
CustomActionButton(repeatAction, media, size, toggle = true)
|
CustomActionButton(repeatAction, media, size, on = repeatAction.on)
|
||||||
} else {
|
} else {
|
||||||
GsIconButton(
|
GsIconButton(
|
||||||
if (modes.repeat == PlaybackStateCompat.REPEAT_MODE_ONE) MediaIcons.RepeatOne else MediaIcons.Repeat,
|
if (modes.repeat == PlaybackStateCompat.REPEAT_MODE_ONE) MediaIcons.RepeatOne else MediaIcons.Repeat,
|
||||||
@@ -191,12 +191,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, toggle: Boolean = false) {
|
private fun CustomActionButton(action: CustomAction, media: CarMedia, size: Dp, on: 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(if (toggle && action.on) GsTone.Primary else GsTone.Neutral),
|
colors = gsColors(if (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))
|
||||||
|
|||||||
@@ -44,15 +44,27 @@ class MediaEntry(
|
|||||||
val group: String? = null,
|
val group: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
/** [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, val iconName: String = "") {
|
* [icon] is a drawable id inside the media app's own package, not Gearslip's; 0 for none.
|
||||||
|
* [hasOutline] says the app also has an outline of the same icon (ic_heart beside ic_heart_outline).
|
||||||
|
*/
|
||||||
|
class CustomAction(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val extras: Bundle?,
|
||||||
|
val icon: Int = 0,
|
||||||
|
val iconName: String = "",
|
||||||
|
val hasOutline: Boolean = false,
|
||||||
|
) {
|
||||||
/**
|
/**
|
||||||
* Whether a toggle like heart or shuffle is on, read from the icon's name - Tidal swaps
|
* 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
|
* 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".
|
* button's name the same. Whole words only, so "inactive" doesn't count as "active".
|
||||||
|
* Metrolist swaps ic_heart_outline for a plain ic_heart, so a plain icon that has an outline
|
||||||
|
* twin counts as on too.
|
||||||
*/
|
*/
|
||||||
val on: Boolean
|
val on: Boolean
|
||||||
get() = iconName.lowercase().split('_', '.', '-').any { it in ON_WORDS }
|
get() = hasOutline || iconName.lowercase().split('_', '.', '-').any { it in ON_WORDS }
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
val ON_WORDS = setOf("filled", "fill", "active", "on", "selected", "checked", "enabled", "solid", "one", "all")
|
val ON_WORDS = setOf("filled", "fill", "active", "on", "selected", "checked", "enabled", "solid", "one", "all")
|
||||||
@@ -118,8 +130,8 @@ class NowPlaying(
|
|||||||
* only says which way it goes next by its name ("Remove from Liked Songs"), so that's read.
|
* only says which way it goes next by its name ("Remove from Liked Songs"), so that's read.
|
||||||
*/
|
*/
|
||||||
val liked: Boolean?
|
val liked: Boolean?
|
||||||
get() = heart ?: likeAction?.name?.let { name ->
|
get() = heart ?: likeAction?.let { action ->
|
||||||
listOf("remove", "unlike", "unfav", "unheart").any { name.contains(it, ignoreCase = true) }
|
action.on || listOf("remove", "unlike", "unfav", "unheart").any { action.name.contains(it, ignoreCase = true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
val playing get() = state == PlaybackState.STATE_PLAYING || state == PlaybackState.STATE_BUFFERING
|
val playing get() = state == PlaybackState.STATE_PLAYING || state == PlaybackState.STATE_BUFFERING
|
||||||
@@ -392,7 +404,10 @@ 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 = withNextChapter(state?.customActions.orEmpty().map { CustomAction(it.action, it.name.toString(), it.extras, it.icon, iconName(it.icon)) }),
|
custom = withNextChapter(state?.customActions.orEmpty().map {
|
||||||
|
val iconName = iconName(it.icon)
|
||||||
|
CustomAction(it.action, it.name.toString(), it.extras, it.icon, iconName, hasOutline(it.icon, iconName))
|
||||||
|
}),
|
||||||
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) {
|
||||||
@@ -405,7 +420,6 @@ class CarMedia(private val context: Context) {
|
|||||||
|
|
||||||
private var appResources: Pair<String, android.content.res.Resources?>? = null
|
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. */
|
|
||||||
/**
|
/**
|
||||||
* AntennaPod takes a Next chapter command whenever it's playing, but offers the button only
|
* AntennaPod takes a Next chapter command whenever it's playing, but offers the button only
|
||||||
* when the driver turns it on in its notification settings. Gearslip offers it either way,
|
* when the driver turns it on in its notification settings. Gearslip offers it either way,
|
||||||
@@ -424,16 +438,30 @@ class CarMedia(private val context: Context) {
|
|||||||
private fun resourcesOf(pkg: String): android.content.res.Resources? {
|
private fun resourcesOf(pkg: String): android.content.res.Resources? {
|
||||||
if (appResources?.first != pkg) {
|
if (appResources?.first != pkg) {
|
||||||
appResources = pkg to runCatching { context.packageManager.getResourcesForApplication(pkg) }.getOrNull()
|
appResources = pkg to runCatching { context.packageManager.getResourcesForApplication(pkg) }.getOrNull()
|
||||||
|
outlines.clear()
|
||||||
}
|
}
|
||||||
return appResources?.second
|
return appResources?.second
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The icon's resource name in the app's package, or "" when it can't be read. */
|
||||||
private fun iconName(icon: Int): String {
|
private fun iconName(icon: Int): String {
|
||||||
val pkg = controller?.packageName ?: return ""
|
val pkg = controller?.packageName ?: return ""
|
||||||
val res = resourcesOf(pkg) ?: return ""
|
val res = resourcesOf(pkg) ?: return ""
|
||||||
return runCatching { res.getResourceEntryName(icon) }.getOrDefault("")
|
return runCatching { res.getResourceEntryName(icon) }.getOrDefault("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether each icon has an outline twin, by icon id: looked up once, since state changes often. */
|
||||||
|
private val outlines = HashMap<Int, Boolean>()
|
||||||
|
|
||||||
|
private fun hasOutline(icon: Int, name: String): Boolean {
|
||||||
|
val pkg = controller?.packageName ?: return false
|
||||||
|
val res = resourcesOf(pkg) ?: return false
|
||||||
|
if (name.isEmpty() || OUTLINE_WORDS.any { name.contains(it, ignoreCase = true) }) return false
|
||||||
|
return outlines.getOrPut(icon) {
|
||||||
|
OUTLINE_WORDS.any { res.getIdentifier("${name}_$it", "drawable", pkg) != 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var loggedButtons: String? = null
|
private var loggedButtons: String? = null
|
||||||
|
|
||||||
/** The app's own buttons, by id, name and icon, each time they change. */
|
/** The app's own buttons, by id, name and icon, each time they change. */
|
||||||
@@ -822,6 +850,8 @@ class CarMedia(private val context: Context) {
|
|||||||
/** How long an app gets to start playing a search before Gearslip says it didn't. */
|
/** How long an app gets to start playing a search before Gearslip says it didn't. */
|
||||||
const val SEARCH_WAIT_MS = 6_000L
|
const val SEARCH_WAIT_MS = 6_000L
|
||||||
const val SEARCH_NOTICE_MS = 6_000L
|
const val SEARCH_NOTICE_MS = 6_000L
|
||||||
|
/** How apps name the empty version of an icon. */
|
||||||
|
val OUTLINE_WORDS = listOf("outline", "outlined", "border", "empty")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user