0.2.76 - Icon Packs
This commit is contained in:
+44
-10
@@ -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 16–24px 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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user