Skip to content

UI Polish: layout fixes, hover outlines, smooth scroll, and playing card highlight#20

Open
Kryxzort wants to merge 35 commits into
sunwoo101:mainfrom
Kryxzort:pr/ui-polish
Open

UI Polish: layout fixes, hover outlines, smooth scroll, and playing card highlight#20
Kryxzort wants to merge 35 commits into
sunwoo101:mainfrom
Kryxzort:pr/ui-polish

Conversation

@Kryxzort

@Kryxzort Kryxzort commented May 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Layout: outer grid gains a third row so the playlist strip and status bar sit below the tab area without overlap; all modals updated to RowSpan=3
  • Hover outlines: library-hover-outline CSS class fades in an accent border on pointer-over for Browse, Library, and Playlist cards (0.15s opacity transition)
  • Currently-playing highlight: green Success border on the active card in both the Library grid and Playlist strip; cleared on Stop; restored on GUI open after --restore
  • Playlist strip: relocated to Grid.Row=1 (always visible on Library tab); persistent scrollbar (AllowAutoHide=False); material icons for settings/load/save/play buttons; circular remove badge
  • Scrollbars: thin 6px track, expand to 8px on hover, transparent background, transitions
  • Preview modal: wallpaper title is now selectable (read-only TextBox); video duration shown below title
  • Playlist settings popup: interval and advance-on-end knobs show global values when override is off, per-playlist values when on (DisplayInterval* proxies)
  • Button press animation: scale(0.96) with 65ms transition on all buttons
  • Theme responsiveness: all StaticResource color references converted to DynamicResource so theme switching updates every element live; AccentHover, DangerBg, Success colors added

Notes

Stacked on #12 (Library UI Overhaul) — retarget base to main after that merges.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added library search with debounced updates
    • Sorting options by name, date, video count, or scene content
    • Customizable card size and thumbnail aspect ratio settings
    • GIF thumbnail animations on hover
    • Stop and Play All library controls
    • "Currently playing" wallpaper indicator
    • Video duration display in preview modal
    • Smooth scroll animations
    • Enhanced grid-based library layout with visual filtering
  • Documentation

    • Updated UI and session documentation with expanded settings and layout details

Review Change Stack

Kryxzort and others added 18 commits May 8, 2026 04:15
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LibraryItem: add WorkshopId, AddedAt (used by sort/search)
WallpaperCardViewModel: add IsScene, IsCurrentlyPlaying
PlayerHelper: add OnWallpaperChanged action

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BrowseScrollViewer/LibraryScrollViewer: swap Padding for
  HorizontalScrollBarVisibility=Disabled + AllowAutoHide=False
- SettingsScrollViewer: add HorizontalScrollBarVisibility=Disabled
- Library header: reorder to Shuffle/Import/Stop/Play All/Search/Sort
- Sort button: Width=28, MenuFlyout Placement=BottomEdgeAlignedRight
- Sort menu labels: use en-dash, "Newest/Oldest added"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Kryxzort, we couldn't start this review because you've used your available PR reviews for now.

Your plan includes 1 review of capacity. Refill in 59 minutes and 55 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8c8aec30-3911-44b0-8690-6d34f26e44db

📥 Commits

Reviewing files that changed from the base of the PR and between 82bfddb and ac5c33d.

📒 Files selected for processing (7)
  • src/livepaper/App.axaml
  • src/livepaper/Helpers/PlayerHelper.cs
  • src/livepaper/ViewModels/MainWindowViewModel.cs
  • src/livepaper/ViewModels/WallpaperCardViewModel.cs
  • src/livepaper/Views/MainWindow.axaml
  • src/livepaper/Views/MainWindow.axaml.cs
  • src/livepaper/livepaper.csproj
📝 Walkthrough

Walkthrough

This PR adds library filtering and sorting, customizable card layout (thumbnail aspect and size), GIF thumbnail playback, video duration display, and inertial smooth scrolling. Browse and Library grids migrate to ItemsRepeater with UniformGridLayout. Playlist settings now expose display-proxy properties that switch between global and per-playlist timing based on override flags.

Changes

Library Filtering, Card Customization, and Enhanced Wallpaper Display

