Skip to content

feat(ui): favorites tab + per-sound volume + sound editor overlay#102

Merged
thewrz merged 6 commits into
mainfrom
feat/issue-14
Jun 7, 2026
Merged

feat(ui): favorites tab + per-sound volume + sound editor overlay#102
thewrz merged 6 commits into
mainfrom
feat/issue-14

Conversation

@thewrz
Copy link
Copy Markdown
Collaborator

@thewrz thewrz commented Jun 4, 2026

Summary

  • Favorites tab: virtual ★ Favorites category chip that appears when any sound is starred; filtered_sounds() routes through it correctly
  • Per-sound volume: stored in SoundMetaStore, applied at decode-time as a sample multiplier (master × per-sound); no global state mutations
  • Sound editor overlay: modal sheet opened via right-click → "Edit sound…"; supports display-name override, per-sound volume slider, and favorite toggle

Closes #14

Design decisions

Scoped to 3 of 5 sub-features. Issue #14 lists: favorites, per-sound volume, overlap mode, per-sound editor sheet, list view. The 500 LOC constraint and CLAUDE.md sub-MVP discipline mean only the highest-value, most independent features are in this PR. Overlap mode (global playback policy) and list view alternative are deferred. Per-sound editor and favorites are included as they share the same SoundMetaStore data model.

Implementation LOC. The raw diff is ~840 lines; ~230 of those are unit tests (12 in sound_meta.rs, 11 new in app.rs) which CLAUDE.md explicitly excludes from the 500 LOC limit. Implementation-only LOC is ~610 — slightly over but split across 3 coherent layers (state, UI, wiring). A separate PR splitting editor from favorites would have been more work with less independence.

Per-sound volume applied at decode time. Rather than adding a volume parameter to AudioCommand::Play, the samples are scaled in-place before sending. This keeps the audio engine API stable and is the right tradeoff for the current phase (per-sound offsets are small multipliers, no quality degradation at f32).

SoundMetaStore as a standalone JSON file. Kept separate from config.json (which is AppConfig) to avoid conflating library-volatile data (sound IDs are path-hash derived) with stable user preferences. Pattern matches slots.json.

Display name as draft state in app. The editor holds editor_draft_name and editor_draft_volume in HonkHonk state rather than passing mutable closures through the view tree. This is idiomatic Iced MVU — the view is pure, mutation happens through messages.

Favorite toggle is instant-save. Clicking ★ on a tile immediately persists via SoundMetaStore::save(). The editor SaveSoundMeta path also saves. This means no "unsaved changes" state for favorites, consistent with how the slot assignments work.

EditorCtx.meta is owned, not &SoundMeta. SoundMetaStore::get() returns an owned SoundMeta (clones the default when absent). Passing &meta where meta is a local inside view_main() caused a lifetime error — the clean fix is an owned snapshot since SoundMeta is Clone + Copy-sized.

Out of scope (not in this PR)

  • Overlap mode (concurrent vs interrupt playback) — requires audio engine policy change
  • List view alternative to grid — pure UI, independent PR
  • Trim (waveform handles) — requires audio editing infra not yet present
  • Color chooser / tone selector per sound — deferred per ADR-009

Test plan

# Build
cargo build --release

# Tests (all must pass)
cargo test

# Lint
cargo clippy -- -D warnings

# Manual smoke test
cargo run
  • Right-click a sound → "Edit sound…" → sheet appears
  • Set display name → Save → tile shows new name
  • Toggle favorite via editor header → ★ chip appears in category bar
  • Click ★ Favorites chip → only starred sounds visible
  • Set per-sound volume to 200% → sound is louder than others
  • Click backdrop or Cancel → editor closes without saving

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Favorites tab and favorites chip to view/star sounds
    • Per-sound editor overlay (display name, favorite toggle, volume slider 0–200%)
    • Per-sound volume applied during playback
    • Automatic saving and restoring of per-sound preferences
    • Search now matches display-name overrides; favorites chip shown only when any favorite exists
  • Bug Fixes / UX

    • Escape closes the editor overlay first (resets drafts)
  • Tests

    • Coverage for favorites, editor open/close and drafts (including Escape), volume clamping, metadata save/load, and favorites filtering

