Skip to content

Commit 37bcdb8

Browse files
authored
Merge pull request #16 from chrisuthe/chrisuthe/task/compose-now-playing-and-welcome-adaptive-layout
Compose Now Playing and Welcome: adaptive layout, blurred art backdrop, auto-connect prompt
2 parents 18c51de + 52cd705 commit 37bcdb8

24 files changed

Lines changed: 1791 additions & 280 deletions

docs/ARCHITECTURE.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,3 +1152,65 @@ other file under `src/Sendspin.Player`. The progress bar no longer steps by the
11521152
(`Core/MediaSession/AnchoredPosition.cs`) and projects from there on each tick, so it is right on a
11531153
head whose timer is a quantum late as much as on one whose timer is exact. Phase 5 paces the backdrop
11541154
with the same helper at 60 Hz.
1155+
1156+
### As shipped (reskin phase 3) — Now Playing, Welcome, the backdrop, the prompt
1157+
1158+
**Now Playing** (`Views/NowPlayingView.axaml`) is one tree with two compositions. Below 640 px of view
1159+
width it is the reference's stacked column — the art tile, then centred title / artist / album, then
1160+
the progress row, then the transport; at 640 px and above the art sits on the left with the text
1161+
column, progress row and transport left-aligned beside it, vertically centred. The switch is the
1162+
`wide` class on the composition grid, set from `OnSizeChanged` in the code-behind, and every
1163+
difference between the two is a style in the view keyed on that class (grid row and column spans,
1164+
alignments, text alignment). It is a class toggle rather than an Avalonia 12 container query because
1165+
the test that pins which composition is active (`Sendspin.Ui.Tests/NowPlayingViewTests`) and Phase
1166+
5's breathing-art work both read the switch off the one control, and because the art's size is a
1167+
computed value a query could not express anyway: the body width minus the 24 px margins when
1168+
stacked, the body height when split, less what the text and transport need, never more than 320 px
1169+
and never less than 96 (`NowPlayingView.ArtSizeFor`, pinned by a theory). The tile is three borders:
1170+
`ArtBreath`, the wrapper Phase 5 animates, carrying nothing today; `ArtTile`, which holds the resting
1171+
`BoxShadow` (the cheap glow in the effect table above) and must not clip, since a shadow lies outside
1172+
the bounds; and `ArtClip`, which clips the picture to the 12 px radius and paints `ArtPlaceholderBrush`
1173+
behind the `MusicIcon` glyph. The shadow is set from the code-behind because a `BoxShadow` holds a
1174+
`Color`, not a brush, so it reads `.Color` off the `ArtShadowBrush` token and re-reads it on
1175+
`ActualThemeVariantChanged`. The progress row is elapsed / a 4 px bar / duration; for a live stream
1176+
(`MediaSessionState.IsLive`, the one rule) the bar is hidden and the duration slot reads `LIVE`.
1177+
`MainViewModel` exposes `ElapsedText`, `DurationText`, `HasKnownDuration` and `RepeatTooltip`;
1178+
`PositionText` is gone, nothing else bound it. The idle title is null rather than a failed binding, so
1179+
"Nothing playing" is a `TargetNullValue` — the Phase 2 `FallbackValue` alone never showed it.
1180+
1181+
**The blurred backdrop.** `MainViewModel.LoadArtwork` decodes the artwork file once per artwork
1182+
change (it already deduplicated by path) and, alongside `Artwork`, produces `ArtBackdrop` with
1183+
`Bitmap.CreateScaledBitmap` to 64×64 — a decoded `Bitmap`, which is what that method accepts. Layer 0
1184+
of the window binds it to an `Image` with `Stretch="UniformToFill"`, a `BlurEffect` of radius 32 and
1185+
a −48 px margin so the blur's soft edge falls outside the window instead of fading to the opaque root
1186+
along the border. The blur runs over the 64 px source and is cached — the `blur-once` row above —
1187+
so dragging the window does not repaint it. `HasArtBackdrop` is connected-and-bitmap-present, set in
1188+
the two places that change either, and the veil follows `HasBackdrop` as Phase 2 left it. Both
1189+
bitmaps are unbound before they are disposed, for the render-thread reason in the view model.
1190+
1191+
**Welcome** (`Views/WelcomeView.axaml`) is cards on `TranslucentSurfaceBrush` with the settings card's
1192+
1 px `SystemControlForegroundBaseLowBrush` hairline — with no backdrop behind Welcome the surface is
1193+
the root's own colour, and it is the border that makes a card read as one. "This player" holds the
1194+
name as a write-through `TextBox` and, while advertising, an accent dot with "Broadcasting, visible
1195+
to servers". The dot pulses at 2 Hz from a `UiClock` in the view's code-behind — an Avalonia
1196+
`Animation` would spin a core on the Wayland head, per the clock table — and the clock runs only while
1197+
the view is attached, the player is advertising and nothing is connected, which is when the card is on
1198+
screen. Which mode the player is in is read once, at view-model construction, exactly as the player
1199+
service reads it: a mode changed in Settings takes effect after a restart, and the card describes what
1200+
the service is doing now. Discover mode shows "Servers on this network" (a `ListBox` of name and
1201+
`host:port`, "Searching for servers…" while empty, Connect, or "Connecting…" while `IsConnecting`);
1202+
advertise mode shows "Waiting for a server" instead. "Connect by address" is an `Expander` beneath
1203+
either, collapsed by default.
1204+
1205+
**The auto-connect prompt** is an in-window card over the body (`AutoConnectPrompt` in
1206+
`MainWindow.axaml`), shown once per server: when a connection succeeds in discover mode while
1207+
`AutoConnect` is `Never` and `PlayerSettings.AutoConnectPromptedServerId` is not the server just
1208+
connected to. The id is read from `LastServerId`, which the player service writes before it raises
1209+
the connection event. Every answer records the id, so "Not now" is remembered too; "Just once" and
1210+
"Always" write the policy through `SettingsViewModel.AutoConnect` — the same
1211+
`SettingsService.Update` — so the Settings combo shows the answer rather than the value it loaded at
1212+
startup. The service's start-up auto-connect is untouched: a "just once" connection reverts the policy
1213+
to `Never` before it connects, and the server it connects to has already been asked about.
1214+
`Sendspin.Ui.Tests/AutoConnectPromptTests` walks each answer and the once-per-server rule.
1215+
Screenshots: `docs/screenshots/reskin/phase3-{narrow,wide}-{light,dark}.png` and
1216+
`phase3-welcome-{advertise,discover}.png`.
231 KB
Loading
229 KB
Loading
71.4 KB
Loading
59.3 KB
Loading
296 KB
Loading
300 KB
Loading