Layer / File(s) Summary
Documentation and Dependencies
.claude/rules/session.md, .claude/rules/ui.md, src/livepaper/livepaper.csproj
Documentation expands to describe Browse/Library grid layouts, search/sort/appearance controls, and Avalonia filter-suppression patterns. Three NuGet dependencies added: ItemsRepeater, Material.Icons.Avalonia, and Avalonia.Labs.Gif.
Data Model Extensions
src/livepaper/Models/AppSettings.cs, src/livepaper/Models/LibraryItem.cs
AppSettings adds ThumbnailAspect, CardSize, and LibrarySortIndex properties. LibraryItem adds WorkshopId (nullable) and AddedAt (DateTime) metadata fields.
Library Service Media Discovery and Metadata
src/livepaper/Helpers/LibraryService.cs
Media enumeration now skips .png files with .scene sidecars or conflicting .mp4 files. WorkshopId is validated from SourceId (digit-only, otherwise null). AddedAt is populated from media file UTC creation time.
Playback State Tracking and IPC Queries
src/livepaper/Helpers/PlayerHelper.cs
Adds OnWallpaperChanged callback emitted after ApplyPlaylist and SwitchToFile launch wallpaper. Implements QueryCurrentPath() via Unix socket IPC; stubs QueryCurrentSceneWorkshopId().
Application Theme and Control Styling
src/livepaper/App.axaml
Adds Material Icons namespace and theme brushes (Accent/Danger/Success). Updates button/input/list/tab/checkbox/scrollbar styles to use DynamicResource; adds button RenderTransform transitions, scrollbar template overrides, and hover opacity animation.
Card View Model: GIF and Duration Support
src/livepaper/ViewModels/WallpaperCardViewModel.cs
Adds GIF state (IsGifActive, GifSource, ActiveGifSource). Implements LoadDurationAsync() and ReadDuration() to fetch and format video duration via ffprobe. Adds IsCurrentlyPlaying observable property.
Main Window ViewModel: Card Layout, Filtering, and Playback Tracking
src/livepaper/ViewModels/MainWindowViewModel.cs
Adds card layout options (ThumbnailAspectOptions, CardSizeOptions, CardLayoutChanged callback). Implements library filter/sort (search query, sort index, debounced FilteredLibraryWallpapers). Adds display-proxy properties that switch between global/per-playlist interval and advance. Tracks currently-playing card via OnWallpaperChanged callback. Updates selection to operate on filtered library. Suppresses filter updates during bulk LoadLibrary().
Main Window View Layout and Templates
src/livepaper/Views/MainWindow.axaml
Refactors Browse and Library grids to ItemsRepeater + UniformGridLayout. Adds ThumbnailDisplay template with GIF support. Introduces Material Icons for refresh, sort, and playlist actions. Library now binds FilteredLibraryWallpapers. Card overlays enhanced with hover outline, selection, and currently-playing borders. Adds library search textbox and sort flyout. Introduces dedicated Playlist panel. Updates playlist settings to use DisplayAdvanceOnVideoEnd/DisplayInterval* bindings. Adds Appearance settings for thumbnail aspect and card size. Adjusts modal row spans and preview-modal duration display.
Main Window Code-Behind: Scrolling and Interaction
src/livepaper/Views/MainWindow.axaml.cs
Adds SmoothScroller for inertial scrolling with friction decay. Implements UpdateCardThumbnailHeight() to compute card dimensions from repeater width, aspect ratio, and size. Pointer entered/exited handlers toggle IsGifActive for cards. OnPointerPressed deselects library/browse on null card lookup. Pointer capture lost/release disable IsGifActive on dragged card.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant MainVM as MainWindowViewModel
  participant PlayerHelper
  participant LibraryService
  User->>MainVM: Type search text (debounced)
  MainVM->>MainVM: UpdateFilteredLibrary()
  MainVM->>MainVM: FilteredLibraryWallpapers updated
  User->>MainVM: SelectAll or range-select
  MainVM->>MainVM: Operate on FilteredLibraryWallpapers
  User->>PlayerHelper: Launch/switch wallpaper
  PlayerHelper->>MainVM: OnWallpaperChanged(path)
  MainVM->>MainVM: Update _currentlyPlayingCard
  LibraryService->>MainVM: LoadLibrary() (suppress filter updates)
  MainVM->>MainVM: Bulk add cards to LibraryWallpapers
  MainVM->>MainVM: Single UpdateFilteredLibrary() call after
Loading
sequenceDiagram
  participant User
  participant MainWindow
  participant Card as WallpaperCard
  User->>MainWindow: PointerEntered on card
  MainWindow->>Card: IsGifActive = true
  Card->>Card: ActiveGifSource shown
  User->>MainWindow: Drag started
  MainWindow->>MainWindow: _currentDragCard set
  User->>MainWindow: PointerExited
  MainWindow->>MainWindow: If not dragging, IsGifActive = false
  User->>MainWindow: Drag ended (PointerReleased)
  MainWindow->>Card: IsGifActive = false
  MainWindow->>MainWindow: _currentDragCard cleared
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • sunwoo101/livepaper#7: Both PRs refine .png vs .mp4 selection and LibraryItem construction in LibraryService.LoadAll(), directly connecting image-import handling with the main PR's media discovery logic.
  • sunwoo101/livepaper#5: Both PRs modify PlayerHelper.cs (IPC-based wallpaper switching and OnWallpaperChanged emission) and LibraryService.LoadAll() library maintenance, linking playback control to library state.
  • sunwoo101/livepaper#2: Both PRs modify PlayerHelper.cs playlist/playback control—main PR hooks OnWallpaperChanged into ApplyPlaylist/SwitchToFile, while retrieved PR #2 rewrites PlayerHelper's playlist and IPC handling, connecting at the code level.