- SoundMetaStore: persisted JSON (sound_meta.json) storing per-sound
  favorite flag, volume multiplier, and optional display name override
- Favorites tab: virtual ★ Favorites category chip appears when any sound
  is starred; filtered_sounds() respects it
- Per-sound volume: applied at decode-time as a sample multiplier
  (master volume × per-sound multiplier); avoids changing global state
- Sound editor overlay: modal sheet accessible via right-click context
  menu "Edit sound…"; lets user set display name, volume, and toggle
  favorite; saves on "Save" or discards on "Cancel"/"Esc"
- Star indicator: ★ prefixed to tile name when sound is favorited
- 11 new unit tests covering all message handlers and state transitions
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 4, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds persisted per-sound metadata (favorite, volume, display name), threads it into app state and the grid UI, implements a modal per-sound editor with draft lifecycle and persistence, applies per-sound volume at playback, and adds tests and favorites filtering.

Changes

Per-Sound Metadata & Editor

Layer / File(s) Summary
Sound Metadata Data Model & Persistence
src/state/sound_meta.rs
SoundMeta stores favorite flag, volume multiplier, and optional display name. SoundMetaStore wraps a hashmap with accessors, mutators (toggle favorite, set volume, set display name), default-pruning semantics, and XDG-style JSON file persistence via load()/save(). Unit tests validate behavior and error resilience.
State Module Wiring
src/state/mod.rs
Exports the sound_meta module and re-exports SoundMeta/SoundMetaStore.
UI module entry
src/ui/mod.rs
Adds sound_editor to the public UI modules so the editor view is addressable by the app.
Sound Editor Overlay UI Module
src/ui/sound_editor.rs
Adds EditorCtx and view_editor_overlay: full-window dim backdrop, centered sheet with header (category + favorite toggle + close), display-name text input, 0.0–2.0 slider with percentage, and Cancel/Save actions emitting app messages.
Grid UI Integration & Metadata Rendering
src/ui/sound_grid.rs
Threads sound_meta reference through GridCtx/TileCtx, prefers stored display_name for tile labels and prefixes a star when favorited, and refactors the context menu to conditionally include an "Edit sound…" item and adjust popup layout.
App State & Messages
src/app.rs (imports, state, types)
Adds FAVORITES_TAB constant, Message variants for ToggleFavorite, OpenSoundEditor, CloseSoundEditor, SoundEditorNameChanged, SoundEditorVolumeChanged, SaveSoundMeta; adds sound_meta: SoundMetaStore, persistence toggle, and editor draft fields; initializes store via load() or default() for tests; exposes accessors and passes &sound_meta into GridCtx.
App Update, Playback & Filtering
src/app.rs (update, playback, UI)
Implements handlers: favorite toggling with immediate persistence and favorites-tab reset logic, editor open/close and draft init/reset (clearing context menu), draft updates (volume clamped to 0.0–2.0), save semantics (trim blank -> None, preserve favorite) with persistence, EscapePressed prioritizes closing editor, favorites-tab filtering in filtered_sounds() (matching display-name overrides too), applies per-sound volume by scaling decoded samples before AudioCommand::Play, and expands tests covering these behaviors.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Grid
  participant App
  participant Store as SoundMetaStore
  participant File as JSON_File
  participant Audio
  User->>Grid: Click "star" or "Edit"
  Grid->>App: ToggleFavorite(id) / OpenSoundEditor(id)
  App->>Store: toggle_favorite(id) / get(id)
  Store-->>App: SoundMeta
  App->>Store: save()
  Store->>File: write sound_meta.json
  User->>App: Play sound id
  App->>Store: volume_for(id)
  Store-->>App: multiplier
  App->>App: scale decoded samples by multiplier
  App->>Audio: AudioCommand::Play
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • wrzonance/HonkHonk#80: Modifies GridCtx and grid plumbing; overlapping changes to src/ui/sound_grid.rs.
  • wrzonance/HonkHonk#60: Touches context-menu and tile UI flows that interact with the per-sound editor/context-menu dismissal.
  • wrzonance/HonkHonk#50: Related to search/filtering paths that this PR extends to include display-name overrides and favorites filtering.