src/Sendspin.Core/Configuration/PlayerSettings.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,15 @@ public sealed class PlayerSettings
105105
/// <summary>Gets or sets the server id to auto-connect to.</summary>
106106
public string? LastServerId { get; set; }
107107

108+
/// <summary>
109+
/// Gets or sets the server the auto-connect question has already been asked for.
110+
/// </summary>
111+
/// <remarks>
112+
/// The question is asked once per server, whatever the answer, so "not now" has to leave a
113+
/// record too: without one the prompt would come back on every reconnect to the same server.
114+
/// </remarks>
115+
public string? AutoConnectPromptedServerId { get; set; }
116+
108117
/// <summary>
109118
/// Gets or sets a manually entered server URL, remembered so it can be offered again.
110119
/// </summary>

src/Sendspin.Player/Styles/PlayerStyles.axaml

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
</Style>
6060

6161
<Style Selector="StackPanel.transport">
62-
<Setter Property="Margin" Value="16,8,16,12" />
62+
<Setter Property="Margin" Value="0,16,0,0" />
6363
</Style>
6464

6565
<Style Selector="Border.footer">
@@ -68,7 +68,8 @@
6868
<Setter Property="Background" Value="{DynamicResource TranslucentSurfaceBrush}" />
6969
</Style>
7070

71-
<Style Selector="Border.settingsOverlay">
71+
<!-- The settings card and the auto-connect prompt: one card style, laid over the body. -->
72+
<Style Selector="Border.settingsOverlay, Border.promptCard">
7273
<Setter Property="Margin" Value="12" />
7374
<Setter Property="Padding" Value="16" />
7475
<Setter Property="CornerRadius" Value="8" />
@@ -77,6 +78,19 @@
7778
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseLowBrush}" />
7879
</Style>
7980

81+
<!--
82+
The Welcome cards, on the same translucent surface as the footer, with the settings card's
83+
hairline: with no backdrop behind Welcome the surface is the root's own colour, and it is
84+
the border that makes a card read as one.
85+
-->
86+
<Style Selector="Border.card">
87+
<Setter Property="Padding" Value="16" />
88+
<Setter Property="CornerRadius" Value="8" />
89+
<Setter Property="Background" Value="{DynamicResource TranslucentSurfaceBrush}" />
90+
<Setter Property="BorderThickness" Value="1" />
91+
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseLowBrush}" />
92+
</Style>
93+
8094
<Style Selector="Border.diagnostics">
8195
<Setter Property="Padding" Value="14" />
8296
<Setter Property="CornerRadius" Value="6" />
@@ -85,25 +99,46 @@
8599
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseLowBrush}" />
86100
</Style>
87101

88-
<Style Selector="Border.artwork">
89-
<Setter Property="Width" Value="180" />
90-
<Setter Property="Height" Value="180" />
91-
<Setter Property="CornerRadius" Value="8" />
102+
<!--
103+
The art tile. Two borders: the outer one carries the resting shadow (a BoxShadow, which the
104+
spike measured as the cheap kind, set from the code-behind because it wants a Color and
105+
the colour is a token) and must not clip, since a shadow lies outside the bounds; the inner
106+
one clips the picture to the same radius and paints the placeholder behind it.
107+
-->
108+
<Style Selector="Border.artTile">
109+
<Setter Property="CornerRadius" Value="12" />
110+
</Style>
111+
112+
<Style Selector="Border.artClip">
113+
<Setter Property="CornerRadius" Value="12" />
92114
<Setter Property="ClipToBounds" Value="True" />
93115
<Setter Property="Background" Value="{DynamicResource ArtPlaceholderBrush}" />
94116
</Style>
95117