Poem

🐰 A wallpaper ode

Browse the library with search and sorted grace,
GIF thumbnails dance at each hovering place.
Cards reshape themselves in your chosen style,
Smooth scrolling glides through a thousand-item mile.
Now playing badges mark your favorite scene,
The finest wallpaper viewer ever seen! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main visual and UX improvements in the PR: layout fixes (grid row structure), hover outlines (pointer-over border animations), smooth scroll (momentum scrolling), and playing card highlight (success border on active playback).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/livepaper/ViewModels/MainWindowViewModel.cs (1)

1106-1128: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear the full library on plain-click selection.

With an active filter, this only deselects the visible subset. Any filtered-out cards that were already selected stay selected, so DeleteSelected, playlist actions, and the selected-count badge can still include hidden items after what looks like a single-selection click.

Suggested fix
-            foreach (var c in displayed) c.IsSelected = false;
+            foreach (var c in LibraryWallpapers) c.IsSelected = false;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/livepaper/ViewModels/MainWindowViewModel.cs` around lines 1106 - 1128,
SelectCard currently only clears selection on the visible subset "displayed"
(derived from FilteredLibraryWallpapers), so hidden/filtered items remain
selected on a plain click; change the plain-click branch in SelectCard to clear
IsSelected across the full library collection (the underlying collection that
backs FilteredLibraryWallpapers) instead of just iterating "displayed", then
preserve the existing wasOnlySelected logic and set the clicked card as selected
when appropriate. Locate the plain-click else branch in SelectCard and replace
the foreach over "displayed" with a foreach over the full library collection
(the authoritative list of WallpaperCardViewModel instances) so DeleteSelected,
playlist actions, and LibrarySelectedCount reflect the true single selection.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/livepaper/Helpers/PlayerHelper.cs`:
- Line 716: The method QueryCurrentSceneWorkshopId currently returns null
silently which hides "not implemented" vs "no active scene"; replace the no-op
with an explicit unsupported indicator by throwing NotImplementedException or
NotSupportedException from QueryCurrentSceneWorkshopId so callers can detect
unimplemented lookup, and add/update its XML doc comment to state that it throws
when scene lookup is unsupported; ensure any callers of
QueryCurrentSceneWorkshopId are updated to catch/handle this exception or use an
alternative API that distinguishes "no active scene" from "not implemented".
- Around line 718-733: TryQueryCurrentPath currently calls Socket.Receive once
and attempts to JsonDocument.Parse that single read; because mpv IPC is
newline-delimited, you must instead read from the stream until you have at least
one full line (\\n), accumulate bytes into a buffer/string, then split on
newlines and parse the first non-empty line as JSON. Update the logic around the
socket Receive loop in TryQueryCurrentPath to repeatedly Receive into a growing
buffer (respecting SendTimeout/ReceiveTimeout), break when a newline is seen or
socket closed, extract the first complete line, and JsonDocument.Parse that line
(handle partial/multiple messages by ignoring subsequent data for this call).
Ensure exceptions/timeouts still return null and the socket is disposed.

In `@src/livepaper/ViewModels/MainWindowViewModel.cs`:
- Around line 1464-1469: MakeLibraryCard currently calls
WallpaperCardViewModel.LoadDurationAsync for every item which starts an ffprobe
per card; remove that eager call and instead wire duration probing through a
single throttling mechanism or visibility-trigger: stop calling
LoadDurationAsync in MakeLibraryCard, add a shared
DurationProbeQueue/DurationProbeService (using SemaphoreSlim or a bounded Task
queue) with an EnqueueProbe(Func<Task>) method, and have WallpaperCardViewModel
either request duration when it becomes visible (e.g., an IsVisible/OnAppear
event) or call the service's EnqueueProbe to run LoadDurationAsync through the
shared semaphore; update LoadLibrary to create cards without starting probes and
ensure WallpaperCardViewModel exposes a method (e.g., RequestDurationLoad) that
uses the central service to perform ffprobe calls safely.

