0.2.76 - Icon Packs

This commit is contained in:
ponzischeme89
2026-08-18 14:59:29 +12:00
parent 36d171e51b
commit 8c847c59b8
70 changed files with 5267 additions and 673 deletions
+44 -10
View File
@@ -12,6 +12,19 @@ plugins {
// the Jellyfin FFmpeg extension pins which line that can be.
val media3Version = "1.9.4"
// Lifecycle and Activity move as one AndroidX stack: activity-compose depends on the
// lifecycle artifacts, and lifecycle-runtime-compose depends on activity's ComponentActivity
// contract, so a split upgrade resolves to a mixture Gradle picked rather than one the
// libraries were tested as. Keep these two in step.
val lifecycleVersion = "2.11.0"
val activityVersion = "1.13.0"
// The icon packs a server-driven theme may name. One version across every pack: the
// receiver objects (Lucide, FontAwesome.Solid) come from shared base artifacts, so a split
// version resolves two copies of the same object and the extension properties stop
// matching their receiver.
val composeIconsVersion = "2.2.1"
// Set in gradle.properties (or ~/.gradle/gradle.properties, or -Pmemby.serverUrl=...).
// Blank means "no hardwired server": the setup screen asks the user for an address.
val embyServerUrl: String = (project.findProperty("memby.serverUrl") as String?).orEmpty().trim()
@@ -50,7 +63,7 @@ val projectNoticeText =
// A release workflow can derive the app version from its Git tag without editing the
// source tree. Local builds keep using the checked-in default.
val defaultVersionName = "0.2.75"
val defaultVersionName = "0.2.76"
val membyVersionName: String =
(project.findProperty("memby.versionName") as String?)
?.trim()
@@ -84,7 +97,11 @@ fun environmentSecret(key: String): String? {
android {
namespace = "com.ponzischeme89.memby"
compileSdk = 35
// Raised to 37 by the Lifecycle 2.11 / Activity 1.13 upgrade, which refuse to be
// consumed by a project compiled against anything older. This is a *compile* target
// only — targetSdk stays 35, so no new runtime behaviour is opted into and the
// televisions this ships to behave exactly as before.
compileSdk = 37
defaultConfig {
// Matches the Kotlin package. Changed from com.mattcohen.embyclientsname at
@@ -209,20 +226,27 @@ kotlin {
}
dependencies {
val composeBom = platform("androidx.compose:compose-bom:2024.12.01")
// Moved with the Lifecycle 2.11 / Activity 1.13 upgrade, which depend on Compose 1.11
// and so resolve past whatever this BOM says. Left at 2024.12.01 the BOM stopped being
// a floor OR a ceiling: Gradle picked ui 1.11.0 beside foundation 1.10.3, which is a
// Compose pair nobody published or tested together. The BOM's whole job is that the
// artifacts move as one set, so it has to keep up with what depends on them.
val composeBom = platform("androidx.compose:compose-bom:2026.06.01")
implementation(composeBom)
// Core / lifecycle / activity
implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.activity:activity-compose:1.9.3")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
implementation("androidx.work:work-runtime-ktx:2.10.0")
implementation("androidx.activity:activity-compose:$activityVersion")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-runtime-compose:$lifecycleVersion")
implementation("androidx.work:work-runtime-ktx:2.11.2")
// ProcessLifecycleOwner: lets the status poll stop while no Memby screen is on top,
// instead of hitting the gateway every ten seconds for as long as the process lives.
implementation("androidx.lifecycle:lifecycle-process:2.8.7")
implementation("androidx.savedstate:savedstate-ktx:1.2.1")
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
// Pulled to 1.4.0 by lifecycle 2.11 regardless; declared at the resolved version so the
// build file states what actually ships.
implementation("androidx.savedstate:savedstate-ktx:1.4.0")
// Measurement only: JankStats is enabled by PerformanceMonitor for debug builds.
implementation("androidx.metrics:metrics-performance:1.0.0")
// Installs the baseline profile below. Without it the profile is only honoured on
@@ -238,6 +262,16 @@ dependencies {
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.material:material-icons-extended")
// Server-driven icon packs. A theme names a pack (ui/theme/MembyIconPacks.kt) and the
// television paints its marks from it, so a household can be moved off Material's
// marks without an APK release — the same trade the palette makes.
//
// Pure Kotlin ImageVectors, one lazy val per icon, so R8 keeps only the ~70 slots the
// packs below actually name. No .so, no per-ABI multiplier: this is a size decision,
// but a small one, unlike the libmpv episode recorded further down this file.
implementation("com.composables:icons-lucide-cmp:$composeIconsVersion")
implementation("com.composables:icons-font-awesome-solid-cmp:$composeIconsVersion")
// Compose for TV
implementation("androidx.tv:tv-material:1.1.0")
@@ -388,6 +388,12 @@ data class Settings(
* and then flick into the viewer's.
*/
val themePaletteJson: String? = null,
/**
* The icon pack the gateway resolved, cached beside the palette because it arrives with
* it and is worth exactly as much on the next cold start: a set that painted its own
* colours and then swapped its marks a second later would be advertising the request.
*/
val themeIconSet: String = "",
val themeRevision: String = "",
/**
* User ids known to have finished recommendation onboarding on this TV. Cold start
@@ -570,6 +576,7 @@ class SettingsStore(private val context: Context) {
val PROFILE_INITIALS = stringPreferencesKey("profile_initials")
val THEME_ID = stringPreferencesKey("theme_id")
val THEME_PALETTE = stringPreferencesKey("theme_palette")
val THEME_ICON_SET = stringPreferencesKey("theme_icon_set")
val THEME_REVISION = stringPreferencesKey("theme_revision")
val PROFILES = stringPreferencesKey("profiles")
val SEEN_ALERTS = stringPreferencesKey("seen_alert_ids")
@@ -764,14 +771,16 @@ class SettingsStore(private val context: Context) {
* store rewritten every ten seconds to say nothing new is exactly the cost the revision
* exists to avoid.
*/
suspend fun setThemePalette(paletteJson: String, revision: String) {
suspend fun setThemePalette(paletteJson: String, iconSet: String, revision: String) {
if (latestSettings?.themePaletteJson == paletteJson &&
latestSettings?.themeIconSet == iconSet &&
latestSettings?.themeRevision == revision
) {
return
}
context.dataStore.edit { preferences ->
preferences[Keys.THEME_PALETTE] = paletteJson
preferences[Keys.THEME_ICON_SET] = iconSet
preferences[Keys.THEME_REVISION] = revision
}
}
@@ -1397,6 +1406,7 @@ class SettingsStore(private val context: Context) {
// Dropping it puts this set on the default until ThemeSync answers, which is a
// second of the app's own colours rather than a minute of somebody else's.
preferences.remove(Keys.THEME_PALETTE)
preferences.remove(Keys.THEME_ICON_SET)
preferences.remove(Keys.THEME_REVISION)
preferences[Keys.HOME_SECTIONS] = profile.homeSections
preferences[Keys.HOME_CARD_DENSITY] = profile.homeCardDensity
@@ -1520,6 +1530,7 @@ class SettingsStore(private val context: Context) {
?: Settings.DEFAULT_WELCOME_QUOTE_STYLE,
themeId = preferences[Keys.THEME_ID] ?: Settings.DEFAULT_THEME_ID,
themePaletteJson = preferences[Keys.THEME_PALETTE],
themeIconSet = preferences[Keys.THEME_ICON_SET].orEmpty(),
themeRevision = preferences[Keys.THEME_REVISION].orEmpty(),
onboardedUserIds = preferences[Keys.ONBOARDED_USERS].orEmpty(),
whatsNewSeenVersion = preferences[Keys.WHATS_NEW_VERSION],
@@ -5,8 +5,11 @@ import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle
import com.ponzischeme89.memby.data.model.GatewayTheme
import com.ponzischeme89.memby.data.model.GatewayThemeStatus
import com.ponzischeme89.memby.ui.theme.MaterialIconPack
import com.ponzischeme89.memby.ui.theme.MembyPalette
import com.ponzischeme89.memby.ui.theme.applyMembyIconPack
import com.ponzischeme89.memby.ui.theme.applyMembyPalette
import com.ponzischeme89.memby.ui.theme.membyIconPackFor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -95,8 +98,11 @@ class ThemeSync(
// happens to reach the foreground.
scope.launch {
repository.settingsFlow
.distinctUntilChanged { old, new -> old.themePaletteJson == new.themePaletteJson }
.collect { session -> applyCached(session.themePaletteJson) }
.distinctUntilChanged { old, new ->
old.themePaletteJson == new.themePaletteJson &&
old.themeIconSet == new.themeIconSet
}
.collect { session -> applyCached(session.themePaletteJson, session.themeIconSet) }
}
ProcessLifecycleOwner.get().lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
@@ -131,6 +137,7 @@ class ThemeSync(
_available.value = emptyList()
appliedRevision = null
applyMembyPalette(MembyPalette())
applyMembyIconPack(MaterialIconPack.pack)
return
}
// A server that predates themes sends nothing, and there is nothing to fetch. The
@@ -159,6 +166,9 @@ class ThemeSync(
val palette = resolved.palette.toMembyPalette()
applyMembyPalette(palette)
// Unknown slugs resolve to the marks the app shipped with, so a pack the operator
// added after this build went out costs nothing — see membyIconPackFor.
applyMembyIconPack(membyIconPackFor(resolved.iconSet))
// The revision recorded is the one the *response* carried, not the one the poll
// advertised. They differ if a season turned over between the two, and storing the
// poll's would leave this set believing it holds a palette it never received.
@@ -166,6 +176,7 @@ class ThemeSync(
runCatching {
settings.setThemePalette(
json.encodeToString(com.ponzischeme89.memby.data.model.GatewayPalette.serializer(), resolved.palette),
resolved.iconSet,
appliedRevision.orEmpty(),
)
}.onFailure {
@@ -180,7 +191,11 @@ class ThemeSync(
* Paints from what was stored at the end of the last session, before anything is asked
* of the network. A blank or unreadable cache leaves the default palette standing.
*/
private fun applyCached(paletteJson: String?) {
private fun applyCached(paletteJson: String?, iconSet: String) {
// The pack is applied on its own evidence. It is cached as a slug rather than
// inside the palette document, so a set whose stored palette will not parse still
// opens wearing the marks it was told to wear.
applyMembyIconPack(membyIconPackFor(iconSet))
if (paletteJson.isNullOrBlank()) return
val palette = runCatching {
json.decodeFromString(
@@ -319,6 +319,18 @@ data class GatewayTheme(
* field rather than deriving the animation from the theme id.
*/
val decoration: String = "",
/**
* The pack this theme's marks are drawn from: "material", "lucide", "fontawesome", or
* empty for the marks the app shipped with.
*
* A slug, for the reason [decoration] is one — the shapes are the television's, in
* `ui/theme/MembyIconPacks.kt`, and a pack this build has never heard of falls back to
* Material rather than drawing nothing. That is what lets an operator add a pack to the
* catalogue before the fleet has the release that knows it, and it is the same bargain
* the palette makes: the gateway sends a decision, never geometry, so the worst a bad
* theme can do is look unremarkable.
*/
val iconSet: String = "",
val revision: String = "",
val palette: GatewayPalette = GatewayPalette(),
)
@@ -1,5 +1,7 @@
package com.ponzischeme89.memby.ui
import com.ponzischeme89.memby.ui.theme.MembyIcon
import com.ponzischeme89.memby.ui.theme.mark
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
@@ -39,9 +41,6 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
@@ -1254,7 +1253,7 @@ internal fun DetailStripFrame(
// under this strip and this chevron are what say it. Never focusable — it is a
// caption on the Down key, not another thing to land on.
Icon(
Icons.Default.KeyboardArrowDown,
MembyIcon.ChevronDown.mark,
contentDescription = null,
tint = DetailQuietText,
modifier = Modifier.padding(start = 12.dp, bottom = 14.dp).size(18.dp),
@@ -1690,7 +1689,7 @@ private fun DetailExtraCard(item: BaseItem, onClick: () -> Unit, modifier: Modif
AsyncImage(artwork, null, Modifier.fillMaxSize(), contentScale = ContentScale.Crop)
}
Icon(
Icons.Default.PlayArrow,
MembyIcon.Play.mark,
contentDescription = null,
tint = Color.White,
modifier = Modifier
@@ -1,5 +1,7 @@
package com.ponzischeme89.memby.ui
import com.ponzischeme89.memby.ui.theme.MembyIcon
import com.ponzischeme89.memby.ui.theme.mark
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusGroup
@@ -22,12 +24,6 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.DoneAll
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.Tv
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -280,7 +276,7 @@ internal fun EpisodeDetailContent(
DetailHeroAction(
// A heart, not a tick: "Mark watched" beside it is a tick as well. The
// heart is what a home card and the screensaver already use.
icon = if (item.isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
icon = if (item.isFavorite) MembyIcon.Favourite.mark else MembyIcon.FavouriteOutline.mark,
description = if (item.isFavorite) "Remove from Favourites" else "Add to Favourites",
active = item.isFavorite,
onClick = {
@@ -292,7 +288,7 @@ internal fun EpisodeDetailContent(
)
add(
DetailHeroAction(
icon = Icons.Default.DoneAll,
icon = MembyIcon.CheckAll.mark,
description = if (item.userData?.played == true) "Mark unwatched" else "Mark watched",
active = item.userData?.played == true,
onClick = { onTogglePlayed(item, item.userData?.played != true) },
@@ -303,7 +299,7 @@ internal fun EpisodeDetailContent(
item.seriesId?.takeIf(String::isNotBlank)?.let { seriesId ->
add(
DetailHeroAction(
icon = Icons.Default.Tv,
icon = MembyIcon.Tv.mark,
description = "Open ${item.seriesName ?: "the series"}",
onClick = {
onOpenItem(
@@ -453,7 +449,7 @@ private fun SeasonStop(
) {
if (done) {
Icon(
Icons.Default.Check,
MembyIcon.Check.mark,
contentDescription = "Watched",
tint = if (selected || focused) DetailAccent else DetailAccent.copy(alpha = 0.6f),
modifier = Modifier.size(14.dp).padding(end = 1.dp),
@@ -1,5 +1,7 @@
package com.ponzischeme89.memby.ui
import com.ponzischeme89.memby.ui.theme.MembyIcon
import com.ponzischeme89.memby.ui.theme.mark
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
@@ -19,8 +21,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PowerSettingsNew
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -162,7 +162,7 @@ internal fun ExitMembyConfirmation(
contentAlignment = Alignment.Center,
) {
Icon(
Icons.Default.PowerSettingsNew,
MembyIcon.Power.mark,
contentDescription = null,
tint = MembyAccentBright,
modifier = Modifier.size(27.dp),
@@ -1,5 +1,7 @@
package com.ponzischeme89.memby.ui
import com.ponzischeme89.memby.ui.theme.MembyIcon
import com.ponzischeme89.memby.ui.theme.mark
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
@@ -82,37 +84,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AutoAwesome
import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material.icons.filled.BrokenImage
import androidx.compose.material.icons.filled.CalendarMonth
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.ChevronLeft
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.GridView
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.LiveTv
import androidx.compose.material.icons.filled.Movie
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.PlaylistAdd
import androidx.compose.material.icons.filled.PlaylistRemove
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.PushPin
import androidx.compose.material.icons.filled.PlayCircleFilled
import androidx.compose.material.icons.filled.Recommend
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.SentimentVerySatisfied
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.SkipNext
import androidx.compose.material.icons.filled.TheaterComedy
import androidx.compose.material.icons.filled.Tv
import androidx.compose.material.icons.filled.VideoLibrary
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.tv.material3.Icon
import androidx.tv.material3.Text
import coil.compose.AsyncImage
@@ -177,21 +148,21 @@ internal val TvRailContentShift = 112.dp
* while For You is a *narrow* one a handful of ranked titles so it belongs down beside
* the calendar with the other answers rather than above the shelves it draws from.
*/
enum class BrowseDestination(val label: String, val icon: ImageVector) {
HOME("Home", Icons.Default.Home),
enum class BrowseDestination(val label: String, val icon: MembyIcon) {
HOME("Home", MembyIcon.Home),
// The catalogue, browsed by genre rather than by shelf. Hidden unless the gateway has
// the genre browser on — see [TvNavigationRail]'s genresEnabled.
GENRES("Genres", Icons.Default.GridView),
SEARCH("Search", Icons.Default.Search),
MOVIES("Movies", Icons.Default.Movie),
SHOWS("TV Shows", Icons.Default.Tv),
FOR_YOU("For You", Icons.Default.AutoAwesome),
GENRES("Genres", MembyIcon.Grid),
SEARCH("Search", MembyIcon.Search),
MOVIES("Movies", MembyIcon.Movie),
SHOWS("TV Shows", MembyIcon.Tv),
FOR_YOU("For You", MembyIcon.Sparkle),
// Sonarr's schedule, a month at a time. Hidden unless the gateway says the household
// has one — see [TvNavigationRail]'s calendarEnabled.
CALENDAR("TV Calendar", Icons.Default.CalendarMonth),
FAVORITES("Favourites", Icons.Default.Favorite),
PROFILES("User", Icons.Default.Person),
SETTINGS("Settings", Icons.Default.Settings),
CALENDAR("TV Calendar", MembyIcon.Calendar),
FAVORITES("Favourites", MembyIcon.Favourite),
PROFILES("User", MembyIcon.Person),
SETTINGS("Settings", MembyIcon.Settings),
}
/**
@@ -229,40 +200,40 @@ private data class HomeRowVisual(
private fun homeRowVisual(row: HomeBrowseRow): HomeRowVisual = when {
row.id == "continue" -> HomeRowVisual(
Icons.Default.PlayCircleFilled,
MembyIcon.PlayCircle.mark,
)
row.id == "continue-shows" -> HomeRowVisual(
Icons.Default.PlayCircleFilled,
MembyIcon.PlayCircle.mark,
)
row.kind == MediaRowKind.FAVORITES -> HomeRowVisual(
Icons.Default.Favorite,
MembyIcon.Favourite.mark,
)
row.id == "latest-movies" -> HomeRowVisual(
Icons.Default.Movie,
MembyIcon.Movie.mark,
)
row.id == "sonarr-airing-today" -> HomeRowVisual(
Icons.Default.CalendarMonth,
MembyIcon.Calendar.mark,
)
row.id == "curated:apple-tv" -> HomeRowVisual(
Icons.Default.LiveTv,
MembyIcon.LiveTv.mark,
)
row.id == "curated:drama-shows" -> HomeRowVisual(
Icons.Default.TheaterComedy,
MembyIcon.Drama.mark,
)
row.id == "curated:comedy-shows" -> HomeRowVisual(
Icons.Default.SentimentVerySatisfied,
MembyIcon.Happy.mark,
)
row.id.startsWith("similar:") -> HomeRowVisual(
Icons.Default.AutoAwesome,
MembyIcon.Sparkle.mark,
)
row.id == "recommended" -> HomeRowVisual(
Icons.Default.Recommend,
MembyIcon.Recommend.mark,
)
row.id.startsWith("for-you:") -> HomeRowVisual(
Icons.Default.AutoAwesome,
MembyIcon.Sparkle.mark,
)
else -> HomeRowVisual(
Icons.Default.VideoLibrary,
MembyIcon.VideoLibrary.mark,
)
}
@@ -632,7 +603,7 @@ fun UserSwitcherOverlay(
// what replaces the bell that used to sit in the corner of Home.
UserSwitcherAction(
label = "Notifications",
icon = Icons.Default.Notifications,
icon = MembyIcon.Notification.mark,
badge = alertBadgeLabel(alertCount),
modifier = Modifier
.focusRequester(focusRequesters[profiles.size])
@@ -645,7 +616,7 @@ fun UserSwitcherOverlay(
if (showRequests) {
UserSwitcherAction(
label = "Requests",
icon = Icons.Default.PlaylistAdd,
icon = MembyIcon.PlaylistAdd.mark,
modifier = Modifier
.focusRequester(focusRequesters[profiles.size + 1])
.onFocusChanged {
@@ -657,7 +628,7 @@ fun UserSwitcherOverlay(
val settingsIndex = profiles.size + if (showRequests) 2 else 1
UserSwitcherAction(
label = "Settings",
icon = Icons.Default.Settings,
icon = MembyIcon.Settings.mark,
modifier = Modifier
.focusRequester(focusRequesters[settingsIndex])
.onFocusChanged {
@@ -668,7 +639,7 @@ fun UserSwitcherOverlay(
val manageIndex = profiles.size + actionCount - 1
UserSwitcherAction(
label = "Manage users",
icon = Icons.Default.Person,
icon = MembyIcon.Person.mark,
modifier = Modifier
.focusRequester(focusRequesters[manageIndex])
.onFocusChanged {
@@ -743,7 +714,7 @@ private fun UserSwitcherProfileItem(
)
if (current) {
Icon(
Icons.Default.CheckCircle,
MembyIcon.CheckCircle.mark,
contentDescription = null,
tint = EmbyGreen,
modifier = Modifier.size(14.dp),
@@ -876,7 +847,7 @@ fun ExpandableNavigationItem(
)
}
} else {
Icon(destination.icon, contentDescription = null, tint = foreground.value, modifier = Modifier.size(21.dp))
Icon(destination.icon.mark, contentDescription = null, tint = foreground.value, modifier = Modifier.size(21.dp))
}
if (selected) {
Box(
@@ -1158,7 +1129,7 @@ fun MediaQuickActionsOverlay(
)
QuickActionMenuItem(
label = "View details",
icon = Icons.Default.Info,
icon = MembyIcon.Info.mark,
modifier = Modifier
.focusRequester(focusRequesters[0])
.onFocusChanged { if (it.isFocused) focusedIndex = 0 },
@@ -1167,7 +1138,7 @@ fun MediaQuickActionsOverlay(
Spacer(Modifier.height(2.dp))
QuickActionMenuItem(
label = if (item.isFavorite) "Remove from favourites" else "Add to favourites",
icon = if (item.isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
icon = if (item.isFavorite) MembyIcon.Favourite.mark else MembyIcon.FavouriteOutline.mark,
modifier = Modifier
.focusRequester(focusRequesters[1])
.onFocusChanged { if (it.isFocused) focusedIndex = 1 },
@@ -1179,7 +1150,7 @@ fun MediaQuickActionsOverlay(
Spacer(Modifier.height(2.dp))
QuickActionMenuItem(
label = if (item.userData?.played == true) "Mark unwatched" else "Mark watched",
icon = Icons.Default.CheckCircle,
icon = MembyIcon.CheckCircle.mark,
modifier = Modifier
.focusRequester(focusRequesters[2])
.onFocusChanged { if (it.isFocused) focusedIndex = 2 },
@@ -1192,7 +1163,7 @@ fun MediaQuickActionsOverlay(
Spacer(Modifier.height(2.dp))
QuickActionMenuItem(
label = "Remove from Continue Watching",
icon = Icons.Default.PlaylistRemove,
icon = MembyIcon.PlaylistRemove.mark,
modifier = Modifier
.focusRequester(focusRequesters[3])
.onFocusChanged { if (it.isFocused) focusedIndex = 3 },
@@ -1217,7 +1188,7 @@ fun MediaQuickActionsOverlay(
)
QuickActionMenuItem(
label = if (rowPinned) "Unpin row" else "Pin row to top",
icon = Icons.Default.PushPin,
icon = MembyIcon.Pin.mark,
modifier = Modifier
.focusRequester(focusRequesters[rowActionStartIndex])
.onFocusChanged { if (it.isFocused) focusedIndex = rowActionStartIndex },
@@ -1225,7 +1196,7 @@ fun MediaQuickActionsOverlay(
)
QuickActionMenuItem(
label = "Move row up",
icon = Icons.Default.ArrowUpward,
icon = MembyIcon.ArrowUp.mark,
modifier = Modifier
.focusRequester(focusRequesters[rowActionStartIndex + 1])
.onFocusChanged { if (it.isFocused) focusedIndex = rowActionStartIndex + 1 },
@@ -1233,7 +1204,7 @@ fun MediaQuickActionsOverlay(
)
QuickActionMenuItem(
label = "Move row down",
icon = Icons.Default.ArrowDownward,
icon = MembyIcon.ArrowDown.mark,
modifier = Modifier
.focusRequester(focusRequesters[rowActionStartIndex + 2])
.onFocusChanged { if (it.isFocused) focusedIndex = rowActionStartIndex + 2 },
@@ -1241,7 +1212,7 @@ fun MediaQuickActionsOverlay(
)
QuickActionMenuItem(
label = "Hide this row",
icon = Icons.Default.VisibilityOff,
icon = MembyIcon.HideWatched.mark,
modifier = Modifier
.focusRequester(focusRequesters[rowActionStartIndex + 3])
.onFocusChanged { if (it.isFocused) focusedIndex = rowActionStartIndex + 3 },
@@ -1258,7 +1229,7 @@ fun MediaQuickActionsOverlay(
Spacer(Modifier.height(6.dp))
QuickActionMenuItem(
label = "Close",
icon = Icons.Default.ChevronLeft,
icon = MembyIcon.ChevronLeft.mark,
modifier = Modifier
.focusRequester(focusRequesters[actionCount - 1])
.onFocusChanged { if (it.isFocused) focusedIndex = actionCount - 1 },
@@ -1487,11 +1458,11 @@ private fun MetadataStatus(item: BaseItem) {
Text("${(progress * 100).toInt()}% watched", color = MutedText, fontSize = 13.sp)
}
if (item.userData?.played == true) {
Icon(Icons.Default.CheckCircle, contentDescription = "Watched", tint = EmbyGreen, modifier = Modifier.size(17.dp))
Icon(MembyIcon.CheckCircle.mark, contentDescription = "Watched", tint = EmbyGreen, modifier = Modifier.size(17.dp))
Text("Watched", color = MutedText, fontSize = 13.sp)
}
if (item.isFavorite) {
Icon(Icons.Default.Favorite, contentDescription = "Favourite", tint = EmbyGreen, modifier = Modifier.size(17.dp))
Icon(MembyIcon.Favourite.mark, contentDescription = "Favourite", tint = EmbyGreen, modifier = Modifier.size(17.dp))
Text("Favourite", color = MutedText, fontSize = 13.sp)
}
}
@@ -1542,8 +1513,8 @@ internal fun HomeRowHeaderIcon(icon: ImageVector, modifier: Modifier = Modifier)
*/
internal fun mediaTypeMark(item: BaseItem): Pair<ImageVector, String>? = when {
item.isSeries || item.isEpisode || item.type.equals("Season", ignoreCase = true) ->
Icons.Default.Tv to "TV show"
item.type.equals("Movie", ignoreCase = true) -> Icons.Default.Movie to "Film"
MembyIcon.Tv.mark to "TV show"
item.type.equals("Movie", ignoreCase = true) -> MembyIcon.Movie.mark to "Film"
else -> null
}
@@ -1817,7 +1788,7 @@ private fun FavoriteShowsEmptyState(
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Default.FavoriteBorder,
imageVector = MembyIcon.FavouriteOutline.mark,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(25.dp),
@@ -1872,7 +1843,7 @@ private fun GalleryJumpButton(
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = if (forward) Icons.Default.ChevronRight else Icons.Default.ChevronLeft,
imageVector = if (forward) MembyIcon.ChevronRight.mark else MembyIcon.ChevronLeft.mark,
contentDescription = null,
tint = if (enabled) Color.White else QuietText.copy(alpha = 0.45f),
modifier = Modifier.size(23.dp),
@@ -2125,7 +2096,7 @@ private fun MediaCard(
}
if (imageUrl == null || failed) {
Icon(
Icons.Default.BrokenImage,
MembyIcon.BrokenImage.mark,
contentDescription = "Artwork unavailable",
tint = QuietText,
modifier = Modifier.size(30.dp),
@@ -2146,14 +2117,14 @@ private fun MediaCard(
) {
if (item.userData?.played == true) {
MediaStatusIcon(
icon = Icons.Default.CheckCircle,
icon = MembyIcon.CheckCircle.mark,
description = "Watched",
tint = EmbyGreen,
)
}
if (item.isFavorite) {
MediaStatusIcon(
icon = Icons.Default.Favorite,
icon = MembyIcon.Favourite.mark,
description = "Favourite",
tint = Color(0xFFFF6B81),
)
@@ -1,5 +1,7 @@
package com.ponzischeme89.memby.ui
import com.ponzischeme89.memby.ui.theme.MembyIcon
import com.ponzischeme89.memby.ui.theme.mark
import android.content.Intent
import android.os.Build
import android.os.Bundle
@@ -117,11 +119,6 @@ import coil.compose.AsyncImage
import coil.imageLoader
import coil.request.ImageRequest
import com.ponzischeme89.memby.R
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.LightMode
import androidx.compose.material.icons.filled.NightsStay
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.WbSunny
import com.ponzischeme89.memby.BuildConfig
import com.ponzischeme89.memby.ServiceLocator
import com.ponzischeme89.memby.data.Settings
@@ -4111,7 +4108,7 @@ private fun RecentSearchesRow(
modifier = Modifier.fillMaxWidth().padding(horizontal = 36.dp),
verticalAlignment = Alignment.CenterVertically,
) {
HomeRowHeaderIcon(Icons.Default.Search)
HomeRowHeaderIcon(MembyIcon.Search.mark)
Spacer(Modifier.width(HomeRowHeaderIconGap))
Text(
"Recent searches",
@@ -4212,9 +4209,9 @@ private fun HomeClock(
) {
Icon(
imageVector = when (period) {
HomeGreetingPeriod.MORNING -> Icons.Default.LightMode
HomeGreetingPeriod.AFTERNOON -> Icons.Default.WbSunny
HomeGreetingPeriod.EVENING -> Icons.Default.NightsStay
HomeGreetingPeriod.MORNING -> MembyIcon.Sunrise.mark
HomeGreetingPeriod.AFTERNOON -> MembyIcon.Sun.mark
HomeGreetingPeriod.EVENING -> MembyIcon.Night.mark
},
contentDescription = null,
tint = MembyAccent,
@@ -1,5 +1,7 @@
package com.ponzischeme89.memby.ui
import com.ponzischeme89.memby.ui.theme.MembyIcon
import com.ponzischeme89.memby.ui.theme.mark
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
@@ -27,8 +29,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Build
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -288,7 +288,7 @@ private fun PulsingEmblem(pulse: Float, gearRotation: Float) {
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Default.Build,
imageVector = MembyIcon.Build.mark,
contentDescription = null,
tint = MaintenanceAccent,
modifier = Modifier
@@ -1,12 +1,8 @@
package com.ponzischeme89.memby.ui
import com.ponzischeme89.memby.ui.theme.MembyIcon
import com.ponzischeme89.memby.ui.theme.mark
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DoneAll
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.FirstPage
import androidx.compose.material.icons.filled.Movie
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -257,7 +253,7 @@ internal fun MediaDetailContent(
DetailHeroAction(
// A heart, not a tick: "Mark watched" two buttons along is a tick as
// well. The heart is what a home card and the screensaver already use.
icon = if (item.isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
icon = if (item.isFavorite) MembyIcon.Favourite.mark else MembyIcon.FavouriteOutline.mark,
description = if (item.isFavorite) "Remove from Favourites" else "Add to Favourites",
active = item.isFavorite,
onClick = {
@@ -268,11 +264,11 @@ internal fun MediaDetailContent(
),
)
trailer?.let {
add(DetailHeroAction(Icons.Default.Movie, "Play trailer", onClick = { onPlayTrailer(item) }))
add(DetailHeroAction(MembyIcon.Movie.mark, "Play trailer", onClick = { onPlayTrailer(item) }))
}
add(
DetailHeroAction(
icon = Icons.Default.DoneAll,
icon = MembyIcon.CheckAll.mark,
description = if (item.userData?.played == true) "Mark unwatched" else "Mark watched",
active = item.userData?.played == true,
onClick = { onTogglePlayed(item, item.userData?.played != true) },
@@ -283,7 +279,7 @@ internal fun MediaDetailContent(
franchise?.takeIf { it.firstMovie.id != item.id }?.let { start ->
add(
DetailHeroAction(
icon = Icons.Default.FirstPage,
icon = MembyIcon.FirstPage.mark,
description = "Open the first ${start.name} movie, ${start.firstMovie.name}",
label = "Start with ${start.firstMovie.name}",
onClick = { onOpenItem(start.firstMovie) },
@@ -1,5 +1,7 @@
package com.ponzischeme89.memby.ui
import com.ponzischeme89.memby.ui.theme.MembyIcon
import com.ponzischeme89.memby.ui.theme.mark
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
@@ -13,9 +15,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -64,7 +63,7 @@ internal fun MembyPlayButton(
val scale by animateFloatAsState(if (focused) 1.055f else 1f, tween(100), label = "play-focus")
PrimaryActionSurface(
label = label,
icon = Icons.Default.PlayArrow,
icon = MembyIcon.Play.mark,
focused = focused,
compact = compact,
modifier = modifier
@@ -83,7 +82,7 @@ internal fun MembyPlayChip(
) {
PrimaryActionSurface(
label = label,
icon = Icons.Default.PlayArrow,
icon = MembyIcon.Play.mark,
focused = focused,
compact = compact,
modifier = modifier,
@@ -108,7 +107,7 @@ internal fun MembyArtworkPlayCue(modifier: Modifier = Modifier) {
contentAlignment = Alignment.Center,
) {
Icon(
Icons.Default.PlayArrow,
MembyIcon.Play.mark,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(24.dp),
@@ -186,7 +185,7 @@ internal fun MembyChoiceChip(
) {
if (selected) {
Icon(
Icons.Default.Check,
MembyIcon.Check.mark,
contentDescription = null,
tint = if (focused) Color.Black else Color.White,
modifier = Modifier.size(15.dp),
@@ -2,6 +2,8 @@
package com.ponzischeme89.memby.ui
import com.ponzischeme89.memby.ui.theme.MembyIcon
import com.ponzischeme89.memby.ui.theme.mark
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
@@ -19,8 +21,6 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bookmark
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
@@ -77,7 +77,7 @@ internal fun MyShowsStrip(
modifier = Modifier.fillMaxWidth().padding(horizontal = 36.dp),
verticalAlignment = Alignment.CenterVertically,
) {
HomeRowHeaderIcon(Icons.Default.Bookmark)
HomeRowHeaderIcon(MembyIcon.Bookmark.mark)
Spacer(Modifier.width(HomeRowHeaderIconGap))
Text(
"My Shows",
@@ -1,5 +1,7 @@
package com.ponzischeme89.memby.ui
import com.ponzischeme89.memby.ui.theme.MembyIcon
import com.ponzischeme89.memby.ui.theme.mark
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
@@ -26,13 +28,6 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bookmark
import androidx.compose.material.icons.filled.BookmarkBorder
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.Movie
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -377,7 +372,7 @@ internal fun SeriesDetailContent(
// two adjacent circles that both read as "add this" say nothing about
// which list is which. The heart is already what a home card, the
// screensaver and the Favourites row header use for this.
icon = if (item.isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
icon = if (item.isFavorite) MembyIcon.Favourite.mark else MembyIcon.FavouriteOutline.mark,
description = if (item.isFavorite) "Remove from Favourites" else "Add to Favourites",
active = item.isFavorite,
onClick = {
@@ -392,14 +387,14 @@ internal fun SeriesDetailContent(
// Filled/outline says on-or-off for both toggles, so the two differ by
// silhouette alone — heart against bookmark. The +/✓ variants said it
// a second way and borrowed the glyphs the other buttons use.
icon = if (isMyShow) Icons.Default.Bookmark else Icons.Default.BookmarkBorder,
icon = if (isMyShow) MembyIcon.Bookmark.mark else MembyIcon.BookmarkOutline.mark,
description = if (isMyShow) "Remove from My Shows" else "Add to My Shows",
active = isMyShow,
onClick = { onToggleMyShow(item, !isMyShow) },
))
}
trailer?.let {
add(DetailHeroAction(Icons.Default.Movie, "Play trailer", onClick = { onPlayTrailer(item) }))
add(DetailHeroAction(MembyIcon.Movie.mark, "Play trailer", onClick = { onPlayTrailer(item) }))
}
},
) { visibleTab ->
@@ -681,7 +676,7 @@ internal fun EpisodeCard(
}
if (episode.isPlayed) {
Icon(
Icons.Default.CheckCircle,
MembyIcon.CheckCircle.mark,
contentDescription = "Watched",
tint = DetailAccent,
modifier = Modifier
@@ -1,5 +1,7 @@
package com.ponzischeme89.memby.ui
import com.ponzischeme89.memby.ui.theme.MembyIcon
import com.ponzischeme89.memby.ui.theme.mark
import android.os.Build
import android.content.Context
import androidx.activity.compose.BackHandler
@@ -29,8 +31,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -283,7 +283,7 @@ fun UpdateScreen(
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Default.ArrowDownward,
imageVector = MembyIcon.ArrowDown.mark,
contentDescription = null,
tint = UpdateAccent,
modifier = Modifier.size(40.dp),
@@ -1,5 +1,7 @@
package com.ponzischeme89.memby.ui.alerts
import com.ponzischeme89.memby.ui.theme.MembyIcon
import com.ponzischeme89.memby.ui.theme.mark
import android.provider.Settings as AndroidSettings
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
@@ -29,11 +31,6 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.NotificationsActive
import androidx.compose.material.icons.filled.NotificationsNone
import androidx.compose.material.icons.filled.NotificationsOff
import androidx.compose.material.icons.filled.Tv
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -332,7 +329,7 @@ private fun AlertsEmptyMark(listening: Boolean) {
drawAlertsEmptyMark(if (animate) progress.value else null, mark)
}
Icon(
if (listening) Icons.Default.NotificationsNone else Icons.Default.NotificationsOff,
if (listening) MembyIcon.NotificationNone.mark else MembyIcon.NotificationOff.mark,
contentDescription = null,
tint = mark,
modifier = Modifier
@@ -498,8 +495,8 @@ private fun AlertRow(
}
private fun alertIcon(kind: String): ImageVector = when {
kind.contains("return", ignoreCase = true) -> Icons.Default.Tv
kind.contains("series", ignoreCase = true) -> Icons.Default.Tv
kind.startsWith("show-", ignoreCase = true) -> Icons.Default.Tv
else -> Icons.Default.NotificationsActive
kind.contains("return", ignoreCase = true) -> MembyIcon.Tv.mark
kind.contains("series", ignoreCase = true) -> MembyIcon.Tv.mark
kind.startsWith("show-", ignoreCase = true) -> MembyIcon.Tv.mark
else -> MembyIcon.NotificationActive.mark
}
@@ -2,6 +2,8 @@
package com.ponzischeme89.memby.ui.calendar
import com.ponzischeme89.memby.ui.theme.MembyIcon
import com.ponzischeme89.memby.ui.theme.mark
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -25,11 +27,6 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.CalendarMonth
import androidx.compose.material.icons.filled.Event
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -232,7 +229,7 @@ private fun AgendaHeader(
Modifier.size(38.dp).background(MembyAccent.copy(alpha = 0.14f), CircleShape),
contentAlignment = Alignment.Center,
) {
Icon(Icons.Default.CalendarMonth, null, tint = MembyAccent, modifier = Modifier.size(21.dp))
Icon(MembyIcon.Calendar.mark, null, tint = MembyAccent, modifier = Modifier.size(21.dp))
}
Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f)) {
@@ -246,7 +243,7 @@ private fun AgendaHeader(
modifier = Modifier.padding(end = 14.dp),
)
AgendaIconButton(
icon = Icons.AutoMirrored.Filled.ArrowBack,
icon = MembyIcon.ArrowBack.mark,
description = "Previous month",
enabled = previousMonth.isNotEmpty(),
onClick = { onShowMonth(previousMonth) },
@@ -262,7 +259,7 @@ private fun AgendaHeader(
)
Spacer(Modifier.width(8.dp))
AgendaIconButton(
icon = Icons.AutoMirrored.Filled.ArrowForward,
icon = MembyIcon.ArrowForward.mark,
description = "Next month",
enabled = nextMonth.isNotEmpty(),
onClick = { onShowMonth(nextMonth) },
@@ -303,7 +300,7 @@ private fun WeekSwitcher(
) {
Row(verticalAlignment = Alignment.CenterVertically) {
AgendaIconButton(
Icons.AutoMirrored.Filled.ArrowBack,
MembyIcon.ArrowBack.mark,
"Previous week",
weekIndex > 0,
onPrevious,
@@ -326,7 +323,7 @@ private fun WeekSwitcher(
.padding(horizontal = 18.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(Icons.Default.Event, null, tint = if (focused) MembySurface else MembyAccent, modifier = Modifier.size(16.dp))
Icon(MembyIcon.Event.mark, null, tint = if (focused) MembySurface else MembyAccent, modifier = Modifier.size(16.dp))
Spacer(Modifier.width(8.dp))
Text(
week.label,
@@ -344,7 +341,7 @@ private fun WeekSwitcher(
}
Spacer(Modifier.width(10.dp))
AgendaIconButton(
Icons.AutoMirrored.Filled.ArrowForward,
MembyIcon.ArrowForward.mark,
"Next week",
weekIndex < weekCount - 1,
onNext,
@@ -498,7 +495,7 @@ private fun ProgrammePane(
contentAlignment = Alignment.Center,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(Icons.Default.Event, null, tint = MembyQuietText, modifier = Modifier.size(30.dp))
Icon(MembyIcon.Event.mark, null, tint = MembyQuietText, modifier = Modifier.size(30.dp))
Spacer(Modifier.height(8.dp))
Text("Nothing airing", color = MembyMutedText, fontSize = 15.sp)
Text("Choose another day or week", color = MembyQuietText, fontSize = 12.sp)
@@ -2,6 +2,8 @@
package com.ponzischeme89.memby.ui.genre
import com.ponzischeme89.memby.ui.theme.MembyIcon
import com.ponzischeme89.memby.ui.theme.mark
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
@@ -34,23 +36,6 @@ import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.itemsIndexed as rowItemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.HelpOutline
import androidx.compose.material.icons.filled.AutoAwesome
import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.filled.Category
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Gavel
import androidx.compose.material.icons.filled.Landscape
import androidx.compose.material.icons.filled.LiveTv
import androidx.compose.material.icons.filled.LocalFireDepartment
import androidx.compose.material.icons.filled.MilitaryTech
import androidx.compose.material.icons.filled.MusicNote
import androidx.compose.material.icons.filled.SentimentVerySatisfied
import androidx.compose.material.icons.filled.SportsSoccer
import androidx.compose.material.icons.filled.TheaterComedy
import androidx.compose.material.icons.filled.VideoLibrary
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -188,23 +173,23 @@ private fun GenreDiscoveryCard(
private data class GenreVisual(val icon: ImageVector, val colour: Color)
private fun genreVisual(icon: GenreCategoryIcon): GenreVisual = when (icon) {
GenreCategoryIcon.ALL -> GenreVisual(Icons.Default.Category, Color(0xFF4F46A5))
GenreCategoryIcon.ACTION -> GenreVisual(Icons.Default.Bolt, Color(0xFFB45309))
GenreCategoryIcon.COMEDY -> GenreVisual(Icons.Default.TheaterComedy, Color(0xFF15803D))
GenreCategoryIcon.CRIME -> GenreVisual(Icons.Default.Gavel, Color(0xFF475569))
GenreCategoryIcon.DRAMA -> GenreVisual(Icons.Default.TheaterComedy, Color(0xFF7E22CE))
GenreCategoryIcon.HORROR -> GenreVisual(Icons.Default.LocalFireDepartment, Color(0xFF991B1B))
GenreCategoryIcon.MYSTERY -> GenreVisual(Icons.AutoMirrored.Filled.HelpOutline, Color(0xFF4338CA))
GenreCategoryIcon.SCI_FI -> GenreVisual(Icons.Default.AutoAwesome, Color(0xFF0369A1))
GenreCategoryIcon.THRILLER -> GenreVisual(Icons.Default.VisibilityOff, Color(0xFF0F766E))
GenreCategoryIcon.WAR -> GenreVisual(Icons.Default.MilitaryTech, Color(0xFF57534E))
GenreCategoryIcon.FAMILY -> GenreVisual(Icons.Default.SentimentVerySatisfied, Color(0xFFDB2777))
GenreCategoryIcon.DOCUMENTARY -> GenreVisual(Icons.Default.VideoLibrary, Color(0xFF0E7490))
GenreCategoryIcon.ROMANCE -> GenreVisual(Icons.Default.Favorite, Color(0xFFBE185D))
GenreCategoryIcon.WESTERN -> GenreVisual(Icons.Default.Landscape, Color(0xFF92400E))
GenreCategoryIcon.MUSIC -> GenreVisual(Icons.Default.MusicNote, Color(0xFF6D28D9))
GenreCategoryIcon.SPORT -> GenreVisual(Icons.Default.SportsSoccer, Color(0xFF047857))
GenreCategoryIcon.REALITY -> GenreVisual(Icons.Default.LiveTv, Color(0xFFC2410C))
GenreCategoryIcon.ALL -> GenreVisual(MembyIcon.Category.mark, Color(0xFF4F46A5))
GenreCategoryIcon.ACTION -> GenreVisual(MembyIcon.Bolt.mark, Color(0xFFB45309))
GenreCategoryIcon.COMEDY -> GenreVisual(MembyIcon.Drama.mark, Color(0xFF15803D))
GenreCategoryIcon.CRIME -> GenreVisual(MembyIcon.Gavel.mark, Color(0xFF475569))
GenreCategoryIcon.DRAMA -> GenreVisual(MembyIcon.Drama.mark, Color(0xFF7E22CE))
GenreCategoryIcon.HORROR -> GenreVisual(MembyIcon.Fire.mark, Color(0xFF991B1B))
GenreCategoryIcon.MYSTERY -> GenreVisual(MembyIcon.Help.mark, Color(0xFF4338CA))
GenreCategoryIcon.SCI_FI -> GenreVisual(MembyIcon.Sparkle.mark, Color(0xFF0369A1))
GenreCategoryIcon.THRILLER -> GenreVisual(MembyIcon.HideWatched.mark, Color(0xFF0F766E))
GenreCategoryIcon.WAR -> GenreVisual(MembyIcon.Trophy.mark, Color(0xFF57534E))
GenreCategoryIcon.FAMILY -> GenreVisual(MembyIcon.Happy.mark, Color(0xFFDB2777))
GenreCategoryIcon.DOCUMENTARY -> GenreVisual(MembyIcon.VideoLibrary.mark, Color(0xFF0E7490))
GenreCategoryIcon.ROMANCE -> GenreVisual(MembyIcon.Favourite.mark, Color(0xFFBE185D))
GenreCategoryIcon.WESTERN -> GenreVisual(MembyIcon.Landscape.mark, Color(0xFF92400E))
GenreCategoryIcon.MUSIC -> GenreVisual(MembyIcon.Music.mark, Color(0xFF6D28D9))
GenreCategoryIcon.SPORT -> GenreVisual(MembyIcon.Football.mark, Color(0xFF047857))
GenreCategoryIcon.REALITY -> GenreVisual(MembyIcon.LiveTv.mark, Color(0xFFC2410C))
}
/**
@@ -1,5 +1,7 @@
package com.ponzischeme89.memby.ui.requests
import com.ponzischeme89.memby.ui.theme.MembyIcon
import com.ponzischeme89.memby.ui.theme.mark
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
@@ -16,13 +18,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Movie
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Schedule
import androidx.compose.material.icons.filled.Tv
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -188,20 +183,20 @@ internal fun RequestCard(
)
}
if (busy) {
RequestTrailing(icon = Icons.Default.Schedule, label = "Asking", focused = focused)
RequestTrailing(icon = MembyIcon.Schedule.mark, label = "Asking", focused = focused)
} else if (action != null) {
RequestTrailing(icon = action.icon, label = action.label, focused = focused)
RequestTrailing(icon = action.icon.mark, label = action.label, focused = focused)
}
}
}
}
/** The trailing affordance on a card: what the centre button would do. */
internal data class RequestCardAction(val icon: ImageVector, val label: String)
internal data class RequestCardAction(val icon: MembyIcon, val label: String)
internal val RequestActionRequest = RequestCardAction(Icons.Default.Add, "Request")
internal val RequestActionPlay = RequestCardAction(Icons.Default.PlayArrow, "Watch")
internal val RequestActionRemove = RequestCardAction(Icons.Default.CheckCircle, "Remove")
internal val RequestActionRequest = RequestCardAction(MembyIcon.Add, "Request")
internal val RequestActionPlay = RequestCardAction(MembyIcon.Play, "Watch")
internal val RequestActionRemove = RequestCardAction(MembyIcon.CheckCircle, "Remove")
@Composable
private fun RequestTrailing(icon: ImageVector, label: String, focused: Boolean) {
@@ -287,7 +282,7 @@ internal fun requestToneColour(status: String): Color = when (requestStatusTone(
*/
@Composable
private fun MediaTypeGlyph(mediaType: String, modifier: Modifier = Modifier) {
val icon = if (mediaType == "series") Icons.Default.Tv else Icons.Default.Movie
val icon = if (mediaType == "series") MembyIcon.Tv.mark else MembyIcon.Movie.mark
Box(
modifier
.size(24.dp)
@@ -2,6 +2,8 @@
package com.ponzischeme89.memby.ui.requests
import com.ponzischeme89.memby.ui.theme.MembyIcon
import com.ponzischeme89.memby.ui.theme.mark
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -24,10 +26,6 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Inbox
import androidx.compose.material.icons.filled.PlaylistAdd
import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -171,7 +169,7 @@ fun RequestsScreen(
// Permission withdrawn while the page was open. Said plainly, because an
// empty list would read as "you have never asked for anything".
!state.allowed -> RequestsNotice(
icon = Icons.Default.Inbox,
icon = MembyIcon.Inbox.mark,
heading = "Requests are not available",
body = "This profile is no longer allowed to request titles. Ask whoever looks after Memby.",
)
@@ -227,7 +225,7 @@ private fun RequestsHeader(state: RequestsUiState) {
contentAlignment = Alignment.Center,
) {
Icon(
Icons.Default.PlaylistAdd, null,
MembyIcon.PlaylistAdd.mark, null,
tint = MembyAccent, modifier = Modifier.size(21.dp),
)
}
@@ -321,7 +319,7 @@ private fun RequestsTabStrip(
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
if (tab == RequestsTab.MINE) Icons.Default.Inbox else Icons.Default.Search,
if (tab == RequestsTab.MINE) MembyIcon.Inbox.mark else MembyIcon.Search.mark,
null,
tint = if (focused) MembySurface else MembyAccent,
modifier = Modifier.size(15.dp),
@@ -361,7 +359,7 @@ private fun MyRequestsPane(
}
if (state.requestsError != null && state.requests.isEmpty()) {
RequestsNotice(
icon = Icons.Default.Inbox,
icon = MembyIcon.Inbox.mark,
heading = "Could not load your requests",
body = state.requestsError,
action = "Try again",
@@ -375,7 +373,7 @@ private fun MyRequestsPane(
}
if (state.requests.isEmpty()) {
RequestsNotice(
icon = Icons.Default.Inbox,
icon = MembyIcon.Inbox.mark,
heading = "You have not asked for anything yet",
body = "Find a film or series and it will show up here while it is added to your library.",
action = "Request something",
@@ -762,7 +760,7 @@ private fun PaneMessage(heading: String, body: String) {
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(horizontal = 40.dp),
) {
Icon(Icons.Default.Search, null, tint = MembyQuietText, modifier = Modifier.size(30.dp))
Icon(MembyIcon.Search.mark, null, tint = MembyQuietText, modifier = Modifier.size(30.dp))
Spacer(Modifier.height(8.dp))
Text(heading, color = MembyMutedText, fontSize = 15.sp, fontWeight = FontWeight.SemiBold)
Spacer(Modifier.height(4.dp))
@@ -1,5 +1,7 @@
package com.ponzischeme89.memby.ui.screensaver
import com.ponzischeme89.memby.ui.theme.MembyIcon
import com.ponzischeme89.memby.ui.theme.mark
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.Animatable
@@ -22,19 +24,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronLeft
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.AccessTime
import androidx.compose.material.icons.filled.Business
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.LiveTv
import androidx.compose.material.icons.filled.Movie
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.LocalOffer
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -838,12 +827,12 @@ private fun InfoAndActions(
verticalAlignment = Alignment.CenterVertically,
) {
Button(onClick = onPlay, modifier = Modifier.focusRequester(playFocus)) {
Icon(Icons.Default.PlayArrow, contentDescription = null)
Icon(MembyIcon.Play.mark, contentDescription = null)
Text(text = " Play trailer", modifier = Modifier.padding(start = 4.dp))
}
Button(onClick = onToggleFavorite) {
Icon(
imageVector = if (isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
imageVector = if (isFavorite) MembyIcon.Favourite.mark else MembyIcon.FavouriteOutline.mark,
contentDescription = null,
)
Text(
@@ -852,13 +841,13 @@ private fun InfoAndActions(
)
}
Spacer(Modifier.weight(1f))
Button(onClick = onPrev) { Icon(Icons.Default.ChevronLeft, contentDescription = "Previous") }
Button(onClick = onNext) { Icon(Icons.Default.ChevronRight, contentDescription = "Next") }
Button(onClick = onPrev) { Icon(MembyIcon.ChevronLeft.mark, contentDescription = "Previous") }
Button(onClick = onNext) { Icon(MembyIcon.ChevronRight.mark, contentDescription = "Next") }
Button(onClick = onOpenSettings) {
Icon(Icons.Default.Settings, contentDescription = "Settings")
Icon(MembyIcon.Settings.mark, contentDescription = "Settings")
}
Button(onClick = onExit) {
Icon(Icons.Default.Close, contentDescription = "Exit screensaver")
Icon(MembyIcon.Close.mark, contentDescription = "Exit screensaver")
}
}
}
@@ -933,7 +922,7 @@ private fun MediaMetadata(item: BaseItem) {
val metadataColor = Color(0xFFC7CED4)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Icon(
imageVector = if (item.isSeries) Icons.Default.LiveTv else Icons.Default.Movie,
imageVector = if (item.isSeries) MembyIcon.LiveTv.mark else MembyIcon.Movie.mark,
contentDescription = if (item.isSeries) "Series" else "Movie",
tint = metadataColor,
modifier = Modifier.size(20.dp),
@@ -941,15 +930,15 @@ private fun MediaMetadata(item: BaseItem) {
item.productionYear?.let { MetadataText(it.toString(), metadataColor) }
item.communityRating?.let { MetadataText("${"%.1f".format(it)}", metadataColor) }
item.runtimeMinutes?.let {
Icon(Icons.Default.AccessTime, contentDescription = "Runtime", tint = metadataColor, modifier = Modifier.size(18.dp))
Icon(MembyIcon.Clock.mark, contentDescription = "Runtime", tint = metadataColor, modifier = Modifier.size(18.dp))
MetadataText("$it min", metadataColor)
}
item.studios.firstOrNull { it.name.isNotBlank() }?.name?.let {
Icon(Icons.Default.Business, contentDescription = "Studio", tint = metadataColor, modifier = Modifier.size(18.dp))
Icon(MembyIcon.Studio.mark, contentDescription = "Studio", tint = metadataColor, modifier = Modifier.size(18.dp))
MetadataText(it, metadataColor)
}
item.genres.firstOrNull()?.takeIf { it.isNotBlank() }?.let {
Icon(Icons.Default.LocalOffer, contentDescription = "Genre", tint = metadataColor, modifier = Modifier.size(18.dp))
Icon(MembyIcon.Tag.mark, contentDescription = "Genre", tint = metadataColor, modifier = Modifier.size(18.dp))
MetadataText(it, metadataColor)
}
}
@@ -2,6 +2,8 @@
package com.ponzischeme89.memby.ui.search
import com.ponzischeme89.memby.ui.theme.MembyIcon
import com.ponzischeme89.memby.ui.theme.mark
import android.Manifest
import android.app.Activity
import android.content.Intent
@@ -41,21 +43,6 @@ import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.itemsIndexed as rowItemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Backspace
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.AutoAwesome
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.LiveTv
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.Movie
import androidx.compose.material.icons.filled.PlayCircleFilled
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.SentimentVerySatisfied
import androidx.compose.material.icons.filled.SpaceBar
import androidx.compose.material.icons.filled.TheaterComedy
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -461,7 +448,7 @@ internal fun SearchQueryField(
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
Icons.Default.Search,
MembyIcon.Search.mark,
contentDescription = null,
tint = if (query.isEmpty()) Muted else Accent,
modifier = Modifier.size(18.dp),
@@ -505,7 +492,7 @@ internal fun SearchQueryField(
modifier = micModifier.clip(RoundedCornerShape(8.dp)),
) { focused ->
Icon(
Icons.Default.Mic,
MembyIcon.Mic.mark,
contentDescription = null,
tint = if (focused) KeyLabelFocused else KeyLabel,
modifier = Modifier
@@ -608,7 +595,7 @@ internal fun TvKeyboard(
Spacer(Modifier.height(2.dp))
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
ActionKey(
icon = Icons.Default.SpaceBar,
icon = MembyIcon.Space.mark,
label = "Space",
contentDescription = "Insert a space",
onClick = { onCharacter(" ") },
@@ -626,7 +613,7 @@ internal fun TvKeyboard(
),
)
ActionKey(
icon = Icons.AutoMirrored.Filled.Backspace,
icon = MembyIcon.Backspace.mark,
label = "Delete",
contentDescription = "Delete the last character",
onClick = onBackspace,
@@ -643,7 +630,7 @@ internal fun TvKeyboard(
),
)
ActionKey(
icon = Icons.Default.Close,
icon = MembyIcon.Close.mark,
label = "Clear",
contentDescription = "Clear the whole query",
onClick = onClear,
@@ -671,7 +658,7 @@ internal fun TvKeyboard(
if (onSearch != null) {
Spacer(Modifier.height(if (compact) 4.dp else 6.dp))
ActionKey(
icon = Icons.Default.Search,
icon = MembyIcon.Search.mark,
label = "Search",
contentDescription = "Search for what you have typed",
onClick = onSearch,
@@ -961,7 +948,7 @@ private fun ResultsHeading(
contentAlignment = Alignment.Center,
) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
MembyIcon.ArrowBack.mark,
contentDescription = null,
tint = if (focused) KeyLabelFocused else Heading,
modifier = Modifier.size(21.dp),
@@ -1145,7 +1132,7 @@ private fun RequestOptions(
.background(Accent.copy(alpha = 0.16f)),
contentAlignment = Alignment.Center,
) {
Icon(Icons.Default.Add, contentDescription = null, tint = Accent, modifier = Modifier.size(24.dp))
Icon(MembyIcon.Add.mark, contentDescription = null, tint = Accent, modifier = Modifier.size(24.dp))
}
Spacer(Modifier.width(13.dp))
Column(Modifier.weight(1f)) {
@@ -1207,7 +1194,7 @@ private fun RequestOptions(
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
if (state.requestMessageIsError) Icons.Default.Close else Icons.Default.CheckCircle,
if (state.requestMessageIsError) MembyIcon.Close.mark else MembyIcon.CheckCircle.mark,
contentDescription = null,
tint = statusColor,
modifier = Modifier.size(18.dp),
@@ -1288,7 +1275,7 @@ private fun RequestCandidateCard(
)
} else {
Icon(
if (mediaLabel == "MOVIE") Icons.Default.Movie else Icons.Default.LiveTv,
if (mediaLabel == "MOVIE") MembyIcon.Movie.mark else MembyIcon.LiveTv.mark,
contentDescription = null,
tint = Muted.copy(alpha = 0.65f),
modifier = Modifier.size(30.dp),
@@ -1323,7 +1310,7 @@ private fun RequestCandidateCard(
Spacer(Modifier.weight(1f))
Row(verticalAlignment = Alignment.CenterVertically) {
if (candidate.inLibrary || candidate.alreadyAdded) {
Icon(Icons.Default.CheckCircle, contentDescription = null, tint = Accent, modifier = Modifier.size(14.dp))
Icon(MembyIcon.CheckCircle.mark, contentDescription = null, tint = Accent, modifier = Modifier.size(14.dp))
Spacer(Modifier.width(5.dp))
}
Text(
@@ -1394,12 +1381,12 @@ private val GenreColors = listOf(
)
private fun genreIcon(label: String): ImageVector = when {
label.contains("comedy", true) -> Icons.Default.TheaterComedy
label.contains("music", true) -> Icons.Default.LiveTv
label.contains("children", true) || label.contains("family", true) -> Icons.Default.SentimentVerySatisfied
label.contains("sport", true) -> Icons.Default.PlayCircleFilled
label.contains("document", true) -> Icons.Default.Movie
else -> Icons.Default.AutoAwesome
label.contains("comedy", true) -> MembyIcon.Drama.mark
label.contains("music", true) -> MembyIcon.LiveTv.mark
label.contains("children", true) || label.contains("family", true) -> MembyIcon.Happy.mark
label.contains("sport", true) -> MembyIcon.PlayCircle.mark
label.contains("document", true) -> MembyIcon.Movie.mark
else -> MembyIcon.Sparkle.mark
}
@Composable
@@ -8,6 +8,8 @@
package com.ponzischeme89.memby.ui.settings
import com.ponzischeme89.memby.ui.theme.MembyIcon
import com.ponzischeme89.memby.ui.theme.mark
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
@@ -38,13 +40,6 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Devices
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Storage
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -187,20 +182,20 @@ private const val THEME_PICKER_ENABLED = false
internal enum class SettingsPage(
val label: String,
val description: String,
val icon: ImageVector,
val icon: MembyIcon,
val showInRail: Boolean = true,
) {
APPEARANCE("Appearance", "How Memby looks", Icons.Default.Palette),
PLAYBACK("Playback", "What happens while you watch", Icons.Default.PlayArrow),
HOME("Home screen", "What you see when Memby opens", Icons.Default.Home),
APPEARANCE("Appearance", "How Memby looks", MembyIcon.Palette),
PLAYBACK("Playback", "What happens while you watch", MembyIcon.Play),
HOME("Home screen", "What you see when Memby opens", MembyIcon.Home),
// No Updates page. The manual check needs a Gitea address, a repository and a token,
// and nothing on this television can enter them — so the button could only ever report
// a failure, on the one screen a viewer goes to when they suspect something is wrong.
// Updates arrive through the gateway's own verdict (ui/UpdateScreen.kt), which carries
// its download URL with it.
DEVICES("Devices", "TVs signed in to your account", Icons.Default.Devices),
STORAGE("Storage", "Artwork Memby keeps on this TV", Icons.Default.Storage),
ABOUT("About", "Version and release notes", Icons.Default.Info),
DEVICES("Devices", "TVs signed in to your account", MembyIcon.Devices),
STORAGE("Storage", "Artwork Memby keeps on this TV", MembyIcon.Storage),
ABOUT("About", "Version and release notes", MembyIcon.Info),
}
// Black, and one lit thing at a time.
@@ -1518,7 +1513,7 @@ private fun SettingsSecondaryRail(
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
Icon(
page.icon,
page.icon.mark,
contentDescription = null,
tint = when {
focused -> Canvas
@@ -0,0 +1,144 @@
package com.ponzischeme89.memby.ui.theme
import com.composables.icons.fontawesome.FontAwesome
import com.composables.icons.fontawesome.solid.AngleDoubleLeft
import com.composables.icons.fontawesome.solid.ArrowDown
import com.composables.icons.fontawesome.solid.ArrowLeft
import com.composables.icons.fontawesome.solid.ArrowRight
import com.composables.icons.fontawesome.solid.ArrowUp
import com.composables.icons.fontawesome.solid.Backspace
import com.composables.icons.fontawesome.solid.Bell
import com.composables.icons.fontawesome.solid.BellSlash
import com.composables.icons.fontawesome.solid.Bolt
import com.composables.icons.fontawesome.solid.Bookmark
import com.composables.icons.fontawesome.solid.BroadcastTower
import com.composables.icons.fontawesome.solid.Building
import com.composables.icons.fontawesome.solid.CalendarAlt
import com.composables.icons.fontawesome.solid.CalendarDay
import com.composables.icons.fontawesome.solid.Check
import com.composables.icons.fontawesome.solid.CheckCircle
import com.composables.icons.fontawesome.solid.CheckDouble
import com.composables.icons.fontawesome.solid.ChevronDown
import com.composables.icons.fontawesome.solid.ChevronLeft
import com.composables.icons.fontawesome.solid.ChevronRight
import com.composables.icons.fontawesome.solid.Clock
import com.composables.icons.fontawesome.solid.Cog
import com.composables.icons.fontawesome.solid.Desktop
import com.composables.icons.fontawesome.solid.EyeSlash
import com.composables.icons.fontawesome.solid.Film
import com.composables.icons.fontawesome.solid.Fire
import com.composables.icons.fontawesome.solid.Futbol
import com.composables.icons.fontawesome.solid.Gavel
import com.composables.icons.fontawesome.solid.Hdd
import com.composables.icons.fontawesome.solid.Heart
import com.composables.icons.fontawesome.solid.Home
import com.composables.icons.fontawesome.solid.Image
import com.composables.icons.fontawesome.solid.Inbox
import com.composables.icons.fontawesome.solid.InfoCircle
import com.composables.icons.fontawesome.solid.Magic
import com.composables.icons.fontawesome.solid.Medal
import com.composables.icons.fontawesome.solid.Microphone
import com.composables.icons.fontawesome.solid.MinusCircle
import com.composables.icons.fontawesome.solid.Moon
import com.composables.icons.fontawesome.solid.Mountain
import com.composables.icons.fontawesome.solid.Music
import com.composables.icons.fontawesome.solid.Palette
import com.composables.icons.fontawesome.solid.PhotoVideo
import com.composables.icons.fontawesome.solid.Play
import com.composables.icons.fontawesome.solid.PlayCircle
import com.composables.icons.fontawesome.solid.Plus
import com.composables.icons.fontawesome.solid.PlusCircle
import com.composables.icons.fontawesome.solid.PowerOff
import com.composables.icons.fontawesome.solid.QuestionCircle
import com.composables.icons.fontawesome.solid.Search
import com.composables.icons.fontawesome.solid.Shapes
import com.composables.icons.fontawesome.solid.Smile
import com.composables.icons.fontawesome.solid.Sun
import com.composables.icons.fontawesome.solid.Tag
import com.composables.icons.fontawesome.solid.ThLarge
import com.composables.icons.fontawesome.solid.TheaterMasks
import com.composables.icons.fontawesome.solid.ThumbsUp
import com.composables.icons.fontawesome.solid.Thumbtack
import com.composables.icons.fontawesome.solid.Times
import com.composables.icons.fontawesome.solid.Tv
import com.composables.icons.fontawesome.solid.User
import com.composables.icons.fontawesome.solid.Wrench
/**
* Font Awesome Solid filled throughout, and the pack to reach for on a set watched from
* the sofa. Stroke sets are drawn for 1624px on a monitor; at three metres on a 38dp rail
* chip they read thin and washed, where a solid mark keeps its shape.
*
* The outline halves are the ones missing here, the mirror image of Lucide's omission and
* for the same reason.
*/
internal val fontAwesomeIconPack = MembyIconPack(
MEMBY_ICON_PACK_FONT_AWESOME,
mapOf(
MembyIcon.Home to { FontAwesome.Solid.Home },
MembyIcon.Search to { FontAwesome.Solid.Search },
MembyIcon.Grid to { FontAwesome.Solid.ThLarge },
MembyIcon.Movie to { FontAwesome.Solid.Film },
MembyIcon.Tv to { FontAwesome.Solid.Tv },
MembyIcon.LiveTv to { FontAwesome.Solid.BroadcastTower },
MembyIcon.VideoLibrary to { FontAwesome.Solid.PhotoVideo },
MembyIcon.Person to { FontAwesome.Solid.User },
MembyIcon.Settings to { FontAwesome.Solid.Cog },
MembyIcon.Calendar to { FontAwesome.Solid.CalendarAlt },
MembyIcon.Event to { FontAwesome.Solid.CalendarDay },
MembyIcon.Play to { FontAwesome.Solid.Play },
MembyIcon.PlayCircle to { FontAwesome.Solid.PlayCircle },
MembyIcon.Favourite to { FontAwesome.Solid.Heart },
MembyIcon.Bookmark to { FontAwesome.Solid.Bookmark },
MembyIcon.PlaylistAdd to { FontAwesome.Solid.PlusCircle },
MembyIcon.PlaylistRemove to { FontAwesome.Solid.MinusCircle },
MembyIcon.HideWatched to { FontAwesome.Solid.EyeSlash },
MembyIcon.Check to { FontAwesome.Solid.Check },
MembyIcon.CheckCircle to { FontAwesome.Solid.CheckCircle },
MembyIcon.CheckAll to { FontAwesome.Solid.CheckDouble },
MembyIcon.Add to { FontAwesome.Solid.Plus },
MembyIcon.Close to { FontAwesome.Solid.Times },
MembyIcon.ChevronLeft to { FontAwesome.Solid.ChevronLeft },
MembyIcon.ChevronRight to { FontAwesome.Solid.ChevronRight },
MembyIcon.ChevronDown to { FontAwesome.Solid.ChevronDown },
MembyIcon.ArrowBack to { FontAwesome.Solid.ArrowLeft },
MembyIcon.ArrowForward to { FontAwesome.Solid.ArrowRight },
MembyIcon.ArrowUp to { FontAwesome.Solid.ArrowUp },
MembyIcon.ArrowDown to { FontAwesome.Solid.ArrowDown },
MembyIcon.FirstPage to { FontAwesome.Solid.AngleDoubleLeft },
MembyIcon.Backspace to { FontAwesome.Solid.Backspace },
MembyIcon.Sparkle to { FontAwesome.Solid.Magic },
MembyIcon.Recommend to { FontAwesome.Solid.ThumbsUp },
MembyIcon.Drama to { FontAwesome.Solid.TheaterMasks },
MembyIcon.Happy to { FontAwesome.Solid.Smile },
MembyIcon.Music to { FontAwesome.Solid.Music },
MembyIcon.Football to { FontAwesome.Solid.Futbol },
MembyIcon.Trophy to { FontAwesome.Solid.Medal },
MembyIcon.Fire to { FontAwesome.Solid.Fire },
MembyIcon.Tag to { FontAwesome.Solid.Tag },
MembyIcon.Category to { FontAwesome.Solid.Shapes },
MembyIcon.Studio to { FontAwesome.Solid.Building },
MembyIcon.Landscape to { FontAwesome.Solid.Mountain },
MembyIcon.Palette to { FontAwesome.Solid.Palette },
MembyIcon.Notification to { FontAwesome.Solid.Bell },
MembyIcon.NotificationActive to { FontAwesome.Solid.Bell },
MembyIcon.NotificationOff to { FontAwesome.Solid.BellSlash },
MembyIcon.Inbox to { FontAwesome.Solid.Inbox },
MembyIcon.Pin to { FontAwesome.Solid.Thumbtack },
MembyIcon.Bolt to { FontAwesome.Solid.Bolt },
MembyIcon.Clock to { FontAwesome.Solid.Clock },
MembyIcon.Schedule to { FontAwesome.Solid.Clock },
MembyIcon.Sun to { FontAwesome.Solid.Sun },
MembyIcon.Sunrise to { FontAwesome.Solid.Sun },
MembyIcon.Night to { FontAwesome.Solid.Moon },
MembyIcon.Info to { FontAwesome.Solid.InfoCircle },
MembyIcon.Help to { FontAwesome.Solid.QuestionCircle },
MembyIcon.BrokenImage to { FontAwesome.Solid.Image },
MembyIcon.Devices to { FontAwesome.Solid.Desktop },
MembyIcon.Storage to { FontAwesome.Solid.Hdd },
MembyIcon.Build to { FontAwesome.Solid.Wrench },
MembyIcon.Power to { FontAwesome.Solid.PowerOff },
MembyIcon.Gavel to { FontAwesome.Solid.Gavel },
MembyIcon.Mic to { FontAwesome.Solid.Microphone },
),
)
@@ -0,0 +1,146 @@
package com.ponzischeme89.memby.ui.theme
import com.composables.icons.lucide.ArrowDown
import com.composables.icons.lucide.ArrowLeft
import com.composables.icons.lucide.ArrowRight
import com.composables.icons.lucide.ArrowUp
import com.composables.icons.lucide.Zap
import com.composables.icons.lucide.Bell
import com.composables.icons.lucide.BellOff
import com.composables.icons.lucide.BellRing
import com.composables.icons.lucide.Bookmark
import com.composables.icons.lucide.Building2
import com.composables.icons.lucide.CalendarClock
import com.composables.icons.lucide.CalendarDays
import com.composables.icons.lucide.Check
import com.composables.icons.lucide.ChevronDown
import com.composables.icons.lucide.ChevronLeft
import com.composables.icons.lucide.ChevronRight
import com.composables.icons.lucide.ChevronsLeft
import com.composables.icons.lucide.CirclePlay
import com.composables.icons.lucide.CircleQuestionMark
import com.composables.icons.lucide.Clock
import com.composables.icons.lucide.Delete
import com.composables.icons.lucide.Drama
import com.composables.icons.lucide.EyeOff
import com.composables.icons.lucide.Film
import com.composables.icons.lucide.Flame
import com.composables.icons.lucide.Gavel
import com.composables.icons.lucide.HardDrive
import com.composables.icons.lucide.Heart
import com.composables.icons.lucide.House
import com.composables.icons.lucide.ImageOff
import com.composables.icons.lucide.Inbox
import com.composables.icons.lucide.Info
import com.composables.icons.lucide.LayoutGrid
import com.composables.icons.lucide.LibraryBig
import com.composables.icons.lucide.ListMinus
import com.composables.icons.lucide.ListPlus
import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.Mic
import com.composables.icons.lucide.MonitorSpeaker
import com.composables.icons.lucide.MoonStar
import com.composables.icons.lucide.MountainSnow
import com.composables.icons.lucide.Music2
import com.composables.icons.lucide.Palette
import com.composables.icons.lucide.Pin
import com.composables.icons.lucide.Play
import com.composables.icons.lucide.Plus
import com.composables.icons.lucide.Power
import com.composables.icons.lucide.Radio
import com.composables.icons.lucide.Search
import com.composables.icons.lucide.Settings
import com.composables.icons.lucide.Shapes
import com.composables.icons.lucide.Smile
import com.composables.icons.lucide.Space
import com.composables.icons.lucide.Sparkles
import com.composables.icons.lucide.Sun
import com.composables.icons.lucide.Sunrise
import com.composables.icons.lucide.Tag
import com.composables.icons.lucide.ThumbsUp
import com.composables.icons.lucide.Trophy
import com.composables.icons.lucide.Tv
import com.composables.icons.lucide.User
import com.composables.icons.lucide.Volleyball
import com.composables.icons.lucide.Wrench
import com.composables.icons.lucide.X
/**
* Lucide one weight, drawn as strokes.
*
* The filled halves of the state-bearing pairs are deliberately **not** mapped. Lucide is a
* stroke set with no filled heart or bookmark, so mapping [MembyIcon.Favourite] to the same
* glyph as [MembyIcon.FavouriteOutline] would make "this is a favourite" and "this is not"
* identical on screen a pack cannot be allowed to cost the app a distinction. They fall
* back to Material's filled marks, which is a small mixture and the honest one.
*/
internal val lucideIconPack = MembyIconPack(
MEMBY_ICON_PACK_LUCIDE,
mapOf(
MembyIcon.Home to { Lucide.House },
MembyIcon.Search to { Lucide.Search },
MembyIcon.Grid to { Lucide.LayoutGrid },
MembyIcon.Movie to { Lucide.Film },
MembyIcon.Tv to { Lucide.Tv },
MembyIcon.LiveTv to { Lucide.Radio },
MembyIcon.VideoLibrary to { Lucide.LibraryBig },
MembyIcon.Person to { Lucide.User },
MembyIcon.Settings to { Lucide.Settings },
MembyIcon.Calendar to { Lucide.CalendarDays },
MembyIcon.Event to { Lucide.CalendarClock },
MembyIcon.Play to { Lucide.Play },
MembyIcon.PlayCircle to { Lucide.CirclePlay },
MembyIcon.FavouriteOutline to { Lucide.Heart },
MembyIcon.BookmarkOutline to { Lucide.Bookmark },
MembyIcon.PlaylistAdd to { Lucide.ListPlus },
MembyIcon.PlaylistRemove to { Lucide.ListMinus },
MembyIcon.HideWatched to { Lucide.EyeOff },
MembyIcon.Check to { Lucide.Check },
MembyIcon.Add to { Lucide.Plus },
MembyIcon.Close to { Lucide.X },
MembyIcon.ChevronLeft to { Lucide.ChevronLeft },
MembyIcon.ChevronRight to { Lucide.ChevronRight },
MembyIcon.ChevronDown to { Lucide.ChevronDown },
MembyIcon.ArrowBack to { Lucide.ArrowLeft },
MembyIcon.ArrowForward to { Lucide.ArrowRight },
MembyIcon.ArrowUp to { Lucide.ArrowUp },
MembyIcon.ArrowDown to { Lucide.ArrowDown },
MembyIcon.FirstPage to { Lucide.ChevronsLeft },
MembyIcon.Backspace to { Lucide.Delete },
MembyIcon.Space to { Lucide.Space },
MembyIcon.Sparkle to { Lucide.Sparkles },
MembyIcon.Recommend to { Lucide.ThumbsUp },
MembyIcon.Drama to { Lucide.Drama },
MembyIcon.Happy to { Lucide.Smile },
MembyIcon.Music to { Lucide.Music2 },
MembyIcon.Football to { Lucide.Volleyball },
MembyIcon.Trophy to { Lucide.Trophy },
MembyIcon.Fire to { Lucide.Flame },
MembyIcon.Tag to { Lucide.Tag },
MembyIcon.Category to { Lucide.Shapes },
MembyIcon.Studio to { Lucide.Building2 },
MembyIcon.Landscape to { Lucide.MountainSnow },
MembyIcon.Palette to { Lucide.Palette },
MembyIcon.Notification to { Lucide.Bell },
MembyIcon.NotificationActive to { Lucide.BellRing },
MembyIcon.NotificationNone to { Lucide.Bell },
MembyIcon.NotificationOff to { Lucide.BellOff },
MembyIcon.Inbox to { Lucide.Inbox },
MembyIcon.Pin to { Lucide.Pin },
MembyIcon.Bolt to { Lucide.Zap },
MembyIcon.Clock to { Lucide.Clock },
MembyIcon.Schedule to { Lucide.Clock },
MembyIcon.Sun to { Lucide.Sun },
MembyIcon.Sunrise to { Lucide.Sunrise },
MembyIcon.Night to { Lucide.MoonStar },
MembyIcon.Info to { Lucide.Info },
MembyIcon.Help to { Lucide.CircleQuestionMark },
MembyIcon.BrokenImage to { Lucide.ImageOff },
MembyIcon.Devices to { Lucide.MonitorSpeaker },
MembyIcon.Storage to { Lucide.HardDrive },
MembyIcon.Build to { Lucide.Wrench },
MembyIcon.Power to { Lucide.Power },
MembyIcon.Gavel to { Lucide.Gavel },
MembyIcon.Mic to { Lucide.Mic },
),
)
@@ -0,0 +1,202 @@
package com.ponzischeme89.memby.ui.theme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.automirrored.filled.Backspace
import androidx.compose.material.icons.automirrored.filled.HelpOutline
import androidx.compose.material.icons.filled.AccessTime
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material.icons.filled.AutoAwesome
import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.filled.Bookmark
import androidx.compose.material.icons.filled.BookmarkBorder
import androidx.compose.material.icons.filled.BrokenImage
import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.filled.Business
import androidx.compose.material.icons.filled.CalendarMonth
import androidx.compose.material.icons.filled.Category
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.ChevronLeft
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Devices
import androidx.compose.material.icons.filled.DoneAll
import androidx.compose.material.icons.filled.Event
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.FirstPage
import androidx.compose.material.icons.filled.Gavel
import androidx.compose.material.icons.filled.GridView
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Inbox
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.Landscape
import androidx.compose.material.icons.filled.LightMode
import androidx.compose.material.icons.filled.LiveTv
import androidx.compose.material.icons.filled.LocalFireDepartment
import androidx.compose.material.icons.filled.LocalOffer
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.MilitaryTech
import androidx.compose.material.icons.filled.Movie
import androidx.compose.material.icons.filled.MusicNote
import androidx.compose.material.icons.filled.NightsStay
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.NotificationsActive
import androidx.compose.material.icons.filled.NotificationsNone
import androidx.compose.material.icons.filled.NotificationsOff
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.PlayCircleFilled
import androidx.compose.material.icons.filled.PlaylistAdd
import androidx.compose.material.icons.filled.PlaylistRemove
import androidx.compose.material.icons.filled.PowerSettingsNew
import androidx.compose.material.icons.filled.PushPin
import androidx.compose.material.icons.filled.Recommend
import androidx.compose.material.icons.filled.Schedule
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.SentimentVerySatisfied
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.SpaceBar
import androidx.compose.material.icons.filled.SportsSoccer
import androidx.compose.material.icons.filled.Storage
import androidx.compose.material.icons.filled.TheaterComedy
import androidx.compose.material.icons.filled.Tv
import androidx.compose.material.icons.filled.VideoLibrary
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.filled.WbSunny
import androidx.compose.ui.graphics.vector.ImageVector
/**
* The packs a theme may name, and the marks each of them draws.
*
* Adding a pack is one entry in [membyIconPacks] and one map here no call site changes,
* because every screen names a [MembyIcon] and nothing names a pack. That is the property
* the whole feature is for: an operator moves a household onto a different set of marks by
* editing the gateway's theme catalogue, and no television is touched.
*
* Only the ~70 slots named below survive R8 out of packs holding a thousand icons each, so
* this file is also the size bound: a slot costs three vectors, and a slot nothing draws
* costs three vectors for nothing.
*/
object MaterialIconPack {
/**
* Every slot, because this is what a partial pack falls back to. A slot added to
* [MembyIcon] and not to this map would draw nothing at all which is why
* `MembyIconPackTest` asserts the coverage rather than leaving it to a code review.
*/
private val marks: Map<MembyIcon, () -> ImageVector> = mapOf(
MembyIcon.Home to { Icons.Default.Home },
MembyIcon.Search to { Icons.Default.Search },
MembyIcon.Grid to { Icons.Default.GridView },
MembyIcon.Movie to { Icons.Default.Movie },
MembyIcon.Tv to { Icons.Default.Tv },
MembyIcon.LiveTv to { Icons.Default.LiveTv },
MembyIcon.VideoLibrary to { Icons.Default.VideoLibrary },
MembyIcon.Person to { Icons.Default.Person },
MembyIcon.Settings to { Icons.Default.Settings },
MembyIcon.Calendar to { Icons.Default.CalendarMonth },
MembyIcon.Event to { Icons.Default.Event },
MembyIcon.Play to { Icons.Default.PlayArrow },
MembyIcon.PlayCircle to { Icons.Default.PlayCircleFilled },
MembyIcon.Favourite to { Icons.Default.Favorite },
MembyIcon.FavouriteOutline to { Icons.Default.FavoriteBorder },
MembyIcon.Bookmark to { Icons.Default.Bookmark },
MembyIcon.BookmarkOutline to { Icons.Default.BookmarkBorder },
MembyIcon.PlaylistAdd to { Icons.Default.PlaylistAdd },
MembyIcon.PlaylistRemove to { Icons.Default.PlaylistRemove },
MembyIcon.HideWatched to { Icons.Default.VisibilityOff },
MembyIcon.Check to { Icons.Default.Check },
MembyIcon.CheckCircle to { Icons.Default.CheckCircle },
MembyIcon.CheckAll to { Icons.Default.DoneAll },
MembyIcon.Add to { Icons.Default.Add },
MembyIcon.Close to { Icons.Default.Close },
MembyIcon.ChevronLeft to { Icons.Default.ChevronLeft },
MembyIcon.ChevronRight to { Icons.Default.ChevronRight },
MembyIcon.ChevronDown to { Icons.Default.KeyboardArrowDown },
MembyIcon.ArrowBack to { Icons.AutoMirrored.Filled.ArrowBack },
MembyIcon.ArrowForward to { Icons.AutoMirrored.Filled.ArrowForward },
MembyIcon.ArrowUp to { Icons.Default.ArrowUpward },
MembyIcon.ArrowDown to { Icons.Default.ArrowDownward },
MembyIcon.FirstPage to { Icons.Default.FirstPage },
MembyIcon.Backspace to { Icons.AutoMirrored.Filled.Backspace },
MembyIcon.Space to { Icons.Default.SpaceBar },
MembyIcon.Sparkle to { Icons.Default.AutoAwesome },
MembyIcon.Recommend to { Icons.Default.Recommend },
MembyIcon.Drama to { Icons.Default.TheaterComedy },
MembyIcon.Happy to { Icons.Default.SentimentVerySatisfied },
MembyIcon.Music to { Icons.Default.MusicNote },
MembyIcon.Football to { Icons.Default.SportsSoccer },
MembyIcon.Trophy to { Icons.Default.MilitaryTech },
MembyIcon.Fire to { Icons.Default.LocalFireDepartment },
MembyIcon.Tag to { Icons.Default.LocalOffer },
MembyIcon.Category to { Icons.Default.Category },
MembyIcon.Studio to { Icons.Default.Business },
MembyIcon.Landscape to { Icons.Default.Landscape },
MembyIcon.Palette to { Icons.Default.Palette },
MembyIcon.Notification to { Icons.Default.Notifications },
MembyIcon.NotificationActive to { Icons.Default.NotificationsActive },
MembyIcon.NotificationNone to { Icons.Default.NotificationsNone },
MembyIcon.NotificationOff to { Icons.Default.NotificationsOff },
MembyIcon.Inbox to { Icons.Default.Inbox },
MembyIcon.Pin to { Icons.Default.PushPin },
MembyIcon.Bolt to { Icons.Default.Bolt },
MembyIcon.Clock to { Icons.Default.AccessTime },
MembyIcon.Schedule to { Icons.Default.Schedule },
MembyIcon.Sun to { Icons.Default.WbSunny },
MembyIcon.Sunrise to { Icons.Default.LightMode },
MembyIcon.Night to { Icons.Default.NightsStay },
MembyIcon.Info to { Icons.Default.Info },
MembyIcon.Help to { Icons.AutoMirrored.Filled.HelpOutline },
MembyIcon.BrokenImage to { Icons.Default.BrokenImage },
MembyIcon.Devices to { Icons.Default.Devices },
MembyIcon.Storage to { Icons.Default.Storage },
MembyIcon.Build to { Icons.Default.Build },
MembyIcon.Power to { Icons.Default.PowerSettingsNew },
MembyIcon.Gavel to { Icons.Default.Gavel },
MembyIcon.Mic to { Icons.Default.Mic },
)
/** The marks the app shipped with, and the floor every other pack stands on. */
val pack = MembyIconPack(MEMBY_ICON_PACK_MATERIAL, marks)
internal fun fallback(slot: MembyIcon): ImageVector =
marks.getValue(slot).invoke()
}
const val MEMBY_ICON_PACK_MATERIAL = "material"
const val MEMBY_ICON_PACK_LUCIDE = "lucide"
const val MEMBY_ICON_PACK_FONT_AWESOME = "fontawesome"
/**
* Every pack this build can draw, by the slug the gateway names it with.
*
* The list is the client's whole side of the contract. A gateway naming a pack this
* television has never heard of is an ordinary event an operator has added one and this
* set has not been updated so [membyIconPackFor] answers with the marks the app shipped
* with rather than with nothing. That is the same stance an unknown decoration slug and an
* unparseable hex already take: the worst a theme does is look unchanged.
*/
private val membyIconPacks: Map<String, MembyIconPack> = listOf(
MaterialIconPack.pack,
lucideIconPack,
fontAwesomeIconPack,
).associateBy { it.id }
/**
* The pack for a slug, or the app's own marks.
*
* Pure, and tested the slug arrives off a wire, and a blank one (a gateway that predates
* this, a theme that expresses no opinion) has to mean "leave the marks alone" rather than
* "draw nothing".
*/
fun membyIconPackFor(slug: String?): MembyIconPack =
membyIconPacks[slug?.trim()?.lowercase().orEmpty()] ?: MaterialIconPack.pack
/** The slugs this build understands. Read by the tests and by Settings → About. */
val membyIconPackIds: List<String> get() = membyIconPacks.keys.toList()
@@ -0,0 +1,165 @@
package com.ponzischeme89.memby.ui.theme
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.graphics.vector.ImageVector
/**
* The marks this app draws, named by what they mean rather than by who drew them.
*
* This is `DesignTokens.kt` for icons, and it exists for the same reason. A colour scheme
* could not reach the launcher while a hundred literal hexes were written at the call
* sites, and an icon pack could not reach it while seventy `Icons.Default.*` were: the
* gateway can only change what the television has a *slot* for. Naming a slot after the
* Material identifier it happens to hold today (`AutoAwesome`, `SentimentVerySatisfied`)
* would put that back a pack whose recommendation mark is a wand rather than four stars
* would be filed under a name that lies about it. So the slots are named for the job:
* [Sparkle], [Happy], [Drama].
*
* What a slot is *worth* is the other half of the rule. A slot is added when a screen needs
* a mark, never so that a pack can show off a glyph the gateway names a pack and nothing
* else, so an unused slot is dead weight in three maps at once.
*/
enum class MembyIcon {
// Navigation and destinations
Home,
Search,
Grid,
Movie,
Tv,
LiveTv,
VideoLibrary,
Person,
Settings,
Calendar,
Event,
// Playback and library actions
Play,
PlayCircle,
Favourite,
FavouriteOutline,
Bookmark,
BookmarkOutline,
PlaylistAdd,
PlaylistRemove,
HideWatched,
// Affirmation
Check,
CheckCircle,
CheckAll,
Add,
Close,
// Movement
ChevronLeft,
ChevronRight,
ChevronDown,
ArrowBack,
ArrowForward,
ArrowUp,
ArrowDown,
FirstPage,
Backspace,
Space,
// Row and genre character
Sparkle,
Recommend,
Drama,
Happy,
Music,
Football,
Trophy,
Fire,
Tag,
Category,
Studio,
Landscape,
Palette,
// News
Notification,
NotificationActive,
NotificationNone,
NotificationOff,
Inbox,
Pin,
Bolt,
// Time of day and time itself
Clock,
Schedule,
Sun,
Sunrise,
Night,
// Status and diagnosis
Info,
Help,
BrokenImage,
Devices,
Storage,
Build,
Power,
Gavel,
Mic,
}
/**
* One pack's answer for every slot it has a mark for.
*
* The marks are **lambdas, not vectors**, because an `ImageVector` is built the first time
* it is read and a map of them would build all seventy on the first frame that touched the
* pack on the cold start, which is the one thing in this app nothing is allowed to cost.
* Held this way a pack costs one map of function references, and a mark is built when a
* screen actually draws it.
*
* A pack may be **partial**, and that is deliberate rather than tolerated. Solid sets have
* no honest filled form of a chevron or a tick, and a pack forced to name one would either
* block the pack from ever being offered or put a poor mark on the rail. An absent slot
* falls back to [MaterialIconPack], so the worst a pack does is look unchanged in places
* the same stance [parseThemeColor] takes on a hex string it cannot read.
*/
@Stable
class MembyIconPack(
/** The slug the gateway names this pack by. */
val id: String,
private val marks: Map<MembyIcon, () -> ImageVector>,
) {
/** Which slots this pack draws itself. Read by the tests that pin pack coverage. */
val slots: Set<MembyIcon> get() = marks.keys
internal fun markFor(slot: MembyIcon): ImageVector =
marks[slot]?.invoke() ?: MaterialIconPack.fallback(slot)
}
private val currentIconPack = mutableStateOf(MaterialIconPack.pack)
/**
* Repaints every mark in the app.
*
* The state is process-wide and read through [mark] below, so this is the icon half of
* [applyMembyPalette] and arrives by the same road: a theme revision the television does
* not hold, fetched by `ThemeSync`.
*/
fun applyMembyIconPack(pack: MembyIconPack) {
if (currentIconPack.value.id != pack.id) currentIconPack.value = pack
}
/** The pack in force. Exposed for the settings screen's own description of it. */
val membyIconPackId: String get() = currentIconPack.value.id
/**
* The mark to draw for this slot, under whatever pack the gateway last resolved.
*
* Reading this in a composable is what subscribes that composable to a pack change, which
* is the whole delivery mechanism and the reason nothing may hold the result. A mark
* captured in an `enum` constant or a `val` freezes the pack that was in force when its
* class initialised, which is exactly the `val`-versus-`get()` trap that made
* `SettingsSheet` the one screen a palette could never reach. Types that carry a mark
* around (a rail destination, a row's visual) carry the [MembyIcon] and resolve it where
* they draw.
*/
val MembyIcon.mark: ImageVector get() = currentIconPack.value.markFor(this)
@@ -0,0 +1,152 @@
package com.ponzischeme89.memby.ui.theme
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.tv.material3.Icon
import androidx.tv.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.test.core.app.ApplicationProvider
import com.github.takahirom.roborazzi.captureRoboImage
import com.ponzischeme89.memby.ServiceLocator
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode
/**
* Every mark of every pack, to `build/screenshots/icon-packs/`.
*
* ```powershell
* .\gradlew.bat :app:testDebugUnitTest --tests "*IconPackScreenshotTest"
* ```
*
* This is the only test that can judge the feature, for the reason `ThemeScreenshotTest`
* gives about palettes: a unit test can check that a pack names a mark for a slot, and it
* cannot check whether that mark *means* the slot. Nothing would have caught Lucide's
* volleyball standing in for football, or a stroke set going invisible at rail size, except
* looking and a pack nobody has looked at is a pack pushed to a household on faith.
*
* Both sizes are captured on purpose and the small one is the point. 21dp is what the
* navigation rail actually draws, and a stroke set has least to spare there every pack
* looks fine in the large grid, so that is not where a thin one would be caught. The strip
* along the bottom is the same nine marks in the accent, because a rail is read as a column
* of coloured glyphs rather than as isolated shapes on black.
*/
@RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(sdk = [34], qualifiers = "w960dp-h540dp-television-xhdpi")
class IconPackScreenshotTest {
@get:Rule
val compose = createComposeRule()
@Before
fun locator() {
ServiceLocator.init(ApplicationProvider.getApplicationContext())
}
/**
* The pack is process-wide state the whole reason one answer from the gateway
* repaints every marked thing in the app so a test that left one applied would draw
* the wrong marks into every screenshot taken after it in the same JVM.
*/
@After
fun unpack() {
applyMembyIconPack(MaterialIconPack.pack)
}
@Test
fun `every pack, at reading size`() {
sweep("labelled") { Sheet(size = 28.dp, labelled = true) }
}
@Test
fun `every pack, at the size the rail draws them`() {
sweep("rail-size") { Sheet(size = 21.dp, labelled = false) }
}
/**
* Composes the sheet **once** and repaints it by swapping the pack, which is both the
* only thing `setContent` allows and the more honest picture: it is exactly what a
* television already showing a screen does the moment the gateway resolves a different
* pack, rather than what a set that happened to start up under one does.
*/
private fun sweep(suffix: String, content: @Composable () -> Unit) {
compose.setContent(content)
membyIconPackIds.forEach { id ->
applyMembyIconPack(membyIconPackFor(id))
compose.onRoot().captureRoboImage("build/screenshots/icon-packs/$id-$suffix.png")
}
}
@Composable
private fun Sheet(size: androidx.compose.ui.unit.Dp, labelled: Boolean) {
Box(Modifier.fillMaxSize().background(MembySurface)) {
LazyVerticalGrid(
columns = GridCells.Fixed(if (labelled) 9 else 14),
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(if (labelled) 10.dp else 14.dp),
) {
items(MembyIcon.entries.toList()) { slot ->
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
slot.mark,
contentDescription = null,
tint = MembyOnSurface,
modifier = Modifier.size(size),
)
if (labelled) {
Text(
slot.name,
color = MembyQuietText,
fontSize = 7.sp,
textAlign = TextAlign.Center,
modifier = Modifier.width(96.dp).padding(top = 3.dp),
)
}
}
}
}
// The rail is the case the small sheet exists for, so it is on the same image
// rather than in one nobody opens: an accent-tinted strip at the exact size and
// spacing TvNavigationRail draws.
if (!labelled) {
Row(
Modifier.align(Alignment.BottomStart).padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(18.dp),
) {
listOf(
MembyIcon.Home, MembyIcon.Search, MembyIcon.Movie, MembyIcon.Tv,
MembyIcon.Sparkle, MembyIcon.Calendar, MembyIcon.Favourite,
MembyIcon.Person, MembyIcon.Settings,
).forEach {
Icon(it.mark, null, tint = MembyAccent, modifier = Modifier.size(21.dp))
}
}
}
}
}
}
@@ -0,0 +1,71 @@
package com.ponzischeme89.memby.ui.theme
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Test
/**
* The two things about icon packs that can break silently.
*
* Neither is visible at a code review and both look identical to a working feature from the
* outside: a slot Material does not draw is a blank space on one screen somewhere, and a
* slug that does not resolve is a television that quietly ignores what the gateway told it.
*/
class MembyIconPackTest {
@Test
fun `material draws every slot`() {
// The floor every partial pack stands on. A slot added to MembyIcon and forgotten
// here would draw nothing at all — and only on whichever screen happens to use it,
// which is exactly the kind of gap that ships.
val missing = MembyIcon.entries.filterNot { it in MaterialIconPack.pack.slots }
assertTrue("Material has no mark for $missing", missing.isEmpty())
}
@Test
fun `a partial pack is legal and falls back rather than drawing nothing`() {
// Lucide is a stroke set, so it deliberately declines the filled halves of the
// state-bearing pairs. What matters is that declining costs a mark, never a screen.
val lucide = membyIconPackFor(MEMBY_ICON_PACK_LUCIDE)
assertTrue(
"Lucide should decline the filled favourite",
MembyIcon.Favourite !in lucide.slots,
)
MembyIcon.entries.forEach { slot ->
assertNotNull("no mark resolved for $slot", lucide.markFor(slot))
}
}
@Test
fun `an unreadable slug leaves the marks the app shipped with`() {
// Every one of these is an ordinary event rather than a fault: a gateway that
// predates the feature sends nothing, a theme that expresses no opinion sends
// empty, and an operator may add a pack to the catalogue before this build knows
// it. None of them may produce a launcher drawn with no marks.
listOf(null, "", " ", "tabler", "Material ", "LUCIDE").forEach { slug ->
val pack = membyIconPackFor(slug)
when (slug?.trim()?.lowercase()) {
MEMBY_ICON_PACK_LUCIDE -> assertEquals(MEMBY_ICON_PACK_LUCIDE, pack.id)
MEMBY_ICON_PACK_MATERIAL -> assertEquals(MEMBY_ICON_PACK_MATERIAL, pack.id)
else -> assertSame(
"unknown slug $slug should fall back to the shipped marks",
MaterialIconPack.pack,
pack,
)
}
}
}
@Test
fun `the slugs are the ones the gateway sends`() {
// The client's whole half of the wire contract. `internal/api/themes.go` names the
// same three, and its own test pins them from that end — change one without the
// other and a household gets the marks it did not ask for, silently.
assertEquals(
listOf("material", "lucide", "fontawesome").sorted(),
membyIconPackIds.sorted(),
)
}
}