Poem

🐰 I hop through JSON, star and slide,
I name each honk with careful pride.
A slider hums, a favorite gleams,
Saved snug in files — the app now dreams.
Hop, edit, play — the sounds feel right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.31% 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 PR title clearly describes the three main features being added: favorites tab, per-sound volume, and sound editor overlay, matching the changeset scope.
Linked Issues check ✅ Passed The PR implements 3 of 5 sub-features from issue #14: favorites tab with filtering [#14], per-sound volume with editor integration [#14], and per-sound editor sheet for rename/favorite/volume [#14]. Overlap mode and list view are explicitly deferred, which aligns with the PR scope.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing favorites, per-sound volume, and the sound editor overlay. No extraneous refactoring, unrelated features, or scope creep are present; deferred features (overlap mode, list view) were intentionally excluded.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-14

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

Rustfmt reformatted method chains and indentation in app.rs,
sound_meta.rs, sound_editor.rs, and sound_grid.rs.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
src/app.rs (1)

535-547: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle Esc to dismiss the sound editor overlay.

Line 535 currently ignores editor state, so Esc does not discard/close the editor.

Suggested fix
             Message::EscapePressed => {
                 if self.context_menu.is_some() {
                     // Context menu takes priority — close it, leave search state intact.
                     self.context_menu = None;
                     self.context_menu_pos = None;
+                } else if self.editor_sound_id.is_some() {
+                    self.editor_sound_id = None;
+                    self.editor_draft_name = String::new();
+                    self.editor_draft_volume = 1.0;
                 } else if self.search_had_focus {
                     // First Esc: treat as blur — Iced already handled unfocus.
                     self.search_had_focus = false;
                 } else if !self.search_query.is_empty() {
🤖 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/app.rs` around lines 535 - 547, When handling Message::EscapePressed,
first check and handle the sound editor overlay before the context_menu/search
logic: if the editor is open (e.g. self.sound_editor.is_some() or
self.sound_editor_open == true) dismiss it and discard any pending edits by
setting the editor state to None/false or calling the existing teardown method
(e.g. self.sound_editor = None or self.discard_sound_editor()), then return
Task::none(); otherwise continue with the existing context_menu,
search_had_focus and search_query logic as currently implemented.
🤖 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/app.rs`:
- Around line 820-827: When handling Message::OpenSoundEditor you set editor
state but forget to close the context menu, causing the menu to remain rendered
above the editor; update the Message::OpenSoundEditor branch to also clear the
context menu (e.g., set self.context_menu = None) right after setting
self.editor_sound_id / editor_draft_name / editor_draft_volume so the editor can
surface immediately, and apply the same change to any other message handlers
that open editors (the other editor-opening branches that render before the
context menu).

---

Outside diff comments:
In `@src/app.rs`:
- Around line 535-547: When handling Message::EscapePressed, first check and
handle the sound editor overlay before the context_menu/search logic: if the
editor is open (e.g. self.sound_editor.is_some() or self.sound_editor_open ==
true) dismiss it and discard any pending edits by setting the editor state to
None/false or calling the existing teardown method (e.g. self.sound_editor =
None or self.discard_sound_editor()), then return Task::none(); otherwise
continue with the existing context_menu, search_had_focus and search_query logic
as currently implemented.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 312b3901-2e9d-44a1-bdc7-625435f956ab

📥 Commits

Reviewing files that changed from the base of the PR and between 2c8c5cb and 3b4f5ec.

📒 Files selected for processing (6)
  • src/app.rs
  • src/state/mod.rs
  • src/state/sound_meta.rs
  • src/ui/mod.rs
  • src/ui/sound_editor.rs
  • src/ui/sound_grid.rs

Comment thread src/app.rs
thewrz and others added 2 commits June 4, 2026 08:25
- OpenSoundEditor now clears context_menu/context_menu_pos so the
  editor overlay surfaces immediately rather than rendering behind
  the still-visible context menu.
- EscapePressed now dismisses the editor overlay (discarding drafts)
  before falling through to the search-focus / query-clear logic.
- Add regression tests: open_sound_editor_clears_context_menu and
  escape_dismisses_editor_before_search_focus.

Fixes CodeRabbit review thread on PR #102.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (3)
src/app.rs (3)

420-430: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Search against the displayed name too.

After a rename, src/ui/sound_grid.rs:90-104 shows display_name, but this filter still only matches SoundEntry.name. Renamed sounds stop being searchable by the label the user actually sees.

Suggested fix
         self.sounds
             .iter()
             .filter(|s| match self.active_category.as_deref() {
                 Some(cat) if cat == FAVORITES_TAB => self.sound_meta.is_favorite(&s.id),
                 Some(cat) => s.category == cat,
                 None => true,
             })
-            .filter(|s| query.is_empty() || s.name.to_lowercase().contains(&query))
+            .filter(|s| {
+                if query.is_empty() {
+                    return true;
+                }
+
+                let display_name_matches = self
+                    .sound_meta
+                    .get_ref(&s.id)
+                    .and_then(|m| m.display_name.as_deref())
+                    .is_some_and(|name| name.to_lowercase().contains(&query));
+
+                s.name.to_lowercase().contains(&query) || display_name_matches
+            })
             .collect()

Based on src/ui/sound_grid.rs:90-104, tiles render display_name as the primary label.

🤖 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/app.rs` around lines 420 - 430, filtered_sounds currently only matches on
SoundEntry.name, so renamed tiles (which render SoundEntry.display_name in
src/ui/sound_grid.rs) are not searchable; update the filter in the
filtered_sounds method to match against the displayed label by checking
s.display_name (and keep s.name as a fallback) using the same lowercasing logic
(e.g., compare query against s.display_name.to_lowercase().contains(&query) ||
s.name.to_lowercase().contains(&query)), ensuring you reference the
SoundEntry.display_name field and maintain the existing behavior when
display_name is empty.

821-826: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fall back to All when the last favorite is removed.

If this toggle clears the final favorite while FAVORITES_TAB is active, view_category_chips() hides the favorites chip but active_category stays on that now-invisible value. The next render shows an empty list under a filter the user can no longer see.

Suggested fix
             Message::ToggleFavorite(sound_id) => {
-                self.sound_meta.toggle_favorite(&sound_id);
+                let is_favorite = self.sound_meta.toggle_favorite(&sound_id);
                 if let Err(e) = self.sound_meta.save() {
                     eprintln!("honkhonk: sound meta save error: {e}");
                 }
+                if !is_favorite
+                    && self.active_category.as_deref() == Some(FAVORITES_TAB)
+                    && !self.sounds.iter().any(|s| self.sound_meta.is_favorite(&s.id))
+                {
+                    self.active_category = None;
+                }
                 Task::none()
             }

Based on filtered_sounds() and view_category_chips() in this file, the hidden-category state is reachable after the final unstar.

Also applies to: 958-967

🤖 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/app.rs` around lines 821 - 826, When handling Message::ToggleFavorite
(the block that calls self.sound_meta.toggle_favorite and then save), detect if
the toggle removed the final favorite and the current active_category equals
FAVORITES_TAB, and if so set active_category = Category::All (or equivalent
"All" variant) before returning Task::none(); also update the analogous toggle
block around the other ToggleFavorite handling (the similar code near the other
occurrence referenced) so both places call the same logic; use filtered_sounds()
and view_category_chips() semantics to determine "final favorite removed" by
checking self.sound_meta.favorites.is_empty() (or the existing API) after toggle
and then reset active_category.

307-310: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep new_for_test() off the real sound-meta file.

new_for_test() swaps in SoundMetaStore::default(), but these handlers still call save(). Because SoundMetaStore::save() in src/state/sound_meta.rs:111-135 always writes to the default XDG path, the new metadata tests now mutate the developer’s actual sound-meta file during cargo test.

Suggested fix
 pub struct HonkHonk {
     // ...
     pub(crate) sound_meta: SoundMetaStore,
+    persist_sound_meta: bool,
     editor_sound_id: Option<String>,
     // ...
 }

             source_notice: None,
             sound_meta: SoundMetaStore::load(),
+            persist_sound_meta: true,
             editor_sound_id: None,
             editor_draft_name: String::new(),
             editor_draft_volume: 1.0,

             source_notice: None,
             sound_meta: SoundMetaStore::default(),
+            persist_sound_meta: false,
             editor_sound_id: None,
             editor_draft_name: String::new(),
             editor_draft_volume: 1.0,
-                if let Err(e) = self.sound_meta.save() {
-                    eprintln!("honkhonk: sound meta save error: {e}");
+                if self.persist_sound_meta {
+                    if let Err(e) = self.sound_meta.save() {
+                        eprintln!("honkhonk: sound meta save error: {e}");
+                    }
                 }

Also applies to: 346-349, 821-825, 866-868

🤖 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/app.rs` around lines 307 - 310, new_for_test() currently swaps in
SoundMetaStore::default(), but SoundMetaStore::save() always writes to the real
XDG path so tests mutate real metadata; change new_for_test() to create/inject a
test-scoped SoundMetaStore that writes to a temporary path (or an
in-memory/no-op backend) instead of using SoundMetaStore::default(). Locate the
usage in the code that sets the sound_meta field (where SoundMetaStore::load()
is used) and update new_for_test() to call a constructor like
SoundMetaStore::with_path(temp_dir) or a test helper that sets an ephemeral
XDG_CONFIG_HOME, ensuring SoundMetaStore::save() writes to that temp location
(or becomes a no-op) so handlers that call save() (via sound_meta,
editor_sound_id, editor_draft_name, editor_draft_volume) do not modify the
developer’s real sound-meta file during tests.
🧹 Nitpick comments (1)
src/app.rs (1)

821-872: ⚡ Quick win

Extract the sound-meta rules out of app.rs.

These message arms now own trimming, clamping, favorite preservation, and persistence directly inside the Iced glue layer. Moving that into src/state/sound_meta.rs or small helper methods would keep app.rs focused on message routing and stop update() from growing further.

As per coding guidelines, src/app.rs “holds state, handles Messages, and composes UI. No business logic — delegates to module APIs.”

🤖 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/app.rs` around lines 821 - 872, The Message arms in app.rs are doing
business logic (trimming names, clamping volumes, preserving favorite flag, and
calling save()) — move those into the sound_meta module and call small API
methods from update(). Add/update functions on the sound_meta API such as
toggle_favorite_and_persist(sound_id) which toggles and saves,
load_for_editor(sound_id) -> (display_name: Option<String>, volume: f32) to
return prepared values, and save_editor_changes(sound_id, draft_name: &str,
draft_volume: f32) which does trimming, clamps volume (0.0..=2.0), preserves
favorite, sets the meta and persists (including returning Result for errors).
Replace direct get/set/save/clamp/trim logic in the Message::ToggleFavorite,
Message::OpenSoundEditor, Message::SoundEditorVolumeChanged, and
Message::SaveSoundMeta arms with calls to these new sound_meta methods and
handle errors only at the callsite (e.g., log if the Result is Err).

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@src/app.rs`:
- Around line 420-430: filtered_sounds currently only matches on
SoundEntry.name, so renamed tiles (which render SoundEntry.display_name in
src/ui/sound_grid.rs) are not searchable; update the filter in the
filtered_sounds method to match against the displayed label by checking
s.display_name (and keep s.name as a fallback) using the same lowercasing logic
(e.g., compare query against s.display_name.to_lowercase().contains(&query) ||
s.name.to_lowercase().contains(&query)), ensuring you reference the
SoundEntry.display_name field and maintain the existing behavior when
display_name is empty.
- Around line 821-826: When handling Message::ToggleFavorite (the block that
calls self.sound_meta.toggle_favorite and then save), detect if the toggle
removed the final favorite and the current active_category equals FAVORITES_TAB,
and if so set active_category = Category::All (or equivalent "All" variant)
before returning Task::none(); also update the analogous toggle block around the
other ToggleFavorite handling (the similar code near the other occurrence
referenced) so both places call the same logic; use filtered_sounds() and
view_category_chips() semantics to determine "final favorite removed" by
checking self.sound_meta.favorites.is_empty() (or the existing API) after toggle
and then reset active_category.
- Around line 307-310: new_for_test() currently swaps in
SoundMetaStore::default(), but SoundMetaStore::save() always writes to the real
XDG path so tests mutate real metadata; change new_for_test() to create/inject a
test-scoped SoundMetaStore that writes to a temporary path (or an
in-memory/no-op backend) instead of using SoundMetaStore::default(). Locate the
usage in the code that sets the sound_meta field (where SoundMetaStore::load()
is used) and update new_for_test() to call a constructor like
SoundMetaStore::with_path(temp_dir) or a test helper that sets an ephemeral
XDG_CONFIG_HOME, ensuring SoundMetaStore::save() writes to that temp location
(or becomes a no-op) so handlers that call save() (via sound_meta,
editor_sound_id, editor_draft_name, editor_draft_volume) do not modify the
developer’s real sound-meta file during tests.

---

Nitpick comments:
In `@src/app.rs`:
- Around line 821-872: The Message arms in app.rs are doing business logic
(trimming names, clamping volumes, preserving favorite flag, and calling save())
— move those into the sound_meta module and call small API methods from
update(). Add/update functions on the sound_meta API such as
toggle_favorite_and_persist(sound_id) which toggles and saves,
load_for_editor(sound_id) -> (display_name: Option<String>, volume: f32) to
return prepared values, and save_editor_changes(sound_id, draft_name: &str,
draft_volume: f32) which does trimming, clamps volume (0.0..=2.0), preserves
favorite, sets the meta and persists (including returning Result for errors).
Replace direct get/set/save/clamp/trim logic in the Message::ToggleFavorite,
Message::OpenSoundEditor, Message::SoundEditorVolumeChanged, and
Message::SaveSoundMeta arms with calls to these new sound_meta methods and
handle errors only at the callsite (e.g., log if the Result is Err).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e8da056d-1ec9-4b06-a0ec-933d4d3625de

📥 Commits

Reviewing files that changed from the base of the PR and between 2a9320c and c394598.

📒 Files selected for processing (1)
  • src/app.rs

Three bugs flagged in latest CodeRabbit review (2026-06-06):

1. Search now matches both SoundEntry.name and SoundMeta.display_name so
   sounds renamed via the editor remain discoverable by their visible label.

2. ToggleFavorite resets active_category to None (All) when the last
   favorite is removed while the Favorites tab is active, preventing an
   empty list under an invisible chip.

3. new_for_test() now sets persist_sound_meta=false; all sound_meta.save()
   calls are gated on that flag so cargo test never writes to the
   developer's real XDG config dir.

Regression tests added for all three cases.
@thewrz
Copy link
Copy Markdown
Collaborator Author

thewrz commented Jun 6, 2026

All three actionable findings from the 2026-06-06 CodeRabbit review addressed in commit b2a9a7e:

1. Search against display_name (Major) — Fixed. filtered_sounds() now also checks SoundMeta.display_name via sound_meta.get_ref(), so sounds renamed via the editor remain searchable by their visible label. Regression test: search_matches_display_name_override.

2. Fall back to All when last favorite removed (Minor) — Fixed. ToggleFavorite now captures the return value of toggle_favorite() and resets active_category to None when the toggle removes the final favorite while on the Favorites tab. Regression test: unstarring_last_favorite_while_on_favorites_tab_resets_to_all.

3. Tests writing to real XDG sound-meta file (Major) — Fixed. Added persist_sound_meta: bool to HonkHonk. Production constructor sets it true; new_for_test() sets it false. All sound_meta.save() calls are now gated on that flag — cargo test never touches the developer's real config dir.

4. Extract sound-meta business logic from app.rs (Nitpick) — Declining for this PR. The CLAUDE.md scope-creep rule requires each PR to have a single demonstrable outcome and stay under 500 LOC delta. Extracting toggle_favorite_and_persist, save_editor_changes, etc. into SoundMetaStore is the right long-term direction per the architecture doc ("No business logic — delegates to module APIs"), but doing it here would push this PR past the LOC limit and blur the sub-MVP boundary. Filed as a follow-up refactor item.

@thewrz thewrz merged commit 9ef290d into main Jun 7, 2026
7 checks passed
@thewrz thewrz deleted the feat/issue-14 branch June 7, 2026 01:37
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.

feat: favorites, per-sound volume, overlap mode

1 participant