In `@src/livepaper/ViewModels/WallpaperCardViewModel.cs`:
- Around line 21-23: The new properties IsScene and WorkshopId on
WallpaperCardViewModel are never set, causing incorrect defaults and breaking
LoadDurationAsync's logic; update both constructors of WallpaperCardViewModel to
initialize IsScene and WorkshopId from the incoming model/data (e.g., use the
source object or parameters used to build the view model) so IsScene reflects
whether the card is a scene and WorkshopId contains the original workshop
identifier, ensuring LoadDurationAsync and the workshop-id restore path can find
matching cards.

In `@src/livepaper/Views/MainWindow.axaml`:
- Around line 21-33: The ThumbnailDisplay ControlTemplate (containing
AdvancedImage, gif:GifImage, IsGifActive binding, ThumbnailSource and
ActiveGifSource) is not being used by the card templates, so GIF hover previews
never render; update each card thumbnail host (Browse, Library, Playlist and the
other occurrences noted around lines 152-155, 326-329, 1022-1025) to use the
ThumbnailDisplay template instead of directly instantiating AdvancedImage—e.g.
replace the inline AdvancedImage block with a control that applies the
ThumbnailDisplay ControlTemplate (ensuring the control's DataContext is the
vm:WallpaperCardViewModel so ThumbnailSource, ActiveGifSource and IsGifActive
bindings work), and verify the Viewbox/gif:GifImage path from the template is
preserved so hover GIFs display.

In `@src/livepaper/Views/MainWindow.axaml.cs`:
- Around line 184-185: The deselect branch currently clears selection whenever
no card is hit, which also fires for scrollbar clicks; update the event handler
that contains the null-card check so it first inspects the event's
original/source element (e.g., PointerPressedEventArgs.OriginalSource or
e.Source) and returns early if the click originated from a ScrollBar (or a child
of a ScrollBar/ScrollViewer) instead of calling Vm?.DeselectAllLibrary(); keep
the existing Vm?.SelectCard(card, shift, ctrl) behavior when a card is present;
reference the null-card branch and the calls to Vm?.DeselectAllLibrary() and
Vm?.SelectCard(...) when making the change.

---

Outside diff comments:
In `@src/livepaper/ViewModels/MainWindowViewModel.cs`:
- Around line 1106-1128: SelectCard currently only clears selection on the
visible subset "displayed" (derived from FilteredLibraryWallpapers), so
hidden/filtered items remain selected on a plain click; change the plain-click
branch in SelectCard to clear IsSelected across the full library collection (the
underlying collection that backs FilteredLibraryWallpapers) instead of just
iterating "displayed", then preserve the existing wasOnlySelected logic and set
the clicked card as selected when appropriate. Locate the plain-click else
branch in SelectCard and replace the foreach over "displayed" with a foreach
over the full library collection (the authoritative list of
WallpaperCardViewModel instances) so DeleteSelected, playlist actions, and
LibrarySelectedCount reflect the true single selection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cffa3646-6333-474a-87ef-8573e75cd0e3

📥 Commits

Reviewing files that changed from the base of the PR and between e4828ba and 82bfddb.

📒 Files selected for processing (12)
  • .claude/rules/session.md
  • .claude/rules/ui.md
  • src/livepaper/App.axaml
  • src/livepaper/Helpers/LibraryService.cs
  • src/livepaper/Helpers/PlayerHelper.cs
  • src/livepaper/Models/AppSettings.cs
  • src/livepaper/Models/LibraryItem.cs
  • src/livepaper/ViewModels/MainWindowViewModel.cs
  • src/livepaper/ViewModels/WallpaperCardViewModel.cs
  • src/livepaper/Views/MainWindow.axaml
  • src/livepaper/Views/MainWindow.axaml.cs
  • src/livepaper/livepaper.csproj


public static string? QueryCurrentPath() => TryQueryCurrentPath();

public static string? QueryCurrentSceneWorkshopId() => null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't expose a silent no-op as the scene query API.