96118
<Style Selector="PathIcon.artworkPlaceholder">
97-
<Setter Property="Width" Value="56" />
98-
<Setter Property="Height" Value="56" />
119+
<Setter Property="Width" Value="80" />
120+
<Setter Property="Height" Value="80" />
99121
<Setter Property="Opacity" Value="0.35" />
100122
<Setter Property="HorizontalAlignment" Value="Center" />
101123
<Setter Property="VerticalAlignment" Value="Center" />
102124
</Style>
103125

104-
<Style Selector="StackPanel.trackInfo">
105-
<Setter Property="Margin" Value="16,0,0,0" />
126+
<!-- The progress row's bar: theme foreground on a low-opacity track, 4 px. -->
127+
<Style Selector="ProgressBar.track">
128+
<Setter Property="Height" Value="4" />
129+
<Setter Property="MinHeight" Value="4" />
130+
<Setter Property="CornerRadius" Value="2" />
131+
<Setter Property="VerticalAlignment" Value="Center" />
132+
<Setter Property="Foreground" Value="{DynamicResource SystemControlForegroundBaseHighBrush}" />
133+
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" />
134+
</Style>
135+
136+
<!-- The Welcome card's "broadcasting" dot; the code-behind pulses its opacity. -->
137+
<Style Selector="Ellipse.broadcastDot">
138+
<Setter Property="Width" Value="8" />
139+
<Setter Property="Height" Value="8" />
106140
<Setter Property="VerticalAlignment" Value="Center" />
141+
<Setter Property="Fill" Value="{DynamicResource SystemControlForegroundAccentBrush}" />
107142
</Style>
108143

109144
<!--
@@ -144,6 +179,12 @@
144179
<Setter Property="FontSize" Value="12" />
145180
</Style>
146181

182+
<!-- The album line under the artist: caption-sized, and quieter still. -->
183+
<Style Selector="TextBlock.album">
184+
<Setter Property="FontSize" Value="12" />
185+
<Setter Property="Opacity" Value="0.5" />
186+
</Style>
187+
147188
<Style Selector="TextBlock.warning">
148189
<Setter Property="FontSize" Value="12" />
149190
<Setter Property="Foreground" Value="{DynamicResource SystemControlErrorTextForegroundBrush}" />
@@ -213,6 +254,18 @@
213254
<Setter Property="CornerRadius" Value="18" />
214255
</Style>
215256

257+
<!-- Same reason: shuffle and repeat are the transport's 40 px pair, either side of the 48s. -->
258+
<Style Selector="Button.iconButton.small, ToggleButton.iconButton.small">
259+
<Setter Property="Width" Value="40" />
260+
<Setter Property="Height" Value="40" />
261+
<Setter Property="CornerRadius" Value="20" />
262+
</Style>
263+
264+
<Style Selector="Button.iconButton.small PathIcon, ToggleButton.iconButton.small PathIcon">
265+
<Setter Property="Width" Value="21" />
266+
<Setter Property="Height" Value="21" />
267+
</Style>
268+
216269
<Style Selector="Border.toolbar PathIcon, Border.footer PathIcon">
217270
<Setter Property="Width" Value="20" />
218271
<Setter Property="Height" Value="20" />

src/Sendspin.Player/Styles/Tokens.axaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,19 @@
2727
<SolidColorBrush x:Key="ArtPlaceholderBrush" Color="{DynamicResource SystemListLowColor}" />
2828
<!-- The art glow until a palette has been extracted from the artwork. -->
2929
<SolidColorBrush x:Key="GlowDefaultBrush" Color="{DynamicResource SystemAccentColorDark1}" />
30+
<!--
31+
The art tile's resting shadow. A Fluent colour that already carries its alpha, because
32+
a BoxShadow takes a Color, not a brush, and reads this one's .Color.
33+
-->
34+
<SolidColorBrush x:Key="ArtShadowBrush" Color="{DynamicResource SystemChromeBlackMediumLowColor}" />
3035
</ResourceDictionary>
3136

3237
<ResourceDictionary x:Key="Dark">
3338
<SolidColorBrush x:Key="VeilBrush" Color="{DynamicResource SystemAltHighColor}" Opacity="0.75" />
3439
<SolidColorBrush x:Key="TranslucentSurfaceBrush" Color="{DynamicResource SystemAltHighColor}" Opacity="0.6" />
3540
<SolidColorBrush x:Key="ArtPlaceholderBrush" Color="{DynamicResource SystemListLowColor}" />
3641
<SolidColorBrush x:Key="GlowDefaultBrush" Color="{DynamicResource SystemAccentColorLight1}" />
42+
<SolidColorBrush x:Key="ArtShadowBrush" Color="{DynamicResource SystemChromeBlackMediumLowColor}" />
3743
</ResourceDictionary>
3844
</ResourceDictionary.ThemeDictionaries>
3945

0 commit comments

Comments
 (0)