From e6055e34277a9a2b1270beb099db8fd6a6d796b7 Mon Sep 17 00:00:00 2001 From: esson <55195336+githubesson@users.noreply.github.com> Date: Tue, 3 Nov 2026 02:49:12 +0000 Subject: [PATCH 1/6] Mobile: a night scene on the welcome screen Signed-out users now land on a record sliding out of its sleeve among the clouds, drawn through a pixel mesh, with the headline, one primary action and one quiet link below it. It replaces the bubble parade, its purple glow and the hopping title. The scene is night in dark mode and morning in light mode. The art is rendered by scripts/render-welcome-art.py and the mesh is a Skia shader, so its lines land on whole device pixels on every phone. If Skia can't read the art or compile the shader, the plain image shows instead. The auth screens' primary buttons fill with the text color, and their titles stop at weight 600. Co-Authored-By: Claude Opus 5.5 --- mobile/app/(auth)/login.tsx | 5 +- mobile/app/(auth)/register.tsx | 18 +- mobile/app/(auth)/reset-password.tsx | 5 +- mobile/app/(auth)/welcome.tsx | 173 ++++++----- mobile/assets/images/welcome-morning.jpg | Bin 0 -> 77098 bytes mobile/assets/images/welcome-night.jpg | Bin 0 -> 87350 bytes mobile/assets/images/welcome-record.png | Bin 0 -> 18760 bytes mobile/components/auth/bouncing-title.tsx | 73 ----- mobile/components/auth/bubble-parade.tsx | 256 ---------------- mobile/components/auth/motion.ts | 39 --- mobile/components/auth/welcome-art.ts | 4 + mobile/components/auth/welcome-hero.tsx | 155 ++++++++++ mobile/components/buttons.tsx | 23 +- mobile/lib/welcome-mesh.ts | 107 +++++++ mobile/scripts/render-welcome-art.py | 345 ++++++++++++++++++++++ mobile/tests/welcome-hero.test.tsx | 71 +++++ mobile/tests/welcome-mesh.test.ts | 142 +++++++++ mobile/tests/welcome-screen.test.tsx | 101 +++++++ mobile/theme/theme.tsx | 4 +- 19 files changed, 1051 insertions(+), 470 deletions(-) create mode 100644 mobile/assets/images/welcome-morning.jpg create mode 100644 mobile/assets/images/welcome-night.jpg create mode 100644 mobile/assets/images/welcome-record.png delete mode 100644 mobile/components/auth/bouncing-title.tsx delete mode 100644 mobile/components/auth/bubble-parade.tsx delete mode 100644 mobile/components/auth/motion.ts create mode 100644 mobile/components/auth/welcome-art.ts create mode 100644 mobile/components/auth/welcome-hero.tsx create mode 100644 mobile/lib/welcome-mesh.ts create mode 100644 mobile/scripts/render-welcome-art.py create mode 100644 mobile/tests/welcome-hero.test.tsx create mode 100644 mobile/tests/welcome-mesh.test.ts create mode 100644 mobile/tests/welcome-screen.test.tsx diff --git a/mobile/app/(auth)/login.tsx b/mobile/app/(auth)/login.tsx index 411cb3e7..c600a4a6 100644 --- a/mobile/app/(auth)/login.tsx +++ b/mobile/app/(auth)/login.tsx @@ -141,8 +141,8 @@ export default function LoginScreen() { opacity: pressed ? 0.6 : 1, })} > - - Have an invite? Create an account + + I have an invite @@ -156,6 +156,7 @@ export default function LoginScreen() { > - This invite is invalid, expired, revoked, or has no uses remaining. + This invite is invalid, expired, revoked, or has no uses remaining ) : ( - + ) : null} {activeInviteState.status === "verification-error" ? ( <> - void verifyInvite(activeInviteState.token)} /> @@ -296,7 +296,7 @@ export default function RegisterScreen() { {activeInviteState.status === "valid" ? ( <> - + - + - - Choose a new password before continuing. + Choose a new password before continuing @@ -114,6 +114,7 @@ export default function ResetPasswordScreen() { (null); const host = serverHost(); + const headline: TextStyle = { + color: theme.color.fg, + fontSize: 36, + lineHeight: 44, + fontWeight: "500", + letterSpacing: -0.4, + }; + const recordSize = RECORD_SIZE * Math.min(fontScale, 1.6); + return ( // Scrolls only when the content outgrows the screen (large text sizes); - // otherwise the bubbles take whatever height is left. + // otherwise the scene takes whatever height is left. - - - + {wordsTop !== null ? ( + + ) : null} - + - setWordsTop(Math.round(event.nativeEvent.layout.y))} style={{ paddingHorizontal: theme.space.xl, paddingBottom: insets.bottom + theme.space.md, - gap: theme.space.xl, }} > - - Your music, + - {"Your music library,\nlit up."} - - - Sign in to your server, or join with an invite. - - + on your + + server + + - + Lumen plays your library from {host ?? "your server"} on your phone, in + the car and offline + + + { void Haptics.selectionAsync(); router.push("/(auth)/login"); }} /> - { - void Haptics.selectionAsync(); - router.push("/(auth)/register"); - }} - accessibilityRole="button" - accessibilityLabel="Create an account with an invite" - style={({ pressed }) => ({ - alignItems: "center", - paddingVertical: 8, - opacity: pressed ? 0.6 : 1, - })} - > - - Have an invite? Create an account - - - + - { + void Haptics.selectionAsync(); + router.push("/(auth)/register"); + }} + accessibilityRole="button" + accessibilityLabel="Create an account with an invite" + style={({ pressed }) => ({ + alignItems: "center", + marginTop: theme.space.xs, + paddingVertical: theme.space.md, + opacity: pressed ? 0.6 : 1, + })} > - {host ? ( - <> - Accounts on {host}{" "} - are invite-only. - - ) : ( - "Accounts are invite-only." - )} - - + + I have an invite + + + ); } -/** Host of the server this build talks to, for the footer; null if unset. */ +/** Host of the server this build talks to, for the subhead; null if unset. */ function serverHost(): string | null { try { return new URL(getBaseUrl()).hostname || null; diff --git a/mobile/assets/images/welcome-morning.jpg b/mobile/assets/images/welcome-morning.jpg new file mode 100644 index 0000000000000000000000000000000000000000..02cb81a6ef54cbb69fa12771acb5df2df279964e GIT binary patch literal 77098 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 literal 0 HcmV?d00001 diff --git a/mobile/assets/images/welcome-night.jpg b/mobile/assets/images/welcome-night.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c51f9f70d62243af821b1973e31a8b4923c3aeb6 GIT binary patch literal 87350 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 DNr-NU literal 0 HcmV?d00001 diff --git a/mobile/components/auth/bouncing-title.tsx b/mobile/components/auth/bouncing-title.tsx deleted file mode 100644 index 2b0e4464..00000000 --- a/mobile/components/auth/bouncing-title.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { View, type TextStyle } from "react-native"; -import Animated, { - FadeInDown, - useAnimatedStyle, - type SharedValue, -} from "react-native-reanimated"; - -/** Seconds between waves rippling through the letters. */ -const WAVE_EVERY = 4.2; -/** Seconds each letter spends in the air. */ -const HOP = 0.32; -/** Seconds between neighboring letters taking off. */ -const HOP_STAGGER = 0.07; - -/** - * A word whose letters drop in one by one on mount, then periodically hop in - * a left-to-right wave. Read as one header by VoiceOver. - */ -export function BouncingTitle({ - text, - clock, - style, -}: { - text: string; - clock: SharedValue; - style: TextStyle; -}) { - const letters = Array.from(text); - return ( - - {letters.map((letter, index) => ( - - - - ))} - - ); -} - -function HoppingLetter({ - letter, - index, - clock, - style, -}: { - letter: string; - index: number; - clock: SharedValue; - style: TextStyle; -}) { - const animatedStyle = useAnimatedStyle(() => { - // The first wave lands after the drop-in settles. - const local = ((clock.value + WAVE_EVERY - 1.6) % WAVE_EVERY) - index * HOP_STAGGER; - const hop = local > 0 && local < HOP ? Math.sin((Math.PI * local) / HOP) : 0; - return { transform: [{ translateY: -7 * hop }] }; - }); - return {letter}; -} diff --git a/mobile/components/auth/bubble-parade.tsx b/mobile/components/auth/bubble-parade.tsx deleted file mode 100644 index eb795868..00000000 --- a/mobile/components/auth/bubble-parade.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import { - Pressable, - StyleSheet, - useWindowDimensions, - View, - type StyleProp, - type ViewStyle, -} from "react-native"; -import * as Haptics from "expo-haptics"; -import { SymbolView, type SFSymbol } from "expo-symbols"; -import Animated, { - Easing, - FadeIn, - useAnimatedStyle, - useSharedValue, - withSequence, - withSpring, - withTiming, - type SharedValue, -} from "react-native-reanimated"; -import { useTheme, withAlpha, type ThemeTokens } from "../../theme/theme"; - -/** Points per second the parade slides left. */ -const SPEED = 14; -/** - * Off-screen run-up on each side. Wider than half the largest bubble, so a - * bubble wraps from the left edge to the right one where nobody can see it. - */ -const EDGE_PAD = 110; - -interface Bubble { - symbol: SFSymbol; - size: number; - /** Vertical offset from the parade's midline. */ - y: number; - phase: number; -} - -// Listed in paint order: smaller bubbles sit behind larger ones. -const BUBBLES: Bubble[] = [ - { symbol: "hifispeaker.fill", size: 112, y: 34, phase: 4.1 }, - { symbol: "opticaldisc.fill", size: 118, y: 24, phase: 1.3 }, - { symbol: "music.mic", size: 126, y: 12, phase: 3.2 }, - { symbol: "pianokeys", size: 142, y: -10, phase: 5.0 }, - { symbol: "headphones", size: 150, y: -22, phase: 0.2 }, - { symbol: "guitars.fill", size: 160, y: -34, phase: 2.4 }, -]; - -// Where each bubble sits along the loop, in `BUBBLES` order, as fractions of -// the loop. Interleaved so neighbors on screen alternate big and small. -const SLOTS = [0.08, 0.58, 0.25, 0.75, 0.42, 0.92]; - -/** - * The welcome screen's hero: glass bubbles, each holding a piece of music - * gear in the brand purple, drifting right to left in an endless loop while - * they bob. Tapping one squishes it and spins what's inside. - */ -export function BubbleParade({ - clock, - style, -}: { - clock: SharedValue; - style?: StyleProp; -}) { - const theme = useTheme(); - const { width } = useWindowDimensions(); - const loop = width + EDGE_PAD * 2; - const glass = glassStyle(theme); - return ( - - - {BUBBLES.map((bubble, index) => ( - - ))} - - ); -} - -/** Scheme-specific glass: a faint fill and rim, lit from above. */ -function glassStyle(theme: ThemeTokens) { - const dark = theme.scheme === "dark"; - return { - sphere: { - backgroundColor: dark ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.02)", - borderColor: dark ? "rgba(255,255,255,0.14)" : "rgba(0,0,0,0.07)", - boxShadow: dark - ? `inset 0 2px 6px rgba(255,255,255,0.22), inset 0 -12px 24px ${withAlpha(theme.color.brand, 0.2)}, inset 0 0 20px rgba(255,255,255,0.06)` - : `inset 0 2px 6px rgba(255,255,255,0.9), inset 0 -12px 24px ${withAlpha(theme.color.brand, 0.08)}, inset 0 0 18px rgba(0,0,0,0.04)`, - } satisfies ViewStyle, - shadow: { - boxShadow: dark - ? "0 18px 36px rgba(0,0,0,0.6)" - : "0 14px 30px rgba(0,0,0,0.1)", - } satisfies ViewStyle, - innerGlow: withAlpha(theme.color.brand, dark ? 0.22 : 0.1), - specular: dark ? 0.45 : 0.9, - }; -} - -function GlassBubble({ - bubble, - start, - loop, - clock, - glass, - tint, -}: { - bubble: Bubble; - start: number; - loop: number; - clock: SharedValue; - glass: ReturnType; - tint: string; -}) { - const size = bubble.size; - const squish = useSharedValue(0); - const spin = useSharedValue(0); - - const travelStyle = useAnimatedStyle(() => { - const t = clock.value; - const along = (((start - t * SPEED) % loop) + loop) % loop; - return { - transform: [ - { translateX: along - EDGE_PAD - size / 2 }, - { translateY: bubble.y + 9 * Math.sin(t * 0.9 + bubble.phase) }, - ], - }; - }); - const sphereStyle = useAnimatedStyle(() => ({ - transform: [ - { scaleX: 1 + 0.1 * squish.value }, - { scaleY: 1 - 0.14 * squish.value }, - ], - })); - const objectStyle = useAnimatedStyle(() => { - const t = clock.value; - return { - transform: [ - { translateY: 3 * Math.sin(t * 1.3 + bubble.phase) }, - { rotate: `${8 * Math.sin(t * 0.8 + bubble.phase) + spin.value}deg` }, - ], - }; - }); - - const onPress = () => { - void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); - squish.set( - withSequence( - withTiming(1, { duration: 90 }), - withSpring(0, { damping: 5, stiffness: 180 }), - ), - ); - spin.set( - withTiming(spin.get() + 360, { - duration: 750, - easing: Easing.out(Easing.back(1.6)), - }), - ); - }; - - const round = { width: size, height: size, borderRadius: size / 2 }; - return ( - - - {/* Kept apart from the clipped sphere so the drop shadow isn't cut off. */} - - - - - - - - - - - - ); -} - -const styles = StyleSheet.create({ - glow: { - position: "absolute", - left: "-10%", - right: "-10%", - top: "15%", - bottom: "5%", - }, - slot: { - position: "absolute", - left: 0, - // A touch below center: the headline block below is taller than the - // wordmark above, so this balances the gaps. - top: "54%", - }, - sphere: { - overflow: "hidden", - alignItems: "center", - justifyContent: "center", - borderWidth: 1, - }, - specular: { - position: "absolute", - transform: [{ rotate: "-32deg" }], - }, -}); diff --git a/mobile/components/auth/motion.ts b/mobile/components/auth/motion.ts deleted file mode 100644 index dc8718ca..00000000 --- a/mobile/components/auth/motion.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useEffect } from "react"; -import { useIsFocused } from "expo-router"; -import { - useFrameCallback, - useReducedMotion, - useSharedValue, - type SharedValue, -} from "react-native-reanimated"; - -/** Largest frame step the clock accepts, so a resume never jumps the scene. */ -const MAX_FRAME_STEP = 1 / 15; - -/** - * Seconds of running animation time, advanced on the UI thread. Every ambient - * loop on the welcome screen (bubble parade, title hop) derives - * its phase from this one clock, so pausing is a single frame-callback toggle - * and resuming picks up where it left off. - */ -export function useMotionClock(active: boolean): SharedValue { - const clock = useSharedValue(0); - const frame = useFrameCallback((info) => { - const step = (info.timeSincePreviousFrame ?? 0) / 1000; - clock.value += Math.min(step, MAX_FRAME_STEP); - }, false); - useEffect(() => { - frame.setActive(active); - }, [active, frame]); - return clock; -} - -/** - * The clock for an auth screen: it only runs while the screen is on top and - * motion is allowed. With Reduce Motion the scene renders as a still frame. - */ -export function useAuthMotionClock(): SharedValue { - const reducedMotion = useReducedMotion(); - const focused = useIsFocused(); - return useMotionClock(focused && !reducedMotion); -} diff --git a/mobile/components/auth/welcome-art.ts b/mobile/components/auth/welcome-art.ts new file mode 100644 index 00000000..9f9960e8 --- /dev/null +++ b/mobile/components/auth/welcome-art.ts @@ -0,0 +1,4 @@ +/** The welcome scene's files, rendered by `scripts/render-welcome-art.py`. */ +export const WELCOME_NIGHT = require("../../assets/images/welcome-night.jpg"); +export const WELCOME_MORNING = require("../../assets/images/welcome-morning.jpg"); +export const WELCOME_RECORD = require("../../assets/images/welcome-record.png"); diff --git a/mobile/components/auth/welcome-hero.tsx b/mobile/components/auth/welcome-hero.tsx new file mode 100644 index 00000000..16bce025 --- /dev/null +++ b/mobile/components/auth/welcome-hero.tsx @@ -0,0 +1,155 @@ +import { useEffect, useState } from "react"; +import { + Image as NativeImage, + PixelRatio, + StyleSheet, + useWindowDimensions, + View, +} from "react-native"; +import { Image } from "expo-image"; +import Animated, { FadeIn } from "react-native-reanimated"; +import { + Canvas, + Fill, + FilterMode, + ImageShader, + MipmapMode, + Shader, + Skia, + type SkImage, + type SkRuntimeEffect, +} from "@shopify/react-native-skia"; +import { + WELCOME_MESH_SHADER, + loadWelcomeArt, + welcomeArtRect, + welcomeFadeHeight, + welcomeMesh, +} from "../../lib/welcome-mesh"; +import { useTheme, withAlpha } from "../../theme/theme"; +import { WELCOME_MORNING, WELCOME_NIGHT } from "./welcome-art"; + +const ENTER = FadeIn.duration(400); +/** How much light is left in the lines between cells, per scheme. */ +const LINE_LIGHT = { dark: 0.6, light: 0.88 } as const; + +let meshEffect: SkRuntimeEffect | null | undefined; + +/** The compiled mesh, or null if this device's Skia refuses it. */ +function getMeshEffect(): SkRuntimeEffect | null { + if (meshEffect === undefined) { + try { + meshEffect = Skia.RuntimeEffect.Make(WELCOME_MESH_SHADER); + } catch { + meshEffect = null; + } + } + return meshEffect; +} + +/** The decoded art for Skia: null while loading, and `failed` if it never will. */ +function useWelcomeArt(source: number): { art: SkImage | null; failed: boolean } { + const [state, setState] = useState<{ art: SkImage | null; failed: boolean }>({ + art: null, + failed: false, + }); + useEffect(() => { + let live = true; + void loadWelcomeArt(NativeImage.resolveAssetSource(source).uri, Skia).then((art) => { + if (live) setState({ art, failed: art === null }); + }); + return () => { + live = false; + }; + }, [source]); + return state; +} + +/** + * The welcome screen's scene: a record sliding out of its sleeve among the + * clouds, at night in dark mode and in the morning in light mode, drawn through + * a pixel mesh and fading into the page. Without the mesh it's the plain image. + */ +export function WelcomeHero({ height }: { height: number }) { + const theme = useTheme(); + const { width } = useWindowDimensions(); + const source = theme.scheme === "dark" ? WELCOME_NIGHT : WELCOME_MORNING; + const { art, failed } = useWelcomeArt(source); + const effect = getMeshEffect(); + const view = { width, height }; + const fadeColor = theme.color.bg; + + return ( + + {effect && !failed ? ( + art ? ( + + + + + + + + + + ) : null + ) : ( + + + + )} + + + ); +} + +const styles = StyleSheet.create({ + hero: { + position: "absolute", + top: 0, + left: 0, + }, + fade: { + position: "absolute", + left: 0, + right: 0, + // One point past the edge, so no row of the mesh survives under the text. + bottom: -1, + }, +}); diff --git a/mobile/components/buttons.tsx b/mobile/components/buttons.tsx index 923b4a2a..ce5c4663 100644 --- a/mobile/components/buttons.tsx +++ b/mobile/components/buttons.tsx @@ -1,6 +1,8 @@ import { ActivityIndicator, Pressable, Text } from "react-native"; import { useTheme } from "../theme/theme"; +const BUTTON_HEIGHT = 48; + /** * Full-width filled accent button used for primary form actions (sign in, * create account, upload, save). Previously hand-rolled in six screens with @@ -14,15 +16,21 @@ export function PrimaryButton({ loading = false, disabled = false, accessibilityLabel, + tone = "accent", }: { label: string; onPress: () => void; loading?: boolean; disabled?: boolean; accessibilityLabel?: string; + /** `inverse` fills with the text color, for the signed-out screens. */ + tone?: "accent" | "inverse"; }) { const theme = useTheme(); const isDisabled = disabled || loading; + const inverse = tone === "inverse"; + const background = inverse ? theme.color.fg : theme.color.accent; + const foreground = inverse ? theme.color.bg : theme.color.onAccent; return ( ({ - backgroundColor: theme.color.accent, + backgroundColor: background, borderRadius: theme.radius.md, - paddingVertical: 14, + minHeight: BUTTON_HEIGHT, + paddingVertical: 12, alignItems: "center", justifyContent: "center", borderCurve: "continuous", @@ -41,11 +50,9 @@ export function PrimaryButton({ })} > {loading ? ( - + ) : ( - + {label} )} @@ -81,8 +88,10 @@ export function SecondaryButton({ backgroundColor: theme.color.bgElev1, borderRadius: theme.radius.md, borderCurve: "continuous", - paddingVertical: 11, + minHeight: BUTTON_HEIGHT, + paddingVertical: 12, alignItems: "center", + justifyContent: "center", opacity: disabled ? 0.5 : pressed ? 0.8 : 1, })} > diff --git a/mobile/lib/welcome-mesh.ts b/mobile/lib/welcome-mesh.ts new file mode 100644 index 00000000..292a75c9 --- /dev/null +++ b/mobile/lib/welcome-mesh.ts @@ -0,0 +1,107 @@ +/** + * The welcome screen's art is a smooth image drawn through a pixel mesh on the + * device. The mesh has to land on whole device pixels: baked into the file it + * would shimmer on any phone that scales the image. + */ + +/** Spacing of the mesh, in points. */ +const CELL_POINTS = 3; +/** The dark line between cells, as a share of a point. */ +const GAP_POINTS = 2 / 3; + +export interface WelcomeMesh { + /** Device pixels from one cell to the next. */ + pitch: number; + /** Device pixels of dark line along each cell's top and left. */ + gap: number; +} + +export function welcomeMesh(pixelRatio: number): WelcomeMesh { + return { + pitch: Math.max(2, Math.round(CELL_POINTS * pixelRatio)), + gap: Math.max(1, Math.round(GAP_POINTS * pixelRatio)), + }; +} + +/** The share of the art's height at which the record sits. */ +const RECORD_IN_ART = 0.395; +/** The highest share of the view the record may be pushed down to. */ +const RECORD_IN_VIEW = 0.46; + +export interface WelcomeArtRect { + x: number; + y: number; + width: number; + height: number; +} + +/** + * Where the art goes: filling the view and centered sideways. It's pinned to the + * top unless the view is short, when it slides up to keep the record clear of + * the fade; the sky above the record is the part it can afford to lose. + */ +export function welcomeArtRect( + view: { width: number; height: number }, + art: { width: number; height: number }, +): WelcomeArtRect { + const scale = Math.max(view.width / art.width, view.height / art.height); + const width = art.width * scale; + const height = art.height * scale; + return { + x: (view.width - width) / 2, + y: Math.min(0, view.height * RECORD_IN_VIEW - height * RECORD_IN_ART), + width, + height, + }; +} + +/** How tall the fade into the page is, for a hero of this height. */ +export function welcomeFadeHeight(heroHeight: number): number { + return Math.min(240, Math.max(96, Math.round(heroHeight * 0.3))); +} + +interface WelcomeArtLoader { + Data: { fromURI: (uri: string) => Promise }; + Image: { MakeImageFromEncoded: (data: Data) => Art | null }; +} + +/** Reads and decodes the art; null if either step fails, so the caller can fall back. */ +export async function loadWelcomeArt( + uri: string, + skia: WelcomeArtLoader, +): Promise { + try { + return skia.Image.MakeImageFromEncoded(await skia.Data.fromURI(uri)); + } catch { + return null; + } +} + +/** + * Colors each cell with the average of four samples of the art, then darkens + * the lines between cells. `density` turns Skia's points back into device pixels. + */ +export const WELCOME_MESH_SHADER = ` +uniform shader art; +uniform float density; +uniform float pitch; +uniform float gap; +uniform float lineLight; + +half4 main(vec2 pos) { + vec2 px = pos * density; + vec2 cell = floor(px / pitch); + vec2 inCell = px - cell * pitch; + vec2 center = (cell + 0.5) * pitch / density; + float spread = pitch / density * 0.25; + half4 color = ( + art.eval(center + vec2(-spread, -spread)) + + art.eval(center + vec2(spread, -spread)) + + art.eval(center + vec2(-spread, spread)) + + art.eval(center + vec2(spread, spread)) + ) * 0.25; + float lit = step(gap, inCell.x) * step(gap, inCell.y); + color.rgb *= mix(lineLight, 1.0, lit); + return color; +} +`; diff --git a/mobile/scripts/render-welcome-art.py b/mobile/scripts/render-welcome-art.py new file mode 100644 index 00000000..fe429b19 --- /dev/null +++ b/mobile/scripts/render-welcome-art.py @@ -0,0 +1,345 @@ +#!/usr/bin/env python3 +"""Render the welcome screen's art: a record sliding out of its sleeve, floating +through clouds. Night for dark mode, early morning for light. + +The output is deliberately smooth. `components/auth/welcome-hero.tsx` turns it +into a pixel mesh on the device, in exact device pixels, because a mesh baked +into the file would shimmer on any phone that has to scale it. + + python3 scripts/render-welcome-art.py [output dir, default assets/images] + +Needs numpy and Pillow. Rerun it only to change the art. +""" +from __future__ import annotations + +import sys +from pathlib import Path + +import numpy as np +from PIL import Image + +W, H = 660, 960 +SS = 2 # supersampling +RNG = np.random.default_rng(7) + +PALETTES = { + "night": dict( + sky_stops=[(0.0, "#040716"), (0.22, "#081242"), (0.52, "#10267c"), (0.76, "#0a154e"), (1.0, "#02040f")], + cloud_shadow="#1c2565", + cloud_mid="#7c78b8", + cloud_lit="#f0e1ea", + cloud_alpha=1.0, + stars=0.9, + paper_lit="#f1e8d6", + paper_shade="#b7ac9c", + rim="#6d6558", + ambient=1.0, + ), + "morning": dict( + sky_stops=[(0.0, "#8fb2ea"), (0.3, "#a9c6f2"), (0.62, "#cfe0f8"), (1.0, "#ffffff")], + cloud_shadow="#b5c4e6", + cloud_mid="#e4e9f7", + cloud_lit="#ffffff", + cloud_alpha=1.0, + stars=0.0, + paper_lit="#fbf5e8", + paper_shade="#d3c8b6", + rim="#8f8676", + ambient=1.0, + ), +} + + +def rgb(value: str) -> np.ndarray: + value = value.lstrip("#") + return np.array([int(value[i : i + 2], 16) / 255 for i in (0, 2, 4)]) + + +def smooth(a, b, x): + t = np.clip((x - a) / (b - a), 0, 1) + return t * t * (3 - 2 * t) + + +def mix(a, b, t): + t = np.asarray(t) + if t.ndim == 2: + t = t[..., None] + return a * (1 - t) + b * t + + +def value_noise(w: int, h: int, cell: float, rng: np.random.Generator) -> np.ndarray: + gw, gh = int(w / cell) + 3, int(h / cell) + 3 + grid = rng.random((gh, gw)) + ys, xs = np.mgrid[0:h, 0:w] / cell + x0, y0 = xs.astype(int), ys.astype(int) + fx, fy = smooth(0, 1, xs - x0), smooth(0, 1, ys - y0) + top = grid[y0, x0] * (1 - fx) + grid[y0, x0 + 1] * fx + bottom = grid[y0 + 1, x0] * (1 - fx) + grid[y0 + 1, x0 + 1] * fx + return top * (1 - fy) + bottom * fy + + +def fbm(w: int, h: int, cell: float, rng: np.random.Generator, octaves: int = 5) -> np.ndarray: + out, amp, total = np.zeros((h, w)), 1.0, 0.0 + for _ in range(octaves): + out += value_noise(w, h, cell, rng) * amp + total += amp + amp *= 0.5 + cell = max(cell / 2, 1.5) + return out / total + + +def gradient(stops, h, w): + pos = np.array([s[0] for s in stops]) + cols = np.stack([rgb(s[1]) for s in stops]) + t = np.arange(h) / (h - 1) + channels = np.stack([np.interp(t, pos, cols[:, c]) for c in range(3)], -1) + # A wide box blur irons out the creases at each stop. + radius = h // 14 + kernel = np.ones(radius * 2 + 1) / (radius * 2 + 1) + padded = np.pad(channels, ((radius, radius), (0, 0)), mode="edge") + channels = np.stack([np.convolve(padded[:, c], kernel, mode="valid") for c in range(3)], -1) + return np.repeat(channels[:, None, :], w, axis=1) + + +def box_blur(a, radius): + """Two passes of a summed-area box blur, close enough to a gaussian.""" + for _ in range(2): + for axis in (0, 1): + pad = [(radius + 1, radius) if i == axis else (0, 0) for i in range(2)] + total = np.cumsum(np.pad(a, pad, mode="edge"), axis=axis) + hi = np.take(total, range(2 * radius + 1, total.shape[axis]), axis=axis) + lo = np.take(total, range(0, total.shape[axis] - 2 * radius - 1), axis=axis) + a = (hi - lo) / (2 * radius + 1) + return a + + +def cumulus(canvas, cx, base, width, height, pal, seed, puffs=70): + """A cloud built from many overlapping spheres on a soft, flat base, lit from + the upper left. The height map is blurred so the puffs melt into each other.""" + h, w = canvas.shape[:2] + cx, base, width, height = cx * w, base * h, width * w, height * w + rng = np.random.default_rng(seed) + ys, xs = np.mgrid[0:h, 0:w].astype(float) + warp = height * 0.3 + bx = xs + (fbm(w, h, 44 * SS, rng, 3) - 0.5) * warp + by = ys + (fbm(w, h, 44 * SS, rng, 3) - 0.5) * warp * 0.6 + z = np.zeros((h, w)) + for _ in range(puffs): + t = rng.uniform(-1, 1) + dome = max(1 - abs(t) ** 2.2, 0) ** 0.8 + r = height * rng.uniform(0.09, 0.3) * (0.35 + 0.65 * dome) + px = cx + t * width + py = base - r * 0.4 - (height - r) * dome * rng.random() ** 1.3 + z = np.maximum(z, np.sqrt(np.maximum(r * r - (bx - px) ** 2 - (by - py) ** 2, 0))) + wisp = (fbm(w, h, 26 * SS, rng, 3) - 0.5) * height * 0.28 + fade = 1 - smooth(-height * 0.1, height * 0.1, by - base + wisp) + alpha = smooth(0.0, 2.5 * SS, z) * fade + soft = box_blur(z, 3 * SS) + gy, gx = np.gradient(soft) + nx, ny, nz = -gx, -gy, np.full_like(z, 0.9) + length = np.sqrt(nx * nx + ny * ny + nz * nz) + lit = (nx * -0.5 + ny * -0.72 + nz * 0.48) / length + rise = np.clip((base - by) / height, 0, 1) + fine = fbm(w, h, 11 * SS, rng, 3) - 0.5 + tone = np.clip(lit * 0.7 + rise * 0.5 + fine * 0.26, 0, 1) + col = mix(rgb(pal["cloud_shadow"]), rgb(pal["cloud_mid"]), smooth(0.08, 0.55, tone)) + col = mix(col, rgb(pal["cloud_lit"]), smooth(0.56, 0.98, tone)) + return mix(canvas, col, alpha * pal["cloud_alpha"]) + + +def plane_coords(w, h, center, size, rot_deg, tilt_x, tilt_y): + """Screen pixels to a tilted plane's coordinates, where 1 unit is `size` px.""" + ys, xs = np.mgrid[0:h, 0:w].astype(float) + x, y = xs - center[0], ys - center[1] + r = np.deg2rad(rot_deg) + ca, sa = np.cos(r), np.sin(r) + # Inverse perspective: plane (u, v) projects to (u, v) / (1 + tx u + ty v). + denom = 1 - tilt_x * x / size - tilt_y * y / size + px, py = x / denom, y / denom + u = (px * ca + py * sa) / size + v = (-px * sa + py * ca) / size + return u, v + + +def rounded_box(u, v, half, radius): + qx, qy = np.abs(u) - half + radius, np.abs(v) - half + radius + return np.hypot(np.maximum(qx, 0), np.maximum(qy, 0)) + np.minimum(np.maximum(qx, qy), 0) - radius + + +def record(u, v, cx, ambient, seed): + """A black disc whose grooves catch one bow tie of light, with an amber label.""" + rng = np.random.default_rng(seed) + du, dv = u - cx, v + r = np.hypot(du, dv) / 0.92 + th = np.arctan2(dv, du) + col = np.zeros(u.shape + (3,)) + col[...] = rgb("#0b0b10") + + # Bands of grooves, split by the smooth gaps between tracks. + grooves = 0.5 + 0.5 * np.sin(r * 300) + gaps = np.minimum.reduce([np.abs(r - g) for g in (0.5, 0.62, 0.76, 0.88)]) + in_gap = 1 - smooth(0.004, 0.013, gaps) + base = 0.055 + grooves * 0.035 + col += base[..., None] * np.array([0.9, 0.95, 1.1]) + + # The bow tie of light any record shows under one lamp, with a faint second + # lobe that picks up a little color. + lamp = -1.05 + ray = np.power(np.abs(np.cos(th - lamp)), 26) + side = np.power(np.abs(np.cos(th - lamp - 0.62)), 48) + body = (0.7 + 0.3 * grooves) * (1 - in_gap * 0.75) * (1 - smooth(0.34, 0.5, 1 - r) * 0) + col += (ray * body * 0.92)[..., None] * np.array([0.86, 0.91, 1.0]) + col += (side * body * 0.34)[..., None] * np.array([0.72, 0.62, 1.0]) + col += (in_gap * 0.045)[..., None] + + # Edge bevel. + bevel = smooth(0.965, 0.985, r) * (1 - smooth(0.992, 1.0, r)) + light = np.power(np.abs(np.cos(th - lamp)), 3) + col += (bevel * (0.08 + 0.5 * light))[..., None] * np.array([0.85, 0.9, 1.0]) + + # Dead wax and label. + wax = smooth(0.37, 0.385, r) * (1 - smooth(0.43, 0.45, r)) + col = mix(col, rgb("#101015"), wax * 0.75) + label = 1 - smooth(0.365, 0.374, r) + amber = mix(rgb("#ff9a2e"), rgb("#c6410b"), smooth(0.02, 0.38, r)) + shine = np.power(np.abs(np.cos(th - lamp)), 8) + amber = mix(amber, rgb("#ffd98a"), shine * 0.62) + ring = (1 - smooth(0.003, 0.012, np.abs(r - 0.315))) * 0.6 + amber = mix(amber, rgb("#7e2406"), ring) + speck = fbm(u.shape[1], u.shape[0], 5, rng, 2) - 0.5 + amber = np.clip(amber + speck[..., None] * 0.08, 0, 1) + col = mix(col, amber, label) + + # Spindle hole. + hole = 1 - smooth(0.034, 0.042, r) + col = mix(col, rgb("#120a05"), hole) + + mask = 1 - smooth(0.996, 1.004, r) + return np.clip(col * ambient, 0, 1), mask, r + + +def sleeve(u, v, pal, seed): + """A worn card sleeve: lit from the upper left, with the disc's ring pressed in.""" + rng = np.random.default_rng(seed) + half = 1.0 + d = rounded_box(u, v, half, 0.012) + mask = 1 - smooth(-0.002, 0.004, d) + light = np.clip(0.66 - (u * 0.34 + v * 0.42), 0, 1) + col = mix(rgb(pal["paper_shade"]), rgb(pal["paper_lit"]), smooth(0.0, 0.95, light)) + + h, w = u.shape + fiber = fbm(w, h, 8, rng, 3) - 0.5 + col = np.clip(col + fiber[..., None] * 0.05, 0, 1) + + # Ring wear from the disc inside. + rr = np.hypot(u + 0.02, v) / 0.92 + ring = 1 - smooth(0.0, 0.03, np.abs(rr - 0.985)) + arc = 0.5 + 0.5 * np.cos(np.arctan2(v, u) + 2.1) + col = mix(col, rgb(pal["paper_lit"]) * 1.03, ring * 0.4 * arc) + col = mix(col, rgb(pal["paper_shade"]) * 0.78, ring * 0.36 * (1 - arc)) + + # The app icon's waveform, printed on the front like a label's mark. + bars = (0.2, 0.46, 0.3, 0.74, 1.0, 0.56, 0.86, 0.38, 0.62, 0.24, 0.12) + ink = np.zeros_like(u) + pitch, half_width = 0.112, 0.04 + for i, tall in enumerate(bars): + bx = (i - (len(bars) - 1) / 2) * pitch + bar_half = tall * 0.46 + dx = np.abs(u - bx) + dy = np.maximum(np.abs(v) - bar_half, 0) + ink = np.maximum(ink, 1 - smooth(half_width - 0.006, half_width + 0.006, np.hypot(dx, dy))) + # A printed mark wears through a little, like everything else on the sleeve. + grain = smooth(0.34, 0.5, fbm(w, h, 7, rng, 2)) + col = mix(col, rgb("#17204f"), ink * (0.8 + 0.2 * grain)) + + # The fold on the closed side, and the lip of the open one. + fold = 1 - smooth(0.0, 0.035, np.abs(u + 0.955)) + col = mix(col, rgb(pal["paper_shade"]) * 0.8, fold * 0.42) + lip = 1 - smooth(0.0, 0.05, half - u) + col = mix(col, rgb(pal["paper_shade"]) * 0.76, lip * 0.5) + # Paler, scuffed corners and edges, where a much-played sleeve wears first. + wear = 1 - smooth(-0.05, 0.0, d) + worn = smooth(-0.07, -0.005, d) * smooth(0.45, 0.62, fbm(w, h, 18, rng, 3)) + col = mix(col, rgb(pal["paper_lit"]) * 1.06, worn * (1 - wear * 0) * 0.5) + return np.clip(col, 0, 1), mask, d + + +def render_scene(name: str) -> np.ndarray: + pal = PALETTES[name] + w, h = W * SS, H * SS + canvas = gradient(pal["sky_stops"], h, w) + + # A few faint stars up high, one mesh cell each once they reach the phone. + if pal["stars"] > 0: + count = 18 + sx, sy = RNG.integers(0, w, count), (RNG.random(count) ** 1.5 * h * 0.5).astype(int) + sb = RNG.random(count) + ys, xs = np.mgrid[0:h, 0:w].astype(float) + star = np.zeros((h, w)) + for x, y, b in zip(sx, sy, sb): + r = 2.4 * SS + x0, x1 = max(x - 12, 0), min(x + 12, w) + y0, y1 = max(y - 12, 0), min(y + 12, h) + star[y0:y1, x0:x1] += np.exp(-(((xs[y0:y1, x0:x1] - x) ** 2 + (ys[y0:y1, x0:x1] - y) ** 2) / (r * r))) * (0.2 + b * 0.6) + canvas = mix(canvas, rgb("#dfe5ff"), np.clip(star * pal["stars"], 0, 1)) + + # A cloud far off behind the record. + canvas = cumulus(canvas, 0.88, 0.125, 0.21, 0.09, pal, 11, puffs=70) + + # The record and sleeve, tilted like something falling slowly through the air. + center = (w * 0.395, h * 0.395) + size = w * 0.205 + plane = dict(rot_deg=-16, tilt_x=0.05, tilt_y=-0.04) + u, v = plane_coords(w, h, center, size, **plane) + + # Card thickness: the same outline, nudged down and right, in shadow. + tu, tv = plane_coords(w, h, (center[0] + 2.0 * SS, center[1] + 3.0 * SS), size, **plane) + _, edge_mask, _ = sleeve(tu, tv, pal, 21) + canvas = mix(canvas, rgb(pal["rim"]), edge_mask) + + disc, disc_mask, _ = record(u, v, 1.42, pal["ambient"], 31) + canvas = mix(canvas, disc, disc_mask) + + paper, paper_mask, _ = sleeve(u, v, pal, 21) + # The sleeve's edge shades the disc just where it comes out. + shade = (1 - smooth(0.0, 0.16, u - 1.0)) * disc_mask * (1 - paper_mask) + canvas = mix(canvas, np.zeros(3), shade * 0.55) + canvas = mix(canvas, paper, paper_mask) + + # Clouds in front, so the record sits inside the scene. + canvas = cumulus(canvas, 0.07, 0.668, 0.33, 0.105, pal, 5, puffs=120) + canvas = cumulus(canvas, 0.92, 0.75, 0.44, 0.21, pal, 8, puffs=150) + + img = Image.fromarray((np.clip(canvas, 0, 1) * 255 + 0.5).astype(np.uint8), "RGB") + return np.asarray(img.resize((W, H), Image.LANCZOS)) + + +def render_badge(size: int = 108) -> np.ndarray: + """The label's record, front on, small enough to sit inside the headline.""" + s = size * 4 + ys, xs = np.mgrid[0:s, 0:s].astype(float) + u, v = (xs - s / 2) / (s / 2) * 0.92, (ys - s / 2) / (s / 2) * 0.92 + col, mask, _ = record(u, v, 0.0, 1.0, 41) + # Larger label than the real proportion: at 30pt this has to read as amber. + r = np.hypot(u, v) / 0.92 + th = np.arctan2(v, u) + amber = mix(rgb("#ff9a2e"), rgb("#c6410b"), smooth(0.02, 0.52, r)) + amber = mix(amber, rgb("#ffd98a"), np.power(np.abs(np.cos(th + 1.05)), 8) * 0.62) + label = 1 - smooth(0.5, 0.515, r) + col = mix(col, amber, label) + col = mix(col, rgb("#120a05"), 1 - smooth(0.07, 0.085, r)) + rgba = np.dstack([np.clip(col, 0, 1), mask]) + img = Image.fromarray((rgba * 255 + 0.5).astype(np.uint8), "RGBA") + return img.resize((size, size), Image.LANCZOS) + + +def main() -> None: + out = Path(sys.argv[1] if len(sys.argv) > 1 else "assets/images") + out.mkdir(parents=True, exist_ok=True) + for name in ("night", "morning"): + Image.fromarray(render_scene(name)).save(out / f"welcome-{name}.jpg", quality=92, subsampling=0) + render_badge().save(out / "welcome-record.png", optimize=True) + + +if __name__ == "__main__": + main() diff --git a/mobile/tests/welcome-hero.test.tsx b/mobile/tests/welcome-hero.test.tsx new file mode 100644 index 00000000..18fdd9d1 --- /dev/null +++ b/mobile/tests/welcome-hero.test.tsx @@ -0,0 +1,71 @@ +import { type ReactNode } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { beforeEach, expect, it, vi } from "vitest"; + +const mock = vi.hoisted(() => ({ make: vi.fn() })); + +vi.mock("react-native", () => ({ + Image: { resolveAssetSource: () => ({ uri: "file:///art.jpg" }) }, + PixelRatio: { get: () => 3 }, + StyleSheet: { absoluteFill: {}, create: (styles: T) => styles }, + useWindowDimensions: () => ({ width: 393, height: 852 }), + View: ({ children }: { children?: ReactNode }) =>
{children}
, +})); +vi.mock("expo-image", () => ({ Image: () => plain art })); +vi.mock("react-native-reanimated", () => { + const builder = { duration: () => builder }; + return { + default: { + View: ({ children }: { children: ReactNode }) =>
{children}
, + }, + FadeIn: builder, + }; +}); +vi.mock("@shopify/react-native-skia", () => ({ + Canvas: () => , + Fill: () => null, + FilterMode: {}, + ImageShader: () => null, + MipmapMode: {}, + Shader: () => null, + Skia: { RuntimeEffect: { Make: mock.make } }, +})); +vi.mock("../components/auth/welcome-art", () => ({ + WELCOME_NIGHT: 1, + WELCOME_MORNING: 2, +})); +vi.mock("../theme/theme", () => ({ + useTheme: () => ({ scheme: "dark", color: { bg: "#000000" } }), + withAlpha: (color: string) => color, +})); + +beforeEach(() => { + vi.resetModules(); + mock.make.mockReset(); +}); + +async function render() { + const { WelcomeHero } = await import("../components/auth/welcome-hero"); + return renderToStaticMarkup(); +} + +it("shows the plain art when this device's Skia refuses the mesh", async () => { + mock.make.mockImplementation(() => { + throw new Error("Error in SKSL"); + }); + expect(await render()).toContain('alt="plain art"'); +}); + +it("waits for the art without flashing the plain image", async () => { + mock.make.mockReturnValue({}); + const html = await render(); + expect(html).not.toContain("plain art"); + expect(html).not.toContain(" { + mock.make.mockReturnValue({}); + await render(); + await render(); + expect(mock.make).toHaveBeenCalledTimes(1); +}); diff --git a/mobile/tests/welcome-mesh.test.ts b/mobile/tests/welcome-mesh.test.ts new file mode 100644 index 00000000..2aee5aa0 --- /dev/null +++ b/mobile/tests/welcome-mesh.test.ts @@ -0,0 +1,142 @@ +import CanvasKitInit from "canvaskit-wasm"; +import { describe, expect, it, vi } from "vitest"; +import { + WELCOME_MESH_SHADER, + loadWelcomeArt, + welcomeArtRect, + welcomeFadeHeight, + welcomeMesh, +} from "../lib/welcome-mesh"; + +describe("welcomeMesh", () => { + it("lands on whole device pixels at every screen density", () => { + expect(welcomeMesh(3)).toEqual({ pitch: 9, gap: 2 }); + expect(welcomeMesh(2)).toEqual({ pitch: 6, gap: 1 }); + expect(welcomeMesh(2.625)).toEqual({ pitch: 8, gap: 2 }); + }); +}); + +describe("welcomeArtRect", () => { + const art = { width: 660, height: 960 }; + + it("fills the width and keeps the sky on a tall view", () => { + expect(welcomeArtRect({ width: 393, height: 540 }, art)).toEqual({ + x: 0, + y: 0, + width: 393, + height: (960 * 393) / 660, + }); + }); + + it("slides up on a short view, to keep the record out of the fade", () => { + const rect = welcomeArtRect({ width: 375, height: 380 }, art); + expect(rect.width).toBeCloseTo(375); + expect(rect.y).toBeLessThan(0); + // The record sits at 39.5% of the art. + expect(rect.y + rect.height * 0.395).toBeCloseTo(380 * 0.46); + }); + + it("fills the height and centers when the view is wider than the art", () => { + const rect = welcomeArtRect({ width: 300, height: 960 }, art); + expect(rect).toEqual({ x: -180, y: 0, width: 660, height: 960 }); + }); +}); + +describe("welcomeFadeHeight", () => { + it("scales with the hero within its limits", () => { + expect(welcomeFadeHeight(200)).toBe(96); + expect(welcomeFadeHeight(540)).toBe(162); + expect(welcomeFadeHeight(1200)).toBe(240); + }); +}); + +describe("loadWelcomeArt", () => { + const skia = (decoded: string | null) => ({ + Data: { fromURI: async (uri: string) => `bytes of ${uri}` }, + Image: { MakeImageFromEncoded: () => decoded }, + }); + + it("decodes the file at the URI", async () => { + const decode = vi.fn(() => "image"); + const art = await loadWelcomeArt("file:///night.jpg", { + Data: skia("").Data, + Image: { MakeImageFromEncoded: decode }, + }); + expect(art).toBe("image"); + expect(decode).toHaveBeenCalledWith("bytes of file:///night.jpg"); + }); + + it("reports a file it can't decode", async () => { + expect(await loadWelcomeArt("file:///night.jpg", skia(null))).toBeNull(); + }); + + it("reports a file it can't read, rather than rejecting", async () => { + const broken = { + Data: { fromURI: () => Promise.reject(new Error("no such file")) }, + Image: { MakeImageFromEncoded: () => "image" }, + }; + await expect(loadWelcomeArt("file:///night.jpg", broken)).resolves.toBeNull(); + }); +}); + +describe("the mesh shader, under real Skia", () => { + async function make() { + const CK = await CanvasKitInit(); + const errors: string[] = []; + const effect = CK.RuntimeEffect.Make(WELCOME_MESH_SHADER, (e) => errors.push(e)); + expect(errors).toEqual([]); + if (!effect) throw new Error("the shader didn't compile"); + return { CK, effect }; + } + + it("takes exactly the uniforms the screen passes", async () => { + const { effect } = await make(); + const names = Array.from({ length: effect.getUniformCount() }, (_, i) => + effect.getUniformName(i), + ); + // RN Skia throws on a missing uniform, by name. + expect(new Set(names)).toEqual( + new Set(["density", ...Object.keys(welcomeMesh(3)), "lineLight"]), + ); + }); + + it("dims a line along each cell's top and left, one mesh period apart", async () => { + const { CK, effect } = await make(); + const density = 3; + const { pitch, gap } = welcomeMesh(density); + const size = pitch * 2; + const surface = CK.MakeSurface(size, size); + if (!surface) throw new Error("no surface"); + const canvas = surface.getCanvas(); + canvas.scale(density, density); + const child = CK.Shader.MakeColor(CK.WHITE, CK.ColorSpace.SRGB); + const uniforms = [density, pitch, gap, 0.5]; + const order = Array.from({ length: effect.getUniformCount() }, (_, i) => + effect.getUniformName(i), + ); + expect(order).toEqual(["density", "pitch", "gap", "lineLight"]); + const paint = new CK.Paint(); + paint.setShader(effect.makeShaderWithChildren(uniforms, [child])); + canvas.drawPaint(paint); + surface.flush(); + + const pixels = canvas.readPixels(0, 0, { + width: size, + height: size, + colorType: CK.ColorType.RGBA_8888, + alphaType: CK.AlphaType.Premul, + colorSpace: CK.ColorSpace.SRGB, + }); + if (!pixels) throw new Error("no pixels"); + const red = (x: number, y: number) => pixels[(y * size + x) * 4]; + const row = Array.from({ length: size }, (_, x) => red(x, pitch - 1)); + const lit = 255; + // Across a cell's row: `gap` dim pixels, then lit ones, then it repeats. + expect(row.slice(0, gap).every((v) => v < 140)).toBe(true); + expect(row.slice(gap, pitch).every((v) => v === lit)).toBe(true); + expect(row.slice(pitch, pitch + gap).every((v) => v < 140)).toBe(true); + expect(row.slice(pitch + gap).every((v) => v === lit)).toBe(true); + // A line across the top of the cell stays dim the whole way. + expect(red(pitch - 1, 0)).toBeLessThan(140); + }); +}); diff --git a/mobile/tests/welcome-screen.test.tsx b/mobile/tests/welcome-screen.test.tsx new file mode 100644 index 00000000..16569eb8 --- /dev/null +++ b/mobile/tests/welcome-screen.test.tsx @@ -0,0 +1,101 @@ +import { type ReactNode } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { beforeEach, expect, it, vi } from "vitest"; +import WelcomeScreen from "../app/(auth)/welcome"; + +const mock = vi.hoisted(() => ({ + push: vi.fn(), + baseUrl: "https://musicfiles.example:8443", + presses: new Map void>(), +})); + +vi.mock("react-native", () => ({ + ActivityIndicator: () => null, + ScrollView: ({ children }: { children: ReactNode }) =>
{children}
, + View: ({ + children, + accessibilityLabel, + }: { + children: ReactNode; + accessibilityLabel?: string; + }) =>
{children}
, + Text: ({ children }: { children: ReactNode }) => {children}, + Pressable: ({ + children, + onPress, + accessibilityLabel, + }: { + children: ReactNode; + onPress: () => void; + accessibilityLabel: string; + }) => { + mock.presses.set(accessibilityLabel, onPress); + return ; + }, + useWindowDimensions: () => ({ width: 393, height: 852, fontScale: 1 }), +})); +vi.mock("expo-router", () => ({ useRouter: () => ({ push: mock.push }) })); +vi.mock("expo-image", () => ({ Image: () => })); +vi.mock("expo-haptics", () => ({ selectionAsync: vi.fn() })); +vi.mock("react-native-safe-area-context", () => ({ + useSafeAreaInsets: () => ({ top: 59, bottom: 34 }), +})); +vi.mock("react-native-reanimated", () => { + const builder = { + delay: () => builder, + duration: () => builder, + withInitialValues: () => builder, + }; + return { + default: { + View: ({ children }: { children: ReactNode }) =>
{children}
, + }, + FadeInDown: builder, + }; +}); +vi.mock("@music-library/core", () => ({ getBaseUrl: () => mock.baseUrl })); +vi.mock("../components/auth/welcome-art", () => ({ WELCOME_RECORD: 1 })); +vi.mock("../components/auth/welcome-hero", () => ({ WelcomeHero: () => null })); +vi.mock("../theme/theme", () => ({ + useTheme: () => ({ + scheme: "dark", + color: { bg: "#000", fg: "#fff", fgMuted: "#888", fgSubtle: "#ccc", accent: "#00f", onAccent: "#fff" }, + radius: { md: 10 }, + space: { xs: 4, sm: 8, md: 12, lg: 16, xl: 24 }, + }), +})); + +beforeEach(() => { + mock.push.mockClear(); + mock.presses.clear(); + mock.baseUrl = "https://musicfiles.example:8443"; +}); + +it("reads the headline as one sentence, whatever sits between its words", () => { + const html = renderToStaticMarkup(); + expect(html).toContain('aria-label="Your music, on your server"'); +}); + +it("names the server this build talks to", () => { + // React splits the sentence around the value, so compare the text alone. + const text = renderToStaticMarkup().replace(/|<[^>]+>/g, ""); + expect(text).toContain("Lumen plays your library from musicfiles.example on your phone"); +}); + +it("still reads properly when the server isn't configured", () => { + mock.baseUrl = "not a url"; + const text = renderToStaticMarkup().replace(/|<[^>]+>/g, ""); + expect(text).toContain("Lumen plays your library from your server on your phone"); +}); + +it("offers one primary way in and one quiet one", () => { + renderToStaticMarkup(); + expect([...mock.presses.keys()]).toEqual([ + "Sign in", + "Create an account with an invite", + ]); + mock.presses.get("Sign in")?.(); + expect(mock.push).toHaveBeenLastCalledWith("/(auth)/login"); + mock.presses.get("Create an account with an invite")?.(); + expect(mock.push).toHaveBeenLastCalledWith("/(auth)/register"); +}); diff --git a/mobile/theme/theme.tsx b/mobile/theme/theme.tsx index 1e093f8e..10aa31b5 100644 --- a/mobile/theme/theme.tsx +++ b/mobile/theme/theme.tsx @@ -32,8 +32,8 @@ export interface ThemePalette { /** Foreground (text/icon/spinner) on top of `accent`. */ onAccent: string; /** - * The app icon's purple, for brand moments like the welcome screen's hero. - * Controls and links keep using `accent`. Lifted in dark so it reads on black. + * The app icon's purple, which in the interface means Favorites and nothing + * else. Controls and links use `accent`. Lifted in dark so it reads on black. */ brand: string; /** Destructive / error (iOS systemRed, per scheme). */ From d4a99cf7df32419e1ce509fd9f51e852ffb0bb04 Mon Sep 17 00:00:00 2001 From: esson <55195336+githubesson@users.noreply.github.com> Date: Tue, 3 Nov 2026 02:49:12 +0000 Subject: [PATCH 2/6] Mobile: a polish pass over color, labels, spacing and copy Color: purple now only means Favorites. The Favorites header loses its wash, swipe-to-favorite matches the tab's purple, and the account avatar is flat. Replay's chart and genre bars are neutral with one accent on the busiest bucket and top genre. Quiet rows stop using the accent. Type: no uppercase tracked labels, weights stop at 600, nothing is under 11pt. The Favorites large title keeps 700 to match native large titles. Icons: the waveform replaces sparkles, since nothing here is AI. Spacing and shape: off-grid values move onto the 4pt grid, cards share a 16 radius and padding, and primary and secondary buttons share a 48pt height. Copy: labels use sentence case, `...` becomes an ellipsis, one-sentence captions lose their period, and filler eyebrows and repeated figures are gone. Co-Authored-By: Claude Opus 5.5 --- mobile/app/(tabs)/(favorites)/_layout.tsx | 6 +- mobile/app/(tabs)/(favorites)/index.tsx | 7 -- .../app/(tabs)/(library,browse)/_layout.tsx | 6 +- .../(tabs)/(library,browse)/albums/[id].tsx | 2 +- .../(tabs)/(library,browse)/albums/edit.tsx | 10 +-- .../(tabs)/(library,browse)/artists/[id].tsx | 2 +- mobile/app/(tabs)/(library,browse)/index.tsx | 4 +- .../(library,browse)/tidal-albums/[id].tsx | 2 +- .../(library,browse)/tidal-artists/[id].tsx | 4 +- .../(tabs)/(library,browse)/track/[id].tsx | 6 +- .../(tabs)/(library,browse)/track/edit.tsx | 12 +-- mobile/app/(tabs)/(library,browse)/upload.tsx | 6 +- mobile/app/(tabs)/(playlists)/[id].tsx | 4 +- mobile/app/(tabs)/(playlists)/_layout.tsx | 12 +-- mobile/app/(tabs)/(playlists)/edit/[id].tsx | 2 +- mobile/app/(tabs)/(playlists)/index.tsx | 2 +- mobile/app/(tabs)/(settings)/_layout.tsx | 10 +-- .../app/(tabs)/(settings)/admin-invites.tsx | 2 +- .../app/(tabs)/(settings)/admin-library.tsx | 4 +- .../(tabs)/(settings)/admin-new-invite.tsx | 8 +- mobile/app/(tabs)/(settings)/admin-tidal.tsx | 14 +-- mobile/app/(tabs)/(settings)/download-log.tsx | 2 +- mobile/app/(tabs)/(settings)/downloads.tsx | 4 +- mobile/app/now-playing.tsx | 8 +- mobile/app/playlist-picker.tsx | 8 +- mobile/app/share-track.tsx | 12 +-- .../add-confirmation-overlay.tsx | 2 +- mobile/components/album-header.tsx | 2 +- mobile/components/artist/artist-hero.tsx | 4 +- mobile/components/dock/dock-mini-player.tsx | 26 +++--- mobile/components/eyebrow.tsx | 10 +-- .../favorites/favorites-list-header.tsx | 4 +- .../components/favorites/favorites-list.tsx | 4 +- mobile/components/favorites/mode-wash.tsx | 90 ------------------- mobile/components/form-field.tsx | 2 +- .../components/library/browse-links-card.tsx | 6 +- .../components/library/ranked-track-row.tsx | 3 +- mobile/components/library/search-results.tsx | 2 +- mobile/components/library/welcome-card.tsx | 85 +++++------------- mobile/components/now-playing/hero-meta.tsx | 2 +- .../now-playing/lyrics-language-selector.tsx | 10 +-- .../components/now-playing/lyrics-section.tsx | 20 ++--- mobile/components/now-playing/queue-row.tsx | 2 +- .../components/now-playing/queue-section.tsx | 2 +- mobile/components/ota-updates.tsx | 4 +- mobile/components/playback-device-button.tsx | 22 ++--- .../components/playlist-picker/picker-row.tsx | 6 +- .../playlist-picker/search-field.tsx | 6 +- .../components/playlist-picker/track-card.tsx | 4 +- .../playlists/playlist-cover-field.tsx | 2 +- .../playlists/playlist-detail-header.tsx | 26 +++--- mobile/components/primitives.tsx | 8 +- .../components/remote-control-indicator.tsx | 4 +- mobile/components/replay/activity-chart.tsx | 22 +++-- mobile/components/replay/genre-list.tsx | 15 ++-- mobile/components/replay/period-picker.tsx | 2 +- .../replay/replay-action-button.tsx | 13 +-- .../components/replay/replay-empty-state.tsx | 11 ++- mobile/components/replay/replay-hero.tsx | 19 +--- mobile/components/replay/summary-grid.tsx | 21 ++--- mobile/components/section.tsx | 6 +- mobile/components/settings/account-card.tsx | 14 ++- .../components/settings/appearance-tile.tsx | 12 +-- mobile/components/settings/device-card.tsx | 2 +- mobile/components/settings/lastfm-row.tsx | 2 +- mobile/components/settings/replay-row.tsx | 4 +- mobile/components/settings/settings-card.tsx | 9 +- .../components/settings/smooth-mix-tile.tsx | 4 +- .../components/share/share-action-button.tsx | 6 +- mobile/components/share/share-panel.tsx | 2 +- mobile/components/share/snippet-panel.tsx | 6 +- .../share/story-background-crop-editor.tsx | 6 +- .../share/story-share-menu-button.tsx | 10 +-- mobile/components/share/track-header.tsx | 2 +- mobile/components/swipe/favorite-mark.tsx | 2 +- mobile/components/swipe/removal-strip.tsx | 4 +- mobile/components/swipe/swipe-action.tsx | 6 +- .../components/swipe/swipeable-track-row.tsx | 8 +- mobile/components/track-actions-menu.tsx | 22 ++--- mobile/components/track-info/info-grid.tsx | 4 +- mobile/components/track-info/player-hero.tsx | 4 +- mobile/tests/playlist-picker-screen.test.tsx | 2 +- mobile/tests/queue-section.test.tsx | 2 +- mobile/tests/replay-activity-chart.test.tsx | 53 +++++++++++ mobile/tests/search-failures.test.tsx | 12 +-- mobile/tests/swipe-actions.test.ts | 2 +- mobile/tests/tidal-album-versions.test.tsx | 8 +- mobile/tests/track-info-screen.test.tsx | 2 +- 88 files changed, 363 insertions(+), 473 deletions(-) delete mode 100644 mobile/components/favorites/mode-wash.tsx create mode 100644 mobile/tests/replay-activity-chart.test.tsx diff --git a/mobile/app/(tabs)/(favorites)/_layout.tsx b/mobile/app/(tabs)/(favorites)/_layout.tsx index 32970aa9..9447ee9c 100644 --- a/mobile/app/(tabs)/(favorites)/_layout.tsx +++ b/mobile/app/(tabs)/(favorites)/_layout.tsx @@ -7,11 +7,11 @@ export default function FavoritesStackLayout() { ); diff --git a/mobile/app/(tabs)/(favorites)/index.tsx b/mobile/app/(tabs)/(favorites)/index.tsx index eac294b9..64ab45c4 100644 --- a/mobile/app/(tabs)/(favorites)/index.tsx +++ b/mobile/app/(tabs)/(favorites)/index.tsx @@ -10,7 +10,6 @@ import { useFavoritesListModel, } from "../../../components/favorites/favorites-list"; import { FavoritesNavBar } from "../../../components/favorites/favorites-nav-bar"; -import { ModeWash } from "../../../components/favorites/mode-wash"; import { useFavoritesQuery } from "../../../context/favorites"; import { DEFAULT_HEADER_GEOMETRY, @@ -184,12 +183,6 @@ export default function FavoritesScreen() { }} style={{ flex: 1, backgroundColor: theme.color.bg }} > - ); diff --git a/mobile/app/(tabs)/(library,browse)/albums/edit.tsx b/mobile/app/(tabs)/(library,browse)/albums/edit.tsx index 8fcd2781..09cbe5ea 100644 --- a/mobile/app/(tabs)/(library,browse)/albums/edit.tsx +++ b/mobile/app/(tabs)/(library,browse)/albums/edit.tsx @@ -66,7 +66,7 @@ export default function AlbumEditScreen() { ); @@ -123,7 +123,7 @@ function AlbumEditor({ id, album }: { id: string; album: Album }) { quality: 0.9, }); } catch { - setError("Couldn't open the photo library."); + setError("Couldn't open the photo library"); return; } if (result.canceled || result.assets.length === 0) return; @@ -205,7 +205,7 @@ function AlbumEditor({ id, album }: { id: string; album: Album }) { <> ( void onSave()} /> ), @@ -296,7 +296,7 @@ function AlbumEditor({ id, album }: { id: string; album: Album }) { ); diff --git a/mobile/app/(tabs)/(library,browse)/index.tsx b/mobile/app/(tabs)/(library,browse)/index.tsx index 3d93c511..8715a97f 100644 --- a/mobile/app/(tabs)/(library,browse)/index.tsx +++ b/mobile/app/(tabs)/(library,browse)/index.tsx @@ -346,7 +346,7 @@ export default function HomeScreen() { )} {hasReplay && topAlbums.length > 0 && ( -
+
{topAlbums.map((a) => ( 0 && (
diff --git a/mobile/app/(tabs)/(library,browse)/tidal-albums/[id].tsx b/mobile/app/(tabs)/(library,browse)/tidal-albums/[id].tsx index 7fb14240..d5ce8647 100644 --- a/mobile/app/(tabs)/(library,browse)/tidal-albums/[id].tsx +++ b/mobile/app/(tabs)/(library,browse)/tidal-albums/[id].tsx @@ -178,7 +178,7 @@ export default function TidalAlbumDetailScreen() { ); diff --git a/mobile/app/(tabs)/(library,browse)/tidal-artists/[id].tsx b/mobile/app/(tabs)/(library,browse)/tidal-artists/[id].tsx index 252ec5a8..2f7146bd 100644 --- a/mobile/app/(tabs)/(library,browse)/tidal-artists/[id].tsx +++ b/mobile/app/(tabs)/(library,browse)/tidal-artists/[id].tsx @@ -63,7 +63,7 @@ export default function TidalArtistScreen() { ); const onTrackPress = usePlayQueue(tracks); const warning = data?.warnings?.join(" "); - const problem = query.isError ? "Couldn't load artist." : warning; + const problem = query.isError ? "Couldn't load artist" : warning; const artistName = data?.artist?.name || name || "TIDAL artist"; // Artists without a TIDAL picture borrow their top track's cover. const imageUri = artistImageUrl( @@ -175,7 +175,7 @@ export default function TidalArtistScreen() { query.isLoading ? ( ) : !problem && releases.length === 0 ? ( - + ) : null } contentInsetAdjustmentBehavior="automatic" diff --git a/mobile/app/(tabs)/(library,browse)/track/[id].tsx b/mobile/app/(tabs)/(library,browse)/track/[id].tsx index 5ef52b18..d8a1d6ab 100644 --- a/mobile/app/(tabs)/(library,browse)/track/[id].tsx +++ b/mobile/app/(tabs)/(library,browse)/track/[id].tsx @@ -135,7 +135,7 @@ export default function TrackInfoScreen() { return ; } if (trackQuery.isError || !t || !item) { - return ; + return ; } const view = trackInfoRows( @@ -149,7 +149,7 @@ export default function TrackInfoScreen() { <> {t.unavailable ? ( - Removed from TIDAL, so it can't play. + Removed from TIDAL, so it can't play ) : null} {view.note ? {view.note} : null} diff --git a/mobile/app/(tabs)/(library,browse)/track/edit.tsx b/mobile/app/(tabs)/(library,browse)/track/edit.tsx index d92d3403..99b22141 100644 --- a/mobile/app/(tabs)/(library,browse)/track/edit.tsx +++ b/mobile/app/(tabs)/(library,browse)/track/edit.tsx @@ -30,7 +30,7 @@ import { EmptyState, retryAction } from "../../../../components/empty-state"; /** * Admin-only track metadata editor. Reached from the track context menu's - * "Edit Metadata" action. Loads the track, lets an admin rewrite the same + * "Edit metadata" action. Loads the track, lets an admin rewrite the same * fields the web edit dialog exposes, and PATCHes only what actually changed. */ export default function TrackEditScreen() { @@ -59,7 +59,7 @@ export default function TrackEditScreen() { ); @@ -67,7 +67,7 @@ export default function TrackEditScreen() { if (!canEdit) { return ( <> - + ( void onSave()} /> ), @@ -147,7 +147,7 @@ function TrackEditor({ id, track }: { id: string; track: TrackDetail }) { autoCapitalize="words" /> - + setField("albumTitle", value)} @@ -156,7 +156,7 @@ function TrackEditor({ id, track }: { id: string; track: TrackDetail }) { ; } if (playlistQuery.isError || tracksQuery.isError || !playlist) { - return { void playlistQuery.refetch(); void tracksQuery.refetch(); } }} />; } diff --git a/mobile/app/(tabs)/(playlists)/_layout.tsx b/mobile/app/(tabs)/(playlists)/_layout.tsx index 1ac7aa2c..e15db9a6 100644 --- a/mobile/app/(tabs)/(playlists)/_layout.tsx +++ b/mobile/app/(tabs)/(playlists)/_layout.tsx @@ -13,14 +13,14 @@ export default function PlaylistsStackLayout() { name="new" options={{ headerLargeTitle: false, - title: "New Playlist", + title: "New playlist", }} /> ); diff --git a/mobile/app/(tabs)/(playlists)/edit/[id].tsx b/mobile/app/(tabs)/(playlists)/edit/[id].tsx index 0445cc53..fda1ee50 100644 --- a/mobile/app/(tabs)/(playlists)/edit/[id].tsx +++ b/mobile/app/(tabs)/(playlists)/edit/[id].tsx @@ -133,7 +133,7 @@ function PlaylistEditor({ id, playlist }: { id: string; playlist: Playlist }) { {forbidden ? ( - + ) : ( <> {permissions.canChangeCover ? ( diff --git a/mobile/app/(tabs)/(playlists)/index.tsx b/mobile/app/(tabs)/(playlists)/index.tsx index 6b4495e1..037594dc 100644 --- a/mobile/app/(tabs)/(playlists)/index.tsx +++ b/mobile/app/(tabs)/(playlists)/index.tsx @@ -217,7 +217,7 @@ function InviteRow({ return ( ); diff --git a/mobile/app/(tabs)/(settings)/admin-invites.tsx b/mobile/app/(tabs)/(settings)/admin-invites.tsx index 0ae0c106..ebc803b6 100644 --- a/mobile/app/(tabs)/(settings)/admin-invites.tsx +++ b/mobile/app/(tabs)/(settings)/admin-invites.tsx @@ -124,7 +124,7 @@ export default function AdminInvitesScreen() { style={{ paddingVertical: 0 }} message={errorMessage( invitesQuery.error, - "Couldn't load invitations.", + "Couldn't load invitations", )} /> ) : ( - Scan all enabled roots for new or changed tracks. + Scan all enabled roots for new or changed tracks )} diff --git a/mobile/app/(tabs)/(settings)/admin-new-invite.tsx b/mobile/app/(tabs)/(settings)/admin-new-invite.tsx index 4eb8b515..21009c8e 100644 --- a/mobile/app/(tabs)/(settings)/admin-new-invite.tsx +++ b/mobile/app/(tabs)/(settings)/admin-new-invite.tsx @@ -209,7 +209,7 @@ export default function AdminNewInviteScreen() { > Invite ready @@ -224,7 +224,7 @@ export default function AdminNewInviteScreen() { @@ -278,7 +278,7 @@ export default function AdminNewInviteScreen() { diff --git a/mobile/app/(tabs)/(settings)/admin-tidal.tsx b/mobile/app/(tabs)/(settings)/admin-tidal.tsx index 4e5f4cb9..d88f31fd 100644 --- a/mobile/app/(tabs)/(settings)/admin-tidal.tsx +++ b/mobile/app/(tabs)/(settings)/admin-tidal.tsx @@ -204,12 +204,12 @@ export default function AdminTidalScreen() { - + Waiting for TIDAL - This page updates automatically after approval. + This page updates automatically after approval @@ -314,12 +314,12 @@ function StatusCard({ /> )} - + {PROXY_STATE_LABELS[state]} - TIDAL credentials stay on the server. + TIDAL credentials stay on the server @@ -401,10 +401,10 @@ function StatusValue({ borderRadius: theme.radius.sm, borderCurve: "continuous", padding: theme.space.sm, - gap: 3, + gap: 4, }} > - + {label} @@ -437,7 +437,7 @@ function AccountCard({ }} > - + {tidalAccountLabel(account)} diff --git a/mobile/app/(tabs)/(settings)/download-log.tsx b/mobile/app/(tabs)/(settings)/download-log.tsx index c7bbf479..61d795fc 100644 --- a/mobile/app/(tabs)/(settings)/download-log.tsx +++ b/mobile/app/(tabs)/(settings)/download-log.tsx @@ -383,7 +383,7 @@ function ActionRow({ alignItems: "center", gap: 12, paddingHorizontal: theme.space.md, - paddingVertical: 14, + paddingVertical: 12, borderTopWidth: border ? StyleSheet.hairlineWidth : 0, borderTopColor: theme.color.separator, backgroundColor: pressed ? theme.color.bgElev2 : "transparent", diff --git a/mobile/app/(tabs)/(settings)/downloads.tsx b/mobile/app/(tabs)/(settings)/downloads.tsx index 8fe11dbf..c094d90f 100644 --- a/mobile/app/(tabs)/(settings)/downloads.tsx +++ b/mobile/app/(tabs)/(settings)/downloads.tsx @@ -36,7 +36,7 @@ const HERO_COVER_MAX = 160; // "Custom" is the download order, newest first. No "Plays": the stored // snapshots carry no play counts. const SORT_OPTIONS: SortMenuOption[] = [ - { key: "custom", label: "Recently Downloaded", symbol: "arrow.down.circle" }, + { key: "custom", label: "Recently downloaded", symbol: "arrow.down.circle" }, { key: "title", label: "Title" }, { key: "duration", label: "Length" }, ]; @@ -184,7 +184,7 @@ const styles = StyleSheet.create({ }, title: { fontSize: 24, - fontWeight: "700", + fontWeight: "600", letterSpacing: -0.2, textAlign: "center", }, diff --git a/mobile/app/now-playing.tsx b/mobile/app/now-playing.tsx index 106b91ac..5e6ea192 100644 --- a/mobile/app/now-playing.tsx +++ b/mobile/app/now-playing.tsx @@ -153,9 +153,9 @@ export default function NowPlayingScreen() { completedTranslation.targetLanguage === targetLanguage; const translationActionLabel = selectedPairMatches ? completedTranslation.visible - ? "Hide Translation" - : "Show Translation" - : "Translate Lyrics"; + ? "Hide translation" + : "Show translation" + : "Translate lyrics"; const previousTrackIdRef = useRef(null); const previousTrackIndexRef = useRef(index); // These refs hold the last committed track solely to choose the direction of @@ -402,7 +402,7 @@ export default function NowPlayingScreen() { alignItems: "center", }} > - No track loaded. + No track loaded ); } diff --git a/mobile/app/playlist-picker.tsx b/mobile/app/playlist-picker.tsx index 28152a49..a7a45049 100644 --- a/mobile/app/playlist-picker.tsx +++ b/mobile/app/playlist-picker.tsx @@ -205,13 +205,13 @@ export default function PlaylistPickerScreen() { ListEmptyComponent={ // Only a link can open the sheet without a track. !trackId ? ( - + ) : !playlists && playlistsQuery.isError ? ( - + ) : !playlists ? ( ) : ( - + ) } /> @@ -252,7 +252,7 @@ const styles = StyleSheet.create({ alignItems: "center", gap: 10, paddingTop: 24, - paddingHorizontal: 14, + paddingHorizontal: 16, }, close: { width: 44, diff --git a/mobile/app/share-track.tsx b/mobile/app/share-track.tsx index 06896745..eb09a88d 100644 --- a/mobile/app/share-track.tsx +++ b/mobile/app/share-track.tsx @@ -433,7 +433,7 @@ export default function ShareTrackScreen() { }, [getShareUrl, picked, trackQuery.data]); // Downloads the snippet's generated preview video (the MP4 chat apps embed) - // and hands it to the share sheet, whose "Save Video" puts it in Photos. + // and hands it to the share sheet, whose "Save video" puts it in Photos. const saveVideo = useCallback(async () => { if (!picked || videoBusy) return; let url: string; @@ -544,7 +544,7 @@ export default function ShareTrackScreen() { ) : trackQuery.isError || !trackQuery.data ? ( - Couldn't load track. + Couldn't load track ) : ( @@ -595,21 +595,21 @@ export default function ShareTrackScreen() { onPickCustom={() => void pickCustomBackground()} /> void shareLink()} /> void saveVideo()} /> void copyLink()} @@ -619,7 +619,7 @@ export default function ShareTrackScreen() { )} - Share Track + Share track - {confirmation.created ? "New Playlist" : "Added"} + {confirmation.created ? "New playlist" : "Added"} diff --git a/mobile/components/album-header.tsx b/mobile/components/album-header.tsx index bb17bb41..fb0ac232 100644 --- a/mobile/components/album-header.tsx +++ b/mobile/components/album-header.tsx @@ -76,7 +76,7 @@ export function AlbumHeader({ diff --git a/mobile/components/dock/dock-mini-player.tsx b/mobile/components/dock/dock-mini-player.tsx index 3f0d8726..3a33877f 100644 --- a/mobile/components/dock/dock-mini-player.tsx +++ b/mobile/components/dock/dock-mini-player.tsx @@ -496,38 +496,38 @@ function PadTrackActionsButton({ track }: { track: TrackListItem }) { { label: "Play", onPress: actions.play }, { label: actions.favorite - ? "Remove from Favorites" - : "Add to Favorites", + ? "Remove from favorites" + : "Add to favorites", onPress: actions.toggleFavorite, }, - { label: "Track Info", onPress: actions.openInfo }, - ...(actions.canShare ? [{ label: "Share...", onPress: actions.openShare }] : []), - { label: "Add to Playlist...", onPress: actions.openPlaylistPicker }, + { label: "Track info", onPress: actions.openInfo }, + ...(actions.canShare ? [{ label: "Share…", onPress: actions.openShare }] : []), + { label: "Add to playlist…", onPress: actions.openPlaylistPicker }, { - label: actions.downloading ? "Downloading..." : "Download File...", + label: actions.downloading ? "Downloading…" : "Download file…", onPress: actions.download, disabled: actions.downloading, }, ]; if (actions.hasAlbum) { - items.push({ label: "View Album", onPress: actions.openAlbum }); + items.push({ label: "View album", onPress: actions.openAlbum }); } if (actions.canEditMetadata) { items.push({ - label: "Edit Metadata", + label: "Edit metadata", onPress: actions.openEditMetadata, }); } if (actions.hasEditableAlbum) { items.push({ - label: "Edit Album & Cover", + label: "Edit album & cover", onPress: actions.openEditAlbum, }); } if (actions.owned) { items.push({ - label: actions.deleting ? "Deleting..." : "Delete from My Library", + label: actions.deleting ? "Deleting…" : "Delete from my library", onPress: actions.deleteTrack, destructive: true, disabled: actions.deleting, @@ -622,7 +622,7 @@ const styles = StyleSheet.create({ }, padSideClusterWide: { width: 250, - gap: 14, + gap: 12, }, padSideClusterCompact: { width: 154, @@ -654,7 +654,7 @@ const styles = StyleSheet.create({ }, padTitle: { fontSize: 15, - fontWeight: "700", + fontWeight: "600", }, padArtist: { marginTop: 2, @@ -666,7 +666,7 @@ const styles = StyleSheet.create({ flexDirection: "row", alignItems: "center", justifyContent: "flex-end", - gap: 14, + gap: 12, }, padActionClusterCompact: { width: 72, diff --git a/mobile/components/eyebrow.tsx b/mobile/components/eyebrow.tsx index 3d653fda..42da6f27 100644 --- a/mobile/components/eyebrow.tsx +++ b/mobile/components/eyebrow.tsx @@ -2,11 +2,7 @@ import { type ReactNode } from "react"; import { Text, type StyleProp, type TextStyle } from "react-native"; import { useTheme } from "../theme/theme"; -/** - * Muted uppercase micro-label above section titles and feature cards (home - * shelves, replay sections). Smaller and wider-tracked than the form - * `SectionLabel`. - */ +/** Muted caption above section titles and feature cards (home shelves). */ export function Eyebrow({ style, children, @@ -20,9 +16,7 @@ export function Eyebrow({ style={[ { color: theme.color.fgMuted, - fontSize: 11, - letterSpacing: 0.6, - textTransform: "uppercase", + fontSize: 13, }, style, ]} diff --git a/mobile/components/favorites/favorites-list-header.tsx b/mobile/components/favorites/favorites-list-header.tsx index ac861ca4..8ea4d6d9 100644 --- a/mobile/components/favorites/favorites-list-header.tsx +++ b/mobile/components/favorites/favorites-list-header.tsx @@ -25,12 +25,12 @@ import type { ThemeTokens } from "../../theme/theme"; // order, newest first. const SORT_OPTIONS: Record = { favorites: [ - { key: "custom", label: "Recently Added", symbol: "heart" }, + { key: "custom", label: "Recently added", symbol: "heart" }, { key: "title", label: "Title" }, { key: "duration", label: "Length" }, ], recent: [ - { key: "custom", label: "Last Played", symbol: "clock" }, + { key: "custom", label: "Last played", symbol: "clock" }, { key: "title", label: "Title" }, { key: "duration", label: "Length" }, ], diff --git a/mobile/components/favorites/favorites-list.tsx b/mobile/components/favorites/favorites-list.tsx index 146324e7..7ae272f5 100644 --- a/mobile/components/favorites/favorites-list.tsx +++ b/mobile/components/favorites/favorites-list.tsx @@ -124,11 +124,11 @@ export function useFavoritesListModel( const EMPTY_MESSAGES: Record = { favorites: { - error: "Couldn't load favorites.", + error: "Couldn't load favorites", empty: "No favorites yet. Tap the heart on a track to save it here.", }, recent: { - error: "Couldn't load recent plays.", + error: "Couldn't load recent plays", empty: "No recent plays.", }, }; diff --git a/mobile/components/favorites/mode-wash.tsx b/mobile/components/favorites/mode-wash.tsx deleted file mode 100644 index 913b2a36..00000000 --- a/mobile/components/favorites/mode-wash.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { useEffect } from "react"; -import { StyleSheet } from "react-native"; -import Animated, { - Easing, - useAnimatedStyle, - useSharedValue, - withTiming, - type SharedValue, -} from "react-native-reanimated"; -import { modeColor } from "./mode-switch"; -import type { FavoritesMode } from "../../lib/favorites-header"; -import { withAlpha, type ThemeTokens } from "../../theme/theme"; - -const WASH_HEIGHT = 430; -const WASH_FADE = { duration: 350, easing: Easing.linear } as const; - -function washGradient(color: string, alpha: number): string { - return `radial-gradient(130% 75% at 20% 0%, ${withAlpha(color, alpha)} 0%, ${withAlpha(color, 0)} 72%)`; -} - -/** - * A faint wash of the list's colour behind the header. It sits behind both - * lists (they're transparent) so switching cross-fades purple and blue rather - * than snapping, and it scrolls with the active list. A pull to refresh leaves - * it pinned to the top instead of opening a gap above it. - */ -export function ModeWash({ - theme, - mode, - offset, - topInset, -}: { - theme: ThemeTokens; - mode: FavoritesMode; - /** The active list's raw content offset (rest is `-topInset`). */ - offset: SharedValue; - topInset: number; -}) { - const recent = useSharedValue(mode === "recent" ? 1 : 0); - useEffect(() => { - recent.set(withTiming(mode === "recent" ? 1 : 0, WASH_FADE)); - }, [mode, recent]); - - const followStyle = useAnimatedStyle(() => ({ - transform: [{ translateY: -Math.max(0, offset.value + topInset) }], - })); - const favoritesStyle = useAnimatedStyle(() => ({ opacity: 1 - recent.value })); - const recentStyle = useAnimatedStyle(() => ({ opacity: recent.value })); - - // Lighter on white, where the same tint reads much stronger. - const dark = theme.scheme === "dark"; - return ( - - - - - ); -} - -const styles = StyleSheet.create({ - wash: { - position: "absolute", - top: 0, - left: 0, - right: 0, - height: WASH_HEIGHT, - }, -}); diff --git a/mobile/components/form-field.tsx b/mobile/components/form-field.tsx index f75cf741..4b42c468 100644 --- a/mobile/components/form-field.tsx +++ b/mobile/components/form-field.tsx @@ -81,7 +81,7 @@ export function FormTextInput( color: theme.color.fg, borderRadius: theme.radius.md, borderCurve: "continuous", - paddingHorizontal: 14, + paddingHorizontal: 16, paddingVertical: 12, fontSize: 17, }, diff --git a/mobile/components/library/browse-links-card.tsx b/mobile/components/library/browse-links-card.tsx index 7806b49d..f49603ed 100644 --- a/mobile/components/library/browse-links-card.tsx +++ b/mobile/components/library/browse-links-card.tsx @@ -44,7 +44,7 @@ export function BrowseLinksCard({ ); } -/** One disclosure row inside the card: accent icon, label, chevron, and an optional inset hairline above. */ +/** One disclosure row inside the card: icon, label, chevron, and an optional inset hairline above. */ function BrowseLinkRow({ icon, label, @@ -68,7 +68,7 @@ function BrowseLinkRow({ flexDirection: "row", alignItems: "center", paddingHorizontal: theme.space.md, - paddingVertical: 13, + paddingVertical: 12, gap: theme.space.md, backgroundColor: pressed ? theme.color.bgElev2 : "transparent", })} @@ -77,7 +77,7 @@ function BrowseLinkRow({ name={icon} size={20} weight="medium" - tintColor={theme.color.accent} + tintColor={theme.color.fg} /> {label} diff --git a/mobile/components/library/ranked-track-row.tsx b/mobile/components/library/ranked-track-row.tsx index 43165b08..4cc1a772 100644 --- a/mobile/components/library/ranked-track-row.tsx +++ b/mobile/components/library/ranked-track-row.tsx @@ -74,8 +74,7 @@ export function RankedTrackRow({ width: 30, color: rank === 1 ? theme.color.accent : theme.color.fgMuted, fontSize: 22, - fontWeight: "700", - letterSpacing: -0.5, + fontWeight: "600", fontVariant: ["tabular-nums"], textAlign: "center", }} diff --git a/mobile/components/library/search-results.tsx b/mobile/components/library/search-results.tsx index edb714cf..6ae95694 100644 --- a/mobile/components/library/search-results.tsx +++ b/mobile/components/library/search-results.tsx @@ -154,7 +154,7 @@ export function SearchResults({ search }: { search: string }) { } ListEmptyComponent={ !search ? ( - + ) : query.isLoading ? ( ) : query.isError || warnings.length === 0 ? ( diff --git a/mobile/components/library/welcome-card.tsx b/mobile/components/library/welcome-card.tsx index 661c1cfe..fb29af5a 100644 --- a/mobile/components/library/welcome-card.tsx +++ b/mobile/components/library/welcome-card.tsx @@ -1,17 +1,12 @@ -import { - Pressable, - Text, - View, - type StyleProp, - type ViewStyle, -} from "react-native"; +import { Text, View, type StyleProp, type ViewStyle } from "react-native"; import { SymbolView } from "expo-symbols"; +import { PrimaryButton, SecondaryButton } from "../buttons"; import { useTheme } from "../../theme/theme"; /** - * First-run card shown when the account has nothing personal yet (no plays, - * no favorites): a short pitch plus pill buttons into browse and upload, - * instead of a page of empty shelves. + * First-run block shown when the account has nothing personal yet (no plays, + * no favorites): one line plus buttons into browse and upload, instead of a + * page of empty shelves. */ export function WelcomeCard({ onBrowse, @@ -27,26 +22,23 @@ export function WelcomeCard({ - + - Make this place yours + Nothing played yet - Play a few songs and favorite the keepers — this page fills in with - your recents, heavy rotation, and daily rediscoveries. + Songs you play and favorite show up here - - - + + + + + + + ); } - -/** Compact pill CTA used only inside the welcome card. */ -function PillButton({ - label, - background, - color, - onPress, -}: { - label: string; - background: string; - color: string; - onPress: () => void; -}) { - return ( - ({ - backgroundColor: background, - borderRadius: 999, - paddingHorizontal: 18, - paddingVertical: 10, - opacity: pressed ? 0.7 : 1, - })} - > - {label} - - ); -} diff --git a/mobile/components/now-playing/hero-meta.tsx b/mobile/components/now-playing/hero-meta.tsx index 2e2a61b0..61c08ade 100644 --- a/mobile/components/now-playing/hero-meta.tsx +++ b/mobile/components/now-playing/hero-meta.tsx @@ -42,7 +42,7 @@ export function HeroMeta({ style={{ color: theme.color.fg, fontSize: 22, - fontWeight: "700", + fontWeight: "600", letterSpacing: -0.3, flexShrink: 1, }} diff --git a/mobile/components/now-playing/lyrics-language-selector.tsx b/mobile/components/now-playing/lyrics-language-selector.tsx index 2f0c7cc2..0cd7287c 100644 --- a/mobile/components/now-playing/lyrics-language-selector.tsx +++ b/mobile/components/now-playing/lyrics-language-selector.tsx @@ -45,7 +45,7 @@ const TRANSLATION_LANGUAGES: readonly TranslationLanguage[] = [ type ExpandedSelector = "source" | "target" | null; function languageLabel(code: string | null): string { - if (code === null) return "Auto Detect"; + if (code === null) return "Auto detect"; return ( TRANSLATION_LANGUAGES.find((language) => language.code === code)?.label ?? code @@ -121,7 +121,7 @@ export function LyricsLanguageSelector({ title="To" value={ targetLanguage === null - ? "Select Language" + ? "Select language" : languageLabel(targetLanguage) } expanded={expanded === "target"} @@ -245,7 +245,7 @@ function LanguageOptions({ const options: readonly ( TranslationLanguage | { code: null; label: string } )[] = includeAutoDetect - ? [{ code: null, label: "Auto Detect" }, ...TRANSLATION_LANGUAGES] + ? [{ code: null, label: "Auto detect" }, ...TRANSLATION_LANGUAGES] : TRANSLATION_LANGUAGES; return ( @@ -360,11 +360,11 @@ const styles = StyleSheet.create({ minHeight: 42, borderRadius: 21, marginTop: 10, - paddingHorizontal: 14, + paddingHorizontal: 16, flexDirection: "row", alignItems: "center", justifyContent: "center", - gap: 7, + gap: 8, }, actionLabel: { fontSize: 14, diff --git a/mobile/components/now-playing/lyrics-section.tsx b/mobile/components/now-playing/lyrics-section.tsx index 00dcde48..ccd41589 100644 --- a/mobile/components/now-playing/lyrics-section.tsx +++ b/mobile/components/now-playing/lyrics-section.tsx @@ -428,11 +428,9 @@ export const LyricsSection = forwardRef< line.section ? { color: theme.color.accent, - fontSize: 12, - fontWeight: "700", - letterSpacing: 0.66, - lineHeight: 17, - textTransform: "uppercase", + fontSize: 13, + fontWeight: "600", + lineHeight: 18, } : { color: theme.color.fgSubtle, @@ -453,7 +451,7 @@ export const LyricsSection = forwardRef< color: theme.color.fgMuted, fontSize: 14, lineHeight: 20, - paddingTop: 3, + paddingTop: 4, }} > {activeTranslation.lines[index]} @@ -600,12 +598,11 @@ const SyncedLyricLine = memo(function SyncedLyricLine({ ); const baseLineText: TextStyle = { - fontSize: line.section ? 12 : state === "active" ? 19 : 18, + fontSize: line.section ? 13 : state === "active" ? 19 : 18, fontWeight: line.section ? "700" : state === "active" ? "500" : "400", lineHeight: line.section ? 18 : 27, - // Frontend section letter-spacing 0.055em ≈ 0.66 at 12px; body -0.01em. - letterSpacing: line.section ? 0.66 : -0.19, - textTransform: line.section ? "uppercase" : "none", + // Body lines keep the frontend's -0.01em; section labels stay untracked. + letterSpacing: line.section ? 0 : -0.19, }; return ( @@ -645,9 +642,8 @@ const SyncedLyricLine = memo(function SyncedLyricLine({ color: theme.color.fgMuted, fontSize: 14, fontWeight: "400", - letterSpacing: -0.08, lineHeight: 20, - paddingTop: 3, + paddingTop: 4, }} > {translatedText} diff --git a/mobile/components/now-playing/queue-row.tsx b/mobile/components/now-playing/queue-row.tsx index 936fde15..856b1265 100644 --- a/mobile/components/now-playing/queue-row.tsx +++ b/mobile/components/now-playing/queue-row.tsx @@ -65,7 +65,7 @@ function QueueRowImpl({ transitionMs={120} priority="normal" /> - + - Nothing else queued. + Nothing else queued } diff --git a/mobile/components/ota-updates.tsx b/mobile/components/ota-updates.tsx index 82d6e38c..a08b6cd6 100644 --- a/mobile/components/ota-updates.tsx +++ b/mobile/components/ota-updates.tsx @@ -186,7 +186,7 @@ export function OtaUpdateRows() { styles.row, { paddingHorizontal: SETTINGS_INSET, - paddingVertical: 14, + paddingVertical: 12, borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: theme.color.separator, backgroundColor: pressed ? theme.color.bgElev2 : "transparent", @@ -251,7 +251,7 @@ function StatusTrailing({ style={{ backgroundColor: theme.color.accent, borderRadius: 11, - paddingHorizontal: 9, + paddingHorizontal: 8, height: 22, justifyContent: "center", }} diff --git a/mobile/components/playback-device-button.tsx b/mobile/components/playback-device-button.tsx index 423f2fab..e816eef5 100644 --- a/mobile/components/playback-device-button.tsx +++ b/mobile/components/playback-device-button.tsx @@ -88,7 +88,7 @@ export function PlaybackDeviceButton({ swiftFrame({ width: size, height: size }), ]} > -
+
"); expect(out).toContain(""); - expect(out).toContain("Removed from TIDAL, so it can't play."); + expect(out).toContain("Removed from TIDAL, so it can't play"); }); it("doesn't ask for the album after TIDAL failed, and never twice", () => { From c2a48373b49d995f8aca048ba70e7b34a77d076a Mon Sep 17 00:00:00 2001 From: esson <55195336+githubesson@users.noreply.github.com> Date: Tue, 3 Nov 2026 02:49:12 +0000 Subject: [PATCH 3/6] Mobile: Replay's artist tiles only open what exists A TIDAL-only artist has no library page, but the tile always opened one. It now follows core's topArtistAction like desktop: open the page if there is one, else play their top tracks, else do nothing. Tiles show a cover from the artist's top tracks instead of a random color, with a person glyph when there's none. Also drops Replay's filler eyebrows, in the same file. Co-Authored-By: Claude Opus 5.5 --- mobile/app/(tabs)/(settings)/replay.tsx | 88 ++++++---- .../components/replay/ranked-shelf-tile.tsx | 74 ++++---- mobile/components/shelf-tile.tsx | 6 +- mobile/tests/replay-artist-tiles.test.tsx | 159 ++++++++++++++++++ 4 files changed, 256 insertions(+), 71 deletions(-) create mode 100644 mobile/tests/replay-artist-tiles.test.tsx diff --git a/mobile/app/(tabs)/(settings)/replay.tsx b/mobile/app/(tabs)/(settings)/replay.tsx index 97f46073..3f581671 100644 --- a/mobile/app/(tabs)/(settings)/replay.tsx +++ b/mobile/app/(tabs)/(settings)/replay.tsx @@ -10,9 +10,11 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Directory, File, Paths } from "expo-file-system"; import { useRouter } from "expo-router"; import * as Haptics from "expo-haptics"; +import { startListPlayback } from "@music-library/core/player/play-list"; import { ApiError, api, + artistImageUrl, displayText, errorMessage, playsById as playsByIdFor, @@ -22,6 +24,8 @@ import { replayImageRequest, replayPlaylistRequest, replayRequest, + topArtistAction, + topArtistTracks, useAuth, useLocalDay, type ReplayAlbum, @@ -52,7 +56,8 @@ import { SummaryGrid } from "../../../components/replay/summary-grid"; import { TopTrackList } from "../../../components/replay/top-track-list"; import { qk } from "../../../lib/query-keys"; import { QUERY_STALE_TIME } from "../../../lib/query-policy"; -import { usePlayQueue } from "../../../lib/use-play-queue"; +import { listPlaybackQueue, usePlayQueue } from "../../../lib/use-play-queue"; +import { usePlayTrack } from "../../../context/player"; import { usePullToRefresh } from "../../../lib/use-pull-to-refresh"; import { useTheme } from "../../../theme/theme"; import { isShareDismissal } from "../../../lib/share-dismissal"; @@ -98,6 +103,13 @@ export default function ReplayScreen() { ); const playsById = useMemo(() => playsByIdFor(data?.top_tracks), [data?.top_tracks]); const onTrackPress = usePlayQueue(topTracks); + const play = usePlayTrack(); + // Their tracks among the top tracks: the artist tile's cover, and what it plays + // when they have no library page to open. + const artistTracks = useMemo( + () => topArtistTracks(data?.top_artists ?? [], topTracks), + [data?.top_artists, topTracks], + ); const onAlbumPress = useCallback( (album: ReplayAlbum) => { @@ -198,7 +210,7 @@ export default function ReplayScreen() { {replayQuery.isLoading && !data ? ( ) : replayQuery.isError ? ( - + ) : !hasData ? ( ) : data && summary ? ( @@ -211,7 +223,7 @@ export default function ReplayScreen() { /> {topTracks.length > 0 && ( -
+
0 && ( -
+
- {data.top_artists.map((a, i) => ( - - router.push({ - pathname: "/(tabs)/(settings)/artists/[id]", - params: { id: a.id }, - }) - } - /> - ))} + {data.top_artists.map((a, i) => { + const own = artistTracks.get(a.id) ?? []; + const action = topArtistAction(a, own); + const name = displayText(a.name); + return ( + + router.push({ + pathname: "/(tabs)/(settings)/artists/[id]", + params: { id: a.id }, + }) + : action === "play" + ? () => startListPlayback(play, listPlaybackQueue(own), false) + : undefined + } + /> + ); + })}
)} {data.top_albums.length > 0 && ( -
+
{data.top_albums.map((a, i) => ( 0 && ( -
+
0 && ( -
+
void; + /** Omitted when the tile has nothing to open or play. */ + onPress?: () => void; style?: StyleProp; }) { return ( + art.kind === "artist" ? ( + ) : ( ) @@ -68,18 +61,21 @@ export function RankedShelfTile({ subtitleTabular={subtitleTabular} width={TILE_SIZE} accessibilityLabel={accessibilityLabel} - onPress={() => { - void Haptics.selectionAsync(); - onPress(); - }} + onPress={ + onPress + ? () => { + void Haptics.selectionAsync(); + onPress(); + } + : undefined + } style={style} /> ); } -function GradientArt({ seed, rank }: { seed: string; rank: number }) { +function ArtistArt({ imageUrl, rank }: { imageUrl: string | null; rank: number }) { const theme = useTheme(); - const hue = hueFor(seed); return ( - + {imageUrl ? ( + + ) : ( + + )} ); @@ -135,12 +137,12 @@ function RankBadge({ rank }: { rank: number }) { void; + /** Omitted when the tile has nothing to open or play. */ + onPress?: () => void; style?: StyleProp; }) { const theme = useTheme(); return ( [{ width, opacity: pressed ? 0.7 : 1 }, style]} > diff --git a/mobile/tests/replay-artist-tiles.test.tsx b/mobile/tests/replay-artist-tiles.test.tsx new file mode 100644 index 00000000..d71d3d1c --- /dev/null +++ b/mobile/tests/replay-artist-tiles.test.tsx @@ -0,0 +1,159 @@ +import { type ReactNode } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { beforeEach, expect, it, vi } from "vitest"; +import ReplayScreen from "../app/(tabs)/(settings)/replay"; + +interface Tile { + title: string; + accessibilityLabel: string; + art: { kind: string; imageUrl?: string | null }; + onPress?: () => void; +} + +const mock = vi.hoisted(() => ({ + push: vi.fn(), + play: vi.fn(() => true), + tiles: [] as Tile[], + data: undefined as unknown, +})); + +vi.mock("react-native", () => ({ + Alert: { alert: vi.fn() }, + RefreshControl: () => null, + ScrollView: ({ children }: { children: ReactNode }) =>
{children}
, + Share: { share: vi.fn() }, + View: ({ children }: { children: ReactNode }) =>
{children}
, +})); +vi.mock("@tanstack/react-query", () => ({ + useQuery: () => ({ data: mock.data, isLoading: false, isError: false, refetch: vi.fn() }), + useQueryClient: () => ({ invalidateQueries: vi.fn() }), +})); +vi.mock("expo-file-system", () => ({ Directory: vi.fn(), File: vi.fn(), Paths: {} })); +vi.mock("expo-router", () => ({ useRouter: () => ({ push: mock.push }) })); +vi.mock("expo-haptics", () => ({ + notificationAsync: vi.fn(), + NotificationFeedbackType: {}, +})); +vi.mock("@music-library/core", async (importOriginal) => ({ + ...(await importOriginal()), + api: {}, + useAuth: () => ({ me: { id: "me" } }), + useLocalDay: () => "2026-11-03", +})); +vi.mock("../context/player", () => ({ usePlayTrack: () => mock.play })); +vi.mock("../lib/use-play-queue", () => ({ + listPlaybackQueue: (tracks: T[]) => tracks, + usePlayQueue: () => vi.fn(), +})); +vi.mock("../lib/use-pull-to-refresh", () => ({ + usePullToRefresh: () => ({ refreshing: false, onRefresh: vi.fn() }), +})); +vi.mock("../lib/app-resume", () => ({ subscribeAppActive: vi.fn() })); +vi.mock("../lib/share-dismissal", () => ({ isShareDismissal: () => false })); +vi.mock("../theme/theme", () => ({ + useTheme: () => ({ + color: { bg: "#000", fgMuted: "#888" }, + space: { sm: 8, lg: 16, xl: 24 }, + }), +})); +vi.mock("../components/dock/dock-context", () => ({ + useBottomDockInset: () => 0, + useDockScrollHandler: () => ({}), +})); +vi.mock("../components/empty-state", () => ({ EmptyState: () => null })); +vi.mock("../components/horizontal-shelf", () => ({ + HorizontalShelf: ({ children }: { children: ReactNode }) =>
    {children}
, +})); +vi.mock("../components/section", () => ({ + Section: ({ children }: { children: ReactNode }) =>
{children}
, +})); +vi.mock("../components/replay/activity-chart", () => ({ ActivityChart: () => null })); +vi.mock("../components/replay/genre-list", () => ({ GenreList: () => null })); +vi.mock("../components/replay/format", () => ({ + pluralize: (n: number, noun: string) => `${n} ${noun}s`, +})); +vi.mock("../components/replay/period", () => ({ + buildPeriodOptions: () => [], + periodTitle: () => "2026", +})); +vi.mock("../components/replay/period-picker", () => ({ PeriodPicker: () => null })); +vi.mock("../components/replay/replay-action-button", () => ({ + ReplayActionButton: () => null, +})); +vi.mock("../components/replay/replay-empty-state", () => ({ ReplayEmptyState: () => null })); +vi.mock("../components/replay/replay-hero", () => ({ ReplayHero: () => null })); +vi.mock("../components/replay/summary-grid", () => ({ SummaryGrid: () => null })); +vi.mock("../components/replay/top-track-list", () => ({ TopTrackList: () => null })); +vi.mock("../components/replay/ranked-shelf-tile", () => ({ + RankedShelfTile: (props: Tile) => { + mock.tiles.push(props); + return null; + }, +})); + +const track = (id: string, artist: string, artistId: string) => ({ + id, + title: id, + artist, + artist_id: artistId, + artists: [{ id: artistId, name: artist }], + album_id: `album-${id}`, + has_cover: true, + plays: 1, +}); + +function render(topArtists: { id: string; name: string; plays: number; has_page?: boolean }[]) { + mock.tiles = []; + mock.data = { + summary: { total_plays: 9, total_ms: 1000, unique_tracks: 2, unique_artists: 2 }, + available_years: [], + top_tracks: [track("t1", "Library Artist", "lib"), track("t2", "Streamed Artist", "tidal")], + top_artists: topArtists, + top_albums: [], + top_genres: [], + activity: [], + bucket: "month", + }; + renderToStaticMarkup(); + return mock.tiles; +} + +beforeEach(() => { + mock.push.mockClear(); + mock.play.mockClear(); +}); + +it("opens the artist's page when the server says there is one", () => { + const [tile] = render([{ id: "lib", name: "Library Artist", plays: 5, has_page: true }]); + expect(tile.accessibilityLabel).toBe("View artist Library Artist"); + tile.onPress?.(); + expect(mock.push).toHaveBeenCalledWith({ + pathname: "/(tabs)/(settings)/artists/[id]", + params: { id: "lib" }, + }); + expect(mock.play).not.toHaveBeenCalled(); +}); + +it("plays a streamed artist's top tracks, rather than opening Not found", () => { + const [tile] = render([{ id: "tidal", name: "Streamed Artist", plays: 4, has_page: false }]); + expect(tile.accessibilityLabel).toBe("Play Streamed Artist"); + tile.onPress?.(); + expect(mock.push).not.toHaveBeenCalled(); + expect(mock.play).toHaveBeenCalledTimes(1); + expect(mock.play.mock.calls[0]).toMatchObject([ + { id: "t2" }, + [{ id: "t2" }], + ]); +}); + +it("does nothing for an artist with no page and no top tracks", () => { + const [tile] = render([{ id: "gone", name: "Gone Artist", plays: 1 }]); + expect(tile.accessibilityLabel).toBe("Gone Artist"); + expect(tile.onPress).toBeUndefined(); + expect(tile.art).toEqual({ kind: "artist", imageUrl: null }); +}); + +it("shows a cover from the artist's own tracks", () => { + const [tile] = render([{ id: "lib", name: "Library Artist", plays: 5, has_page: true }]); + expect(tile.art.imageUrl).toContain("album-t1"); +}); From 8e21c10af536bb31fee2bcdfef7dba53c55b0818 Mon Sep 17 00:00:00 2001 From: esson <55195336+githubesson@users.noreply.github.com> Date: Tue, 3 Nov 2026 03:00:39 +0000 Subject: [PATCH 4/6] Mobile: fix the welcome and polish review findings - Clamp the welcome art, so cells cut off by the screen's edge stay opaque. - Drop the sampling prop on the art. RN Skia 2.6.2 reads its values as cubic coefficients, and leaving it out takes the same path. - A shelf tile with nothing to do is plain, not "dimmed" for VoiceOver. - Finish the labels: "Auto-download new tracks" and "New playlist". - Finish the weights: lyric section labels and the language selector stop at 600. - Declare canvaskit-wasm, which the mesh test imports directly. Co-Authored-By: Claude Opus 5.5 --- mobile/components/add-confirmation/store.ts | 2 +- mobile/components/auth/welcome-hero.tsx | 9 +-- .../now-playing/lyrics-language-selector.tsx | 2 +- .../components/now-playing/lyrics-section.tsx | 2 +- .../components/playlist-picker/picker-row.tsx | 2 +- .../playlists/playlist-detail-header.tsx | 4 +- mobile/components/shelf-tile.tsx | 4 +- mobile/components/swipe/swipe-action.tsx | 3 +- mobile/package-lock.json | 1 + mobile/package.json | 1 + mobile/tests/shelf-tile.test.tsx | 58 +++++++++++++++++++ mobile/tests/welcome-hero.test.tsx | 48 ++++++++++++--- 12 files changed, 113 insertions(+), 23 deletions(-) create mode 100644 mobile/tests/shelf-tile.test.tsx diff --git a/mobile/components/add-confirmation/store.ts b/mobile/components/add-confirmation/store.ts index f4aa53ac..82578c4c 100644 --- a/mobile/components/add-confirmation/store.ts +++ b/mobile/components/add-confirmation/store.ts @@ -32,7 +32,7 @@ export interface AddConfirmationRequest { playlistArt: { uri: string; placeholder: string | null } | null; /** Glyph for the empty tile. */ playlistGlyph: "music.note.list" | "person.2.fill"; - /** Made for this add: the HUD says "New Playlist" rather than "Added". */ + /** Made for this add: the HUD says "New playlist" rather than "Added". */ created: boolean; /** The add itself, already running. */ done: Promise; diff --git a/mobile/components/auth/welcome-hero.tsx b/mobile/components/auth/welcome-hero.tsx index 16bce025..9b9d5e81 100644 --- a/mobile/components/auth/welcome-hero.tsx +++ b/mobile/components/auth/welcome-hero.tsx @@ -11,9 +11,7 @@ import Animated, { FadeIn } from "react-native-reanimated"; import { Canvas, Fill, - FilterMode, ImageShader, - MipmapMode, Shader, Skia, type SkImage, @@ -101,15 +99,14 @@ export function WelcomeHero({ height }: { height: number }) { > diff --git a/mobile/components/now-playing/lyrics-language-selector.tsx b/mobile/components/now-playing/lyrics-language-selector.tsx index 0cd7287c..617feb02 100644 --- a/mobile/components/now-playing/lyrics-language-selector.tsx +++ b/mobile/components/now-playing/lyrics-language-selector.tsx @@ -213,7 +213,7 @@ function SelectorRow({ {title} diff --git a/mobile/components/now-playing/lyrics-section.tsx b/mobile/components/now-playing/lyrics-section.tsx index ccd41589..b135b770 100644 --- a/mobile/components/now-playing/lyrics-section.tsx +++ b/mobile/components/now-playing/lyrics-section.tsx @@ -599,7 +599,7 @@ const SyncedLyricLine = memo(function SyncedLyricLine({ const baseLineText: TextStyle = { fontSize: line.section ? 13 : state === "active" ? 19 : 18, - fontWeight: line.section ? "700" : state === "active" ? "500" : "400", + fontWeight: line.section ? "600" : state === "active" ? "500" : "400", lineHeight: line.section ? 18 : 27, // Body lines keep the frontend's -0.01em; section labels stay untracked. letterSpacing: line.section ? 0 : -0.19, diff --git a/mobile/components/playlist-picker/picker-row.tsx b/mobile/components/playlist-picker/picker-row.tsx index e6fed120..ac364c0f 100644 --- a/mobile/components/playlist-picker/picker-row.tsx +++ b/mobile/components/playlist-picker/picker-row.tsx @@ -160,7 +160,7 @@ export const NewPlaylistRow = memo(function NewPlaylistRow({ maxFontSizeMultiplier={PICKER_MAX_FONT_SCALE} style={[styles.subtitle, { color: theme.color.accent }]} > - New Playlist + New playlist
diff --git a/mobile/components/playlists/playlist-detail-header.tsx b/mobile/components/playlists/playlist-detail-header.tsx index 8f0510de..ee44b3dd 100644 --- a/mobile/components/playlists/playlist-detail-header.tsx +++ b/mobile/components/playlists/playlist-detail-header.tsx @@ -492,7 +492,7 @@ export function PlaylistMoreMenu({ { text: autoEnabled ? "Turn off auto-download" - : "Auto-Download New Tracks", + : "Auto-download new tracks", onPress: onAutoPress, }, ] @@ -561,7 +561,7 @@ export function PlaylistMoreMenu({ onPress={onDownloadPress} /> [{ width, opacity: pressed ? 0.7 : 1 }, style]} + style={({ pressed }) => [{ width, opacity: pressed && onPress ? 0.7 : 1 }, style]} > {artwork} void; + style: (state: { pressed: boolean }) => { opacity: number }[]; +} + +const mock = vi.hoisted(() => ({ pressable: undefined as PressableProps | undefined })); + +vi.mock("react-native", () => ({ + Pressable: ({ children, ...props }: PressableProps & { children: ReactNode }) => { + mock.pressable = props; + return ; + }, + Text: ({ children }: { children: ReactNode }) => {children}, + View: ({ children }: { children: ReactNode }) =>
{children}
, +})); +vi.mock("../theme/theme", () => ({ + useTheme: () => ({ color: { fg: "#fff", fgMuted: "#888" } }), +})); + +const render = (onPress?: () => void) => { + renderToStaticMarkup( + , + ); + if (!mock.pressable) throw new Error("no pressable"); + return mock.pressable; +}; + +beforeEach(() => { + mock.pressable = undefined; +}); + +it("is a button with press feedback when it does something", () => { + const tile = render(vi.fn()); + expect(tile.accessibilityRole).toBe("button"); + expect(tile.style({ pressed: true })[0].opacity).toBe(0.7); +}); + +it("is plain, not dimmed for VoiceOver, when it has nothing to do", () => { + const tile = render(); + expect(tile.accessibilityRole).toBeUndefined(); + expect(tile.disabled).toBeUndefined(); + expect(tile.accessibilityLabel).toBe("Clayco"); + expect(tile.style({ pressed: true })[0].opacity).toBe(1); +}); diff --git a/mobile/tests/welcome-hero.test.tsx b/mobile/tests/welcome-hero.test.tsx index 18fdd9d1..70ff26ae 100644 --- a/mobile/tests/welcome-hero.test.tsx +++ b/mobile/tests/welcome-hero.test.tsx @@ -2,7 +2,22 @@ import { type ReactNode } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { beforeEach, expect, it, vi } from "vitest"; -const mock = vi.hoisted(() => ({ make: vi.fn() })); +const mock = vi.hoisted(() => ({ + make: vi.fn(), + // Stands in for the effect that decodes the art, which a server render skips. + art: null as null | { width: () => number; height: () => number }, + shader: undefined as undefined | Record, + image: undefined as undefined | Record, +})); + +vi.mock("react", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useState: (initial: T) => + mock.art ? [{ art: mock.art, failed: false }, () => {}] : actual.useState(initial), + }; +}); vi.mock("react-native", () => ({ Image: { resolveAssetSource: () => ({ uri: "file:///art.jpg" }) }, @@ -22,12 +37,16 @@ vi.mock("react-native-reanimated", () => { }; }); vi.mock("@shopify/react-native-skia", () => ({ - Canvas: () => , - Fill: () => null, - FilterMode: {}, - ImageShader: () => null, - MipmapMode: {}, - Shader: () => null, + Canvas: ({ children }: { children: ReactNode }) => {children}, + Fill: ({ children }: { children: ReactNode }) => <>{children}, + ImageShader: (props: Record) => { + mock.image = props; + return null; + }, + Shader: ({ children, ...props }: { children: ReactNode }) => { + mock.shader = props; + return <>{children}; + }, Skia: { RuntimeEffect: { Make: mock.make } }, })); vi.mock("../components/auth/welcome-art", () => ({ @@ -42,6 +61,9 @@ vi.mock("../theme/theme", () => ({ beforeEach(() => { vi.resetModules(); mock.make.mockReset(); + mock.art = null; + mock.shader = undefined; + mock.image = undefined; }); async function render() { @@ -69,3 +91,15 @@ it("compiles the mesh once, however many times the hero renders", async () => { await render(); expect(mock.make).toHaveBeenCalledTimes(1); }); + +it("draws the art through the mesh, clamped so cut-off edge cells stay opaque", async () => { + const effect = {}; + mock.make.mockReturnValue(effect); + mock.art = { width: () => 660, height: () => 960 }; + expect(await render()).toContain(" Date: Tue, 3 Nov 2026 03:02:27 +0000 Subject: [PATCH 5/6] Frontend tests: keep the active invite fixture from expiring The fixture's only active invite expired on 2026-11-01, so /admin/invites showed no Revoke button and "revoke invite confirmation" failed on every viewport, on any branch. Its expiry now sits far in the future. Co-Authored-By: Claude Opus 5.5 --- frontend/tests/ui/fixtures.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/tests/ui/fixtures.ts b/frontend/tests/ui/fixtures.ts index b56cde74..7713bcd5 100644 --- a/frontend/tests/ui/fixtures.ts +++ b/frontend/tests/ui/fixtures.ts @@ -560,7 +560,7 @@ export const adminUsers: AdminUser[] = [ ]; export const invites: Invite[] = [ - { id: "inv-1", token: "lmn_7QxV2pKc9RfT4wZb", target_role: "user", max_uses: 5, uses: 2, expires_at: "2026-11-01T00:00:00Z", created_at: "2026-10-01T12:00:00Z" }, + { id: "inv-1", token: "lmn_7QxV2pKc9RfT4wZb", target_role: "user", max_uses: 5, uses: 2, expires_at: "2099-11-01T00:00:00Z", created_at: "2026-10-01T12:00:00Z" }, { id: "inv-2", token: "lmn_Hd83LwQe1NvY6sAa", target_role: "admin", max_uses: 1, uses: 0, expires_at: "2026-10-18T00:00:00Z", created_at: "2026-10-09T08:30:00Z" }, { id: "inv-3", token: "lmn_Zp4mKt7BcXr2Ue9J", target_role: "user", max_uses: 10, uses: 10, expires_at: null, created_at: "2026-05-20T17:45:00Z" }, ]; From abf09693fabf5ec80b5bf6c7977f65b6b16dddd7 Mon Sep 17 00:00:00 2001 From: esson <55195336+githubesson@users.noreply.github.com> Date: Tue, 3 Nov 2026 03:10:50 +0000 Subject: [PATCH 6/6] Mobile: drop the old welcome scene when the appearance changes If the phone switched between light and dark while Welcome was up, the old scene drew with the new scheme's colors until the new one decoded, then snapped in. The hero now draws nothing until the new scene is ready, and that scene fades in. Co-Authored-By: Claude Opus 5.5 --- mobile/components/auth/welcome-hero.tsx | 14 +++++----- mobile/tests/welcome-hero.test.tsx | 34 +++++++++++++++++++++---- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/mobile/components/auth/welcome-hero.tsx b/mobile/components/auth/welcome-hero.tsx index 9b9d5e81..75a1266f 100644 --- a/mobile/components/auth/welcome-hero.tsx +++ b/mobile/components/auth/welcome-hero.tsx @@ -47,20 +47,22 @@ function getMeshEffect(): SkRuntimeEffect | null { /** The decoded art for Skia: null while loading, and `failed` if it never will. */ function useWelcomeArt(source: number): { art: SkImage | null; failed: boolean } { - const [state, setState] = useState<{ art: SkImage | null; failed: boolean }>({ - art: null, - failed: false, - }); + const [loaded, setLoaded] = useState<{ source: number; art: SkImage | null } | null>( + null, + ); useEffect(() => { let live = true; void loadWelcomeArt(NativeImage.resolveAssetSource(source).uri, Skia).then((art) => { - if (live) setState({ art, failed: art === null }); + if (live) setLoaded({ source, art }); }); return () => { live = false; }; }, [source]); - return state; + // After a switch between light and dark, the old scene belongs to the old + // scheme. Drawing nothing lets the new one fade in on its own. + if (loaded?.source !== source) return { art: null, failed: false }; + return { art: loaded.art, failed: loaded.art === null }; } /** diff --git a/mobile/tests/welcome-hero.test.tsx b/mobile/tests/welcome-hero.test.tsx index 70ff26ae..bfa37b9e 100644 --- a/mobile/tests/welcome-hero.test.tsx +++ b/mobile/tests/welcome-hero.test.tsx @@ -2,20 +2,28 @@ import { type ReactNode } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { beforeEach, expect, it, vi } from "vitest"; +const NIGHT = 1; +const MORNING = 2; + const mock = vi.hoisted(() => ({ make: vi.fn(), // Stands in for the effect that decodes the art, which a server render skips. - art: null as null | { width: () => number; height: () => number }, + loaded: null as null | { + source: number; + art: { width: () => number; height: () => number } | null; + }, shader: undefined as undefined | Record, image: undefined as undefined | Record, })); +// The hero's only state is the decoded art, so this is all of its state. A second +// useState in the hero would need this mock to tell them apart. vi.mock("react", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useState: (initial: T) => - mock.art ? [{ art: mock.art, failed: false }, () => {}] : actual.useState(initial), + mock.loaded ? [mock.loaded, () => {}] : actual.useState(initial), }; }); @@ -61,7 +69,7 @@ vi.mock("../theme/theme", () => ({ beforeEach(() => { vi.resetModules(); mock.make.mockReset(); - mock.art = null; + mock.loaded = null; mock.shader = undefined; mock.image = undefined; }); @@ -95,11 +103,27 @@ it("compiles the mesh once, however many times the hero renders", async () => { it("draws the art through the mesh, clamped so cut-off edge cells stay opaque", async () => { const effect = {}; mock.make.mockReturnValue(effect); - mock.art = { width: () => 660, height: () => 960 }; + const art = { width: () => 660, height: () => 960 }; + mock.loaded = { source: NIGHT, art }; expect(await render()).toContain(" { + mock.make.mockReturnValue({}); + // The theme is dark, and what's decoded is still the morning file. + mock.loaded = { source: MORNING, art: { width: () => 660, height: () => 960 } }; + const html = await render(); + expect(html).not.toContain(" { + mock.make.mockReturnValue({}); + mock.loaded = { source: NIGHT, art: null }; + expect(await render()).toContain('alt="plain art"'); });