This always returns null, so callers cannot distinguish “no active scene” from “scene lookup not implemented”. Given this stack adds currently-playing restoration/highlighting, scene-backed items will stay unresolved through this path until the lookup is implemented or the unsupported state is made explicit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/livepaper/Helpers/PlayerHelper.cs` at line 716, The method
QueryCurrentSceneWorkshopId currently returns null silently which hides "not
implemented" vs "no active scene"; replace the no-op with an explicit
unsupported indicator by throwing NotImplementedException or
NotSupportedException from QueryCurrentSceneWorkshopId so callers can detect
unimplemented lookup, and add/update its XML doc comment to state that it throws
when scene lookup is unsupported; ensure any callers of
QueryCurrentSceneWorkshopId are updated to catch/handle this exception or use an
alternative API that distinguishes "no active scene" from "not implemented".

Comment on lines +718 to +733
private static string? TryQueryCurrentPath()
{
var socketPath = IpcSocket;
if (!File.Exists(socketPath)) return null;
try
{
using var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
socket.SendTimeout = 500;
socket.ReceiveTimeout = 500;
socket.Connect(new UnixDomainSocketEndPoint(socketPath));
var cmd = JsonSerializer.Serialize(new { command = new object[] { "get_property", "path" } });
socket.Send(Encoding.UTF8.GetBytes(cmd + "\n"));
var buf = new byte[4096];
int n = socket.Receive(buf);
using var doc = JsonDocument.Parse(buf.AsMemory(0, n));
if (doc.RootElement.TryGetProperty("data", out var data) && data.ValueKind == JsonValueKind.String)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file and show the relevant method body with line numbers
FILE="src/livepaper/Helpers/PlayerHelper.cs"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE"
  exit 1
fi

# Print a focused window around the indicated line range
sed -n '680,780p' "$FILE" | nl -ba -w2 -s': '

# Search for other mpv IPC receive/parsing helpers in the repo (unix socket / newline handling)
rg -n "TryQueryCurrentPath|mpv.*ipc|UnixDomainSocketEndPoint|socket\.Receive\(|\\n'|newline|IndexOf\\(buf" src/livepaper/Helpers/PlayerHelper.cs src/livepaper/Helpers -S || true

# Check for any shared IPC helper methods that might already implement newline-framing
rg -n "Read.*until|while \\(true\\).*Receive|newline-delimited|\\b\\n\\b|IndexOf\\(.*'\\\\n'|delimiter" src/livepaper/Helpers -S || true

Repository: sunwoo101/livepaper

Length of output: 107


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/livepaper/Helpers/PlayerHelper.cs"
test -f "$FILE" || { echo "Missing file: $FILE"; exit 1; }

# Show focused window around the indicated range with line numbers
sed -n '690,760p' "$FILE" | awk '{printf "%4d: %s\n", NR+689, $0}'

# Search for other IPC receive/parsing helpers (newline framing, loops, etc.)
rg -n --hidden --no-ignore-vcs "UnixDomainSocketEndPoint|socket\.Receive\(|JsonDocument\.Parse\(|TryQueryCurrentPath|mpv.*ipc|IndexOf\\(.*'\\\\n'|newline-delimited|while \\(true\\)" src/livepaper/Helpers -S || true

# Search for any shared framing logic in the whole repo (small scope first)
rg -n --hidden --no-ignore-vcs "newline|\\\\n.*delimiter|while \\(true\\).*Receive|IndexOf\\(buf, \\(byte\\)'\\\\n'|delimited" src/livepaper -S || true

Repository: sunwoo101/livepaper

Length of output: 4086


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE1="src/livepaper/Helpers/PlayerHelper.cs"
FILE2="src/livepaper/Helpers/AudioMonitor.cs"

# Show context around the other while(true) in PlayerHelper.cs
sed -n '320,430p' "$FILE1" | awk '{printf "%4d: %s\n", NR+319, $0}'

# Show context around the other socket.Connect occurrences
sed -n '930,1020p' "$FILE1" | awk '{printf "%4d: %s\n", NR+929, $0}'
sed -n '1020,1090p' "$FILE1" | awk '{printf "%4d: %s\n", NR+1019, $0}'

# Show context in AudioMonitor.cs around its while(true)
sed -n '120,230p' "$FILE2" | awk '{printf "%4d: %s\n", NR+119, $0}'

# Look for any parsing logic that reads until newline or scans received bytes for '\n'
rg -n --hidden --no-ignore-vcs "IndexOf\\(buf.*'\\\\n'|Read.*until.*\\\\n|delimiter|memchr|while \\(true\\).*Receive|Parse\\(.*\\n|\\\\n\\\\" src/livepaper/Helpers/PlayerHelper.cs src/livepaper/Helpers/AudioMonitor.cs -S || true

Repository: sunwoo101/livepaper

Length of output: 15938


🌐 Web query:

mpv input-ipc-server newline delimited JSON reply stream socket framing

💡 Result:

Answer (mpv --input-ipc-server JSON framing over stream sockets) 1) Transport/framing - The JSON IPC protocol uses a stream socket (mpv creates a UNIX domain SOCK_STREAM listening socket) and is message-framed with a line break: every command, reply, and event is separated by a newline character “\n” [1][2]. - Each JSON message must be terminated with “\n” and “\n” must not appear anywhere inside the message; therefore clients should minify JSON before sending [1]. 2) Client -> mpv: what to send - If you send JSON commands, they are native JSON values inside a single object (e.g., {"command": [...]}); mpv expects each message to end with “\n” and does not allow embedded newlines within the message text [1]. - If the first non-whitespace character is not “{”, mpv treats the line as a non-JSON text command (and in that case the command is like input.conf-style and is not part of the JSON request/reply flow) [1]. 3) mpv -> client: replies/events - mpv writes JSON messages back to the same socket, with the same “one JSON per line” framing: commands, replies, and events are each separated by “\n” [1]. - Implementation-wise, common client libraries read from the socket as a byte stream, buffer until they find a newline, then parse that line as one complete JSON message (example python-mpv reads into a buffer, finds b"\n", extracts data[:newline+1], then parses that single JSON line) [3]. 4) Practical guidance for framing in your client - Treat the socket as an unstructured byte stream. - Accumulate bytes in a buffer. - Split on newline (b"\n") to obtain complete JSON messages. - Parse each complete line as one JSON object. - Do not rely on recv boundaries; you may receive partial JSON lines or multiple lines per read. 5) Why newline delimits matters - If your client doesn’t read events/replies promptly, mpv can block trying to write them (the IPC events fill the socket buffer), which can make it appear that commands “stop working” [4]. Sources: mpv IPC documentation (ipc.rst) and mpv’s socket implementation (ipc-unix.c), plus an example client implementation (python-mpv) [1][2][3] (and related behavior about not draining the socket buffer) [4].

Citations:


Fix mpv IPC framing in TryQueryCurrentPath()
TryQueryCurrentPath() reads only once from a Unix SOCK_STREAM and parses whatever bytes arrive as a complete JSON message. mpv’s --input-ipc-server protocol is newline-delimited (“one JSON per line”), so a single Socket.Receive(...) can return a partial line or multiple lines, making JsonDocument.Parse(...) fail and returning null intermittently.

Suggested fix
 private static string? TryQueryCurrentPath()
 {
     var socketPath = IpcSocket;
     if (!File.Exists(socketPath)) return null;
     try
     {
         using var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
         socket.SendTimeout = 500;
         socket.ReceiveTimeout = 500;
         socket.Connect(new UnixDomainSocketEndPoint(socketPath));
         var cmd = JsonSerializer.Serialize(new { command = new object[] { "get_property", "path" } });
         socket.Send(Encoding.UTF8.GetBytes(cmd + "\n"));
-        var buf = new byte[4096];
-        int n = socket.Receive(buf);
-        using var doc = JsonDocument.Parse(buf.AsMemory(0, n));
+        using var response = new MemoryStream();
+        var buf = new byte[512];
+        while (true)
+        {
+            int n = socket.Receive(buf);
+            if (n <= 0) return null;
+
+            int newline = Array.IndexOf(buf, (byte)'\n', 0, n);
+            if (newline >= 0)
+            {
+                response.Write(buf, 0, newline + 1);
+                break;
+            }
+
+            response.Write(buf, 0, n);
+        }
+
+        using var doc = JsonDocument.Parse(response.ToArray());
         if (doc.RootElement.TryGetProperty("data", out var data) && data.ValueKind == JsonValueKind.String)
             return data.GetString();
         return null;
     }
     catch { return null; }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static string? TryQueryCurrentPath()
{
var socketPath = IpcSocket;
if (!File.Exists(socketPath)) return null;
try
{
using var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
socket.SendTimeout = 500;
socket.ReceiveTimeout = 500;
socket.Connect(new UnixDomainSocketEndPoint(socketPath));
var cmd = JsonSerializer.Serialize(new { command = new object[] { "get_property", "path" } });
socket.Send(Encoding.UTF8.GetBytes(cmd + "\n"));
var buf = new byte[4096];
int n = socket.Receive(buf);
using var doc = JsonDocument.Parse(buf.AsMemory(0, n));
if (doc.RootElement.TryGetProperty("data", out var data) && data.ValueKind == JsonValueKind.String)
private static string? TryQueryCurrentPath()
{
var socketPath = IpcSocket;
if (!File.Exists(socketPath)) return null;
try
{
using var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
socket.SendTimeout = 500;
socket.ReceiveTimeout = 500;
socket.Connect(new UnixDomainSocketEndPoint(socketPath));
var cmd = JsonSerializer.Serialize(new { command = new object[] { "get_property", "path" } });
socket.Send(Encoding.UTF8.GetBytes(cmd + "\n"));
using var response = new MemoryStream();
var buf = new byte[512];
while (true)
{
int n = socket.Receive(buf);
if (n <= 0) return null;
int newline = Array.IndexOf(buf, (byte)'\n', 0, n);
if (newline >= 0)
{
response.Write(buf, 0, newline + 1);
break;
}
response.Write(buf, 0, n);
}
using var doc = JsonDocument.Parse(response.ToArray());
if (doc.RootElement.TryGetProperty("data", out var data) && data.ValueKind == JsonValueKind.String)
return data.GetString();
return null;
}
catch { return null; }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/livepaper/Helpers/PlayerHelper.cs` around lines 718 - 733,
TryQueryCurrentPath currently calls Socket.Receive once and attempts to
JsonDocument.Parse that single read; because mpv IPC is newline-delimited, you
must instead read from the stream until you have at least one full line (\\n),
accumulate bytes into a buffer/string, then split on newlines and parse the
first non-empty line as JSON. Update the logic around the socket Receive loop in
TryQueryCurrentPath to repeatedly Receive into a growing buffer (respecting
SendTimeout/ReceiveTimeout), break when a newline is seen or socket closed,
extract the first complete line, and JsonDocument.Parse that line (handle
partial/multiple messages by ignoring subsequent data for this call). Ensure
exceptions/timeouts still return null and the socket is disposed.

Comment thread src/livepaper/ViewModels/MainWindowViewModel.cs
Comment on lines +21 to +23
public bool IsScene { get; }
public string? WorkshopId { get; }
public bool IsGifThumbnail => ThumbnailSource.EndsWith(".gif", StringComparison.OrdinalIgnoreCase);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Initialize the new scene metadata.

These properties are never assigned in either constructor, so every card keeps the default false/null. That makes LoadDurationAsync() probe scene entries and prevents the workshop-id restore path from ever finding a matching card.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/livepaper/ViewModels/WallpaperCardViewModel.cs` around lines 21 - 23, The
new properties IsScene and WorkshopId on WallpaperCardViewModel are never set,
causing incorrect defaults and breaking LoadDurationAsync's logic; update both
constructors of WallpaperCardViewModel to initialize IsScene and WorkshopId from
the incoming model/data (e.g., use the source object or parameters used to build
the view model) so IsScene reflects whether the card is a scene and WorkshopId
contains the original workshop identifier, ensuring LoadDurationAsync and the
workshop-id restore path can find matching cards.

Comment thread src/livepaper/Views/MainWindow.axaml Outdated
Comment thread src/livepaper/Views/MainWindow.axaml.cs
Kryxzort and others added 10 commits May 25, 2026 12:49
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Port fork App.axaml: button press animation, ghost hover foreground,
  thin scrollbar with transitions, library-hover-outline CSS,
  AccentHover/DangerBg/Success colors, DynamicResource throughout
- BrowseScrollViewer/LibraryScrollViewer: swap Padding for
  AllowAutoHide=False + HorizontalScrollBarVisibility=Disabled
  (prevents scrollbar from clipping card content)
- Settings ScrollViewer: add HorizontalScrollBarVisibility=Disabled
- Browse and Library card overlay: wrap in Panel with library-hover-outline
  border so all cards show visual feedback on hover (not just scenes)
- Library header: move Search + Sort to the right of Play All
- Sort button: Width=28, MenuFlyout Placement=BottomEdgeAlignedRight
- Template AdvancedImage: add RenderOptions.BitmapInterpolationMode=LowQuality

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace emoji/symbol text with mi:MaterialIcon:
- Browse refresh: ↺ → Kind=Refresh
- Playlist settings: ⚙ → Kind=Tune
- Load playlist: 📂 → Kind=FolderOpen
- Save playlist: 💾 → Kind=ContentSave
- Play playlist: ▶ Play → Kind=Play + "Play" label

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove play-overlay (dim + ▶ on hover) and always-visible circular − badge
- Match fork: small ✕ ghost button that fades in on card hover only
- PlaylistScrollViewer: AllowAutoHide=False so the horizontal scrollbar
  is permanently visible below the thumbnails
- Remove ClipToBounds=True from the playlist grid so the scrollbar
  is not clipped

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Keep the always-visible accent circular remove button instead of
the hover-only ✕ ghost button from the previous commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Anchor playlist strip to bottom of window as global grid row, visible only on Library tab
- Playlist settings interval/advance controls show global values when override is off
- Remove side margins from thumbnail track so clips run edge-to-edge; header retains inset
- Add 8px scroll-content margin so first/last thumbnails have breathing room at scroll extremes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Kryxzort and others added 7 commits May 25, 2026 12:50
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add PointerEntered/Exited to Browse, Library, and Playlist card borders
- Add OnCardPointerEntered/Exited handlers in code-behind
- Clear IsGifActive on drag end in OnPointerCaptureLost and OnPointerReleased
- Fire OnWallpaperChanged with first path (not null) in ApplyPlaylist multi-path
- Add Avalonia.Labs.Gif package reference
Kryxzort added a commit to Kryxzort/livepaper-pro that referenced this pull request May 25, 2026
PR sunwoo101#20 (UI polish: layout fixes, hover outlines, smooth scroll, playing card highlight)
overlapped heavily with the scene-support, custom-playlist, auto-mute, and per-video
override work already on main. Resolutions:

- PlayerHelper: kept HEAD's scene-aware ApplyPlaylist/timed-mode upgrade, robust
  multi-line JSON IPC parsing, SwitchFromTimedToAdvanceOnEnd/SwitchFromAdvanceOnEndToTimed
  helpers, DoVideoEndWait, restart-daemon coordination, and pending-action IPC.
  Implemented QueryCurrentPath / QueryCurrentSceneWorkshopId properly so the
  daemon-resumed UI highlight finds the playing card. Added OnWallpaperChanged
  invocations from PR so library cards highlight as the playlist advances.

- MainWindowViewModel: kept HEAD's settings handlers (scene/wait-end/refresh/global
  variants) and MakeLibraryCard wiring. Added PR's daemon-resume highlight lookup,
  PR's Stop clearing IsCurrentlyPlaying, and PR's LoadDurationAsync call.
  OnWallpaperChanged now flips IsCurrentlyPlaying off the previous card and on the
  new one. Removed duplicate Display* getters introduced by the merge.

- WallpaperCardViewModel: kept HEAD's AnimatedImage.Avalonia infrastructure
  (StaticThumbnailSource, GifSource/PlaylistGifSource with explicit disposal, IsGifActive
  partial). Already had LoadDurationAsync and VideoDuration from HEAD, so PR's
  duplicate declaration is dropped.

- MainWindow.axaml: kept HEAD's ThumbnailDisplay 3-layer template (static→fallback
  →animated), auto-search + sort menu, scene/crash UI, full playlist panel, and
  preview modal. Fixed the StaticResource→DynamicResource bug on the Appearance
  border (PR was right per the ui.md rule). PR's preview-modal title-as-TextBox
  rewrite dropped since HEAD's TextBlock layout matches the surrounding modal.

- MainWindow.axaml.cs: kept HEAD's GIF auto-play scroll handlers and the
  drag-aware OnPointerReleased/OnPointerCaptureLost guards. Dropped PR's
  duplicate OnCardPointerEntered/Exited since HEAD already has them.

- csproj: dropped Avalonia.Labs.Gif (PR added it for its alternate GIF impl,
  unused after we kept AnimatedImage.Avalonia).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Kryxzort added a commit to Kryxzort/livepaper-pro that referenced this pull request May 25, 2026
…th scroll, playing card highlight (via merge-fix branch)

Merges sunwoo101#20. Conflicts resolved in merge-fix/pr-20-ui-polish.
Kryxzort added a commit to Kryxzort/livepaper-pro that referenced this pull request May 25, 2026
Picks up the four new PR commits (WorkshopId init from LibraryItem, lazy duration
load on preview open, GIF wiring across browse/library/playlist, scrollbar-aware
deselect). Conflicts resolved:

- WallpaperCardViewModel: PR's WorkshopId init in LibraryItem ctor is redundant
  (HEAD already initialized it); kept HEAD.

- MainWindowViewModel: removed LoadDurationAsync call from MakeLibraryCard (PR's
  lazy-load fix); OpenPreview already invokes it on demand.

- MainWindow.axaml: kept HEAD's ThumbnailDisplay template and AnimatedImage.Avalonia
  GIF infrastructure; rewrote PR's gif:GifImage in the bottom PlaylistPanel to use
  aimg:ImageBehavior.AnimatedSource with PlaylistActiveGifSource (HEAD's existing
  separate animated source for the playlist strip).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Kryxzort added a commit to Kryxzort/livepaper-pro that referenced this pull request May 25, 2026
…-fix branch)

Merges new commits e33687c, 23ed40e, 8955126, fd697cd from sunwoo101#20.
Conflicts resolved in merge-fix/pr-20-ui-polish-v2.
Kryxzort added a commit to Kryxzort/livepaper-pro that referenced this pull request May 25, 2026
PR sunwoo101#20 added a hidden bottom PlaylistPanel containing a second copy of
PlaylistScrollViewer / PlaylistItemsControl / PlaylistDropIndicator. The Library
tab already has the inline playlist strip with the same x:Names, so Avalonia's
NameScope rejected the duplicate registration at window construction.

Dropped the unused PR-introduced PlaylistPanel and the PlaylistPanel.IsVisible
tab-change handler. The Library tab's inline strip stays (the working design).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant