Skip to content

Fix/final polish v1 - #4

Merged
Yefee8 merged 13 commits into
mainfrom
fix/final-polish
Aug 2, 2026
Merged

Fix/final polish v1#4
Yefee8 merged 13 commits into
mainfrom
fix/final-polish

Conversation

@Yefee8

@Yefee8 Yefee8 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

No description provided.

Yefee8 and others added 13 commits August 1, 2026 22:00
`.claude/worktrees/*` holds full checkouts of other branches, each with its
own `modules/audio-focus/package.json`. Four packages claiming the name
`audio-focus` makes the Haste map ambiguous, so `jest.mock('audio-focus')`
cannot resolve it at all — the new engine suite failed to run before its first
line — and every other run printed a duplicate-name warning nobody could act
on.

`testPathIgnorePatterns` already skipped the *tests* there. It does not touch
the module map, which is a separate index.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The same miscount has been reported three times and twice recorded as "fixed
and verified against the database". Both verifications were real device
sessions and both were true when they were written; neither could be re-run,
so neither caught what came after.

The gap they left is specific. `ListenCycle` and `isRewindToRestart` each have
good unit tests and each is correct in isolation. Nothing covered the wiring
between them in `AudioEngine.onStatus` — which is where a listen is opened,
banked and reopened, and where every reported defect has actually lived.

So this replays a scripted status stream through the real engine, with
`expo-audio` behind a fake player and time on the fake timer clock that
`ListenCycle` already reads through `Date.now()`. The scenarios are the ones a
person performs, and their expected answers come from ADR 005 and ADR 011
rather than from whatever the code currently does:

  a  played start to finish            -> one play
  b  repeat-one, three times round      -> three plays, three start times
  c  past the play mark, dragged back   -> two listens
     rewind short of the 25% mark       -> still one
     rewind before it has earned a play -> still one
  d  abandoned early, skipped forward   -> one listen, only the audio heard
  e  scrubbing back and forth           -> no extra events

Plus the queue moving on, which is the other way a listen closes: a track
ending and a user skipping each bank exactly one, and a track that never
played banks nothing.

All thirteen pass against the engine as it stands, which is itself a finding —
the counting state machine is right for every tick stream a phone can produce,
so the next place to look for the miscount is the stream, not the machine.

The fake deliberately implements only what the engine calls. A method the
engine starts using shows up as a missing function rather than a silent no-op.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three reports, one layer. `PlayerLayer` mounts the transport strip and Now
Playing outside the router, absolutely positioned over every route, and the
order it drew them in was wrong in three separate ways.

**The mini player drew over an open Now Playing.** The overlay was `z-10`
under the strip's `z-20`, so the surface meant to cover everything was painted
beneath the thing it covers. The fade hid only part of it: `opacity` lived on
the row inside `MiniPlayer`, which left the strip's own panel background, its
top hairline and `MiniProgress` at full opacity — the panel and progress bar
across the bottom of the expanded player were the mini player, still there.
The fade moves up to the wrapper, so the whole strip goes together.

**The queue opened and was never seen.** `app/queue.tsx` was a modal route,
and an opaque full-screen overlay mounted above the navigator covers whatever
the navigator puts under it. Nothing was wrong with `QueueScreen`; it was
rendering correctly, one layer down, behind the player that opened it. That is
a property of the overlay rather than of that screen — any route pushed while
Now Playing is open would have vanished the same way — so the queue gets the
same treatment as Now Playing itself: a root-level sheet, one layer above it,
outside the overlay's transformed and clipped container. The route is gone
along with `router.navigate('/queue')`.

**The opening did not feel connected to the finger.** The gesture already
derived one shared value from `translationY` and the overlay was already
always mounted, so the structure was right. What was missing was everything
after the release: the spring started from rest however hard the strip was
flicked, so a throw and a slow drag opened at identical speed. Both ends of
the gesture now hand their velocity over, in expansion units per second.
The spring is softer and heavier with it (ζ ≈ 0.86 against 0.75, and no mass
term before), and opacity finishes at 40% of the travel instead of tracking
the whole thing — a sheet arriving, not two screens cross-fading. A tap also
mounts the player on press-in rather than on press, so the first frames of the
animation are not spent waiting for a render the animation itself triggered.

`playerLayerLayout` grows a second measurement, the strip's height, which
nothing consumes yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The header was clipped off the top and the transport row sat under the
navigation bar. Two reports, one cause, and it is not the safe area — that was
being applied. The column simply did not fit.

The cover was sized from the screen width alone: a square of `width - 2 ×
gutter`, whatever that came to. On a 393 × 851 dp phone that is 345 dp of
artwork above roughly 440 dp of title, spec strip, scrubber and transport,
inside about 780 dp of usable height. `justify-center` then split the overflow
between both ends, which is exactly the two symptoms. The cover was never
asked whether it fit.

So it is bounded on both axes now, and the height bound is measured rather
than computed from a constant: the chrome below it is not a fixed height,
because the title wraps to two lines and Turkish runs 10–20% longer than
English. `ArtworkCarousel` takes the height flex leaves it and draws a square
of `min(width - gutters, that height)`.

`aspect-square w-full` is gone with it. A width-derived square is only square
while the width is the tighter of the two bounds, and on this screen it was
not — which is the same bug wearing the other reporter's words, "the artwork
is the wrong size".

Three gaps of 32 between four blocks was also 96 dp of air the column did not
have; `gap-6` gives 24 back, and `pb-4` keeps the transport off the navigation
bar rather than merely clear of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Playlists tab showed "0 tracks" beside Liked Songs while opening it showed
the real ones — and the tab also drew "there is nothing here, make a playlist"
underneath a visible row.

Both come from the same habit, and neither is a second query computing the
number differently.

**The count was never told anything had changed.** `useLiveQuery` re-runs when
a table changes, and the only table it watches is the one in `FROM` — it reads
`query.config.table` and compares the name, so joined tables are invisible to
it. `useFavoriteEntries` selected `from(tracks)` and joined `track_stats`,
so it watched `tracks` and never `track_stats`. Liking a song writes only to
`track_stats`. The tab therefore held whatever the query returned when it last
mounted; the detail screen agreed with reality because it had just mounted.
Selecting from `track_stats` is also the honest description of what the list
is: favourites are `track_stats` rows.

**The empty state was answering a different question from the list.** It asked
`playlists.length`, which excludes Liked Songs, while the list rendered `rows`,
which included it. So did the header count. Three views of one collection,
computed three times — the thing `AGENTS.md` names outright: *a count and the
list it describes come from one query*.

`buildPlaylistRows` now builds the rows once, the header renders `rows.length`,
and the empty state is a fact about the same array. Liked Songs is in it only
when it holds something: a virtual playlist with nothing in it is not a
destination yet, and an always-present "0 tracks" row is what forced the count
and the list to disagree in the first place.

`LIKED_SONGS_ID` moves to `services/playlists/order` and is re-exported.
Importing it from the query module drags SQLite into every test that needs to
name this playlist — the module opens the database at import time, which the
new test hit immediately.

The regression is pinned: whatever the rows are built from, the empty state
can only be true when there are none of them.

Also carries this screen's share of the mini-player bottom inset, which the
next commit applies everywhere else — the two changes are three lines apart in
the same list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The transport strip is always visible once something is playing, and it is
absolutely positioned over the routes rather than laid out with them. So the
last rows of every list sat behind it, permanently unreachable — the bottom of
the library, the bottom of a playlist, the last setting.

One measured number, published by the layer that owns the strip and read
through `useMiniPlayerInset`, rather than a hand-tuned `pb-` on each screen:
six screens with six guesses is six things to get wrong, and five of them were
simply absent.

It is a runtime measurement, so it cannot be a Tailwind class — the config
overrides the spacing scale and compiles anything outside it to nothing at
all. This is the exception `AGENTS.md` names. Where a content container needed
both, the whole padding moved into the style object using `SPACING` tokens,
rather than leaving which one wins to NativeWind's merge order between
`contentContainerClassName` and `contentContainerStyle`.

The tab bar is deliberately not in the number. On a tab route the screen
already ends where the bar begins; on a pushed route there is no bar and the
strip measures its own safe-area padding instead. Either way the strip's
measured height is exactly the overlap. It is zero while nothing is playing,
because the mini player renders nothing then — so a list gets its full height
back rather than a band of dead space under it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The third report of "the loop count is wrong", and the first one with a
mechanism that explains why it was *sometimes* right.

Every threshold in the counting rule is a fraction of the track's duration, and
there are two durations that disagree. The scanner's comes from MediaStore and
is null more often than anyone expects — a file copied onto the device and
indexed before its metadata was read has a row with no duration at all, and it
stays that way until stage two of a scan reaches it. The engine's comes from
the open file and is authoritative.

`classifyListen(msPlayed, 0)` returns `partial`; that is its first line. So a
track with no stored duration recorded a `play_event` for every listen, moved
neither counter, and vanished from the play counts. Ten listens produced ten
honest-looking rows and a play count of zero.

`PlaybackState.durationMs` already preferred the engine's figure — the comment
saying MediaStore "is occasionally wrong" has been in `docs/player.md` all
along. The listen handed to the recorder did not: it carried `track.durationMs`
straight off the row. That gap is the defect. `FinishedListen` now carries the
duration explicitly, and the engine fills it with its own, falling back to the
scanner's only when the file never opened.

This is why two rounds of device verification came back clean. Both used tracks
whose stored duration was fine, and those counted correctly the whole time. The
failure needs a track MediaStore has not finished reading, which is exactly the
state a freshly copied file is in — and freshly copied files are what you reach
for when testing playback.

Three cases in `listenRecording.test.ts` cover it, and all three fail against
the previous behaviour: a full listen to a track with no stored duration is a
play, a repeat of one still splits in two, and a stored duration that is far
too short no longer turns a genuine skip into a play.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The system notification showed "playing" for audio that had stopped, and drew
no artwork at all for a track without a cover while the app drew its
music-note placeholder.

**The state.** `setActiveForLockScreen` was called on every track load. On a
player that is already active it does not refresh a session — inside
expo-audio it releases the MediaSession and builds a new one on the main
queue. Between those two there is a window with no live session, and the
notification's play/pause icon is drawn from `session.player.isPlaying` at the
moment it is posted. A state change landing in that window is drawn against a
released session and never corrected. Since our engine keeps one player for the
app's lifetime and swaps sources through `replace()`, that window opened on
every single track change.

It is claimed once now and updated in place with `updateLockScreenMetadata`,
which changes metadata on the live session and re-posts the notification with
no release and no gap. `stop()` hands the session back and clears the flag.

That also explains an entry in `docs/player.md` that had been filed as an
unexplained quirk — `dumpsys media_session` reporting `state=NONE` with a stale
title. It was catching the swap, and the swap was happening constantly.

**The artwork.** A generated PNG of lucide's `music` mark, same stroke weight,
on `--color-panel`, unpacked from the bundle to a `file://` path — the service
loads artwork through `java.net.URL(...).openConnection()`, which knows nothing
about `asset://` or a Metro URL. So a track with no cover looks the same in the
notification as it does on screen.

**No favourite button.** It cannot be done through this engine and ADR 015
records why in full: `AudioLockScreenOptions` has three fields and none is a
custom action, the MediaSession is a private field of expo-audio's own service,
and the routes that remain are a private fork of the library or reflection into
its internals. Deferred to whenever the engine question in ADR 009 is reopened,
and reported as not delivered rather than quietly dropped.

ADR 014 records the other decision this branch made: the queue is a root-level
sheet, because a route cannot be seen from under a root-mounted player overlay.

`tsconfig` paths are reordered, most specific first. TypeScript picks the best
pattern whatever the order; jest-expo turns them into a Jest moduleNameMapper
in this order and Jest takes the first match, so `@/assets/x` was resolving to
`src/assets/x` and no asset import resolved under test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`LIKED_SONGS_ID` moved into `buildPlaylistRows`; the screen no longer names it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`HANDOFF.md` told the next session that `main` holds three files and must never
be branched from. PR #2 merged the project into `main` on 2026-08-02, so that
warning now points the wrong way — and following it blindly would be its own
mistake, because the newest four commits still sat ahead of `main` on
`fix/ux-round-2`. It says "check, in both directions" now, with the command.

`docs/components.md` gains `QueueOverlay`, splits `PlayerLayer` from
`NowPlayingOverlay`, and records the stacking order in the one place that
decides it.

Test count 292 -> 335.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removing the queue route took hardware back with it. Both Now Playing and the
queue are mounted outside the navigator, so nothing else can handle it: back
would pop the screen *underneath* an open player and leave the player sitting
over a screen the user never chose.

`PlayerLayer` is the only place that knows both states, so it takes the press
and closes the innermost one — queue first, then the overlay. Registered only
while one of them is open, so an ordinary screen keeps its own back behaviour.

The handler is declared after `onExpandedChange` on purpose. A dependency array
is built during render, so naming that `const` above its own declaration is a
temporal dead zone error on every render rather than a lint nit — which is how
this was found.

Also `h-full` rather than `flex-1` on the centre artwork slot: it sits in a
row, so `flex-1` put a flex basis of zero against the explicit width and left
which one wins to the shrink factor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… did not

`docs/stats.md` said the repeat-listen behaviour was "verified on device". It
had been, twice, and both reports were true about what they measured. Both were
wrong about what they implied. Every track those sessions used had a good
stored duration, and those counted correctly the whole time — the failure needs
a track whose MediaStore duration is missing, which is the state a freshly
copied file is in and therefore exactly the file you reach for when testing
playback.

The matrix was written down before it was run this time. On the Pixel_7 AVD:

  a  played start to finish            1 event, play                      pass
  b  looped                            7 passes, 7 events, 27-30s apart   pass
  c  past the play mark, rewound to 0  2 events                           pass
  d  skipped forward                   could not be driven
  e  scrubbed back and forth           could not be driven

(d) and (e) need a forward seek and there is no way to perform one from
automation: the scrubber is a Reanimated pan that neither `input swipe` nor a
hand-built `input motionevent` sequence activates, and both
`cmd media_session dispatch fast-forward` and `KEYCODE_MEDIA_FAST_FORWARD` left
the position where it was. Both are covered by the harness, which drives the
real `AudioEngine.seekTo`. So: 5/5 in the harness, 3/5 on hardware, and the
three that ran on hardware are the three the harness cannot model — real audio
timing, a real status stream, a real write per event.

The gaps in (b) are the load-bearing number. 27–30 seconds between consecutive
`started_at_utc` values, each carrying ~27 s of `ms_played`. A double-written
event would sit milliseconds from its twin. None did.

The Mi 9T could not be driven at all: `adb shell input` is still refused, and
its notification shade was stuck open with no shell command able to collapse
it. Everything recorded is the emulator, and the file says so.

`docs/performance.md` also gains the notification results, including one that
closes an old open question: `dumpsys media_session` reporting `state=NONE`
with a stale title was the session being released and rebuilt on every track
change, which is the same window that let the notification show "playing" for
stopped audio. Fixing one made the other truthful.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 2, 2026 21:19
@Yefee8
Yefee8 merged commit 3ed01ff into main Aug 2, 2026
1 check passed

Copilot AI 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.

Pull request overview

This PR is a broad “final polish” pass across the offline music player, focusing on correctness and UX consistency in three main areas: listen counting accuracy (including repeat-one and duration handling), player/queue overlay layering, and “Liked Songs” / library import flows.

Changes:

  • Fix listen recording correctness end-to-end (repeat-one pass handling, authoritative duration for play/skip classification) and improve Android lock-screen/notification metadata + placeholder artwork.
  • Re-architect Now Playing + Queue as root-level overlays (not routes) with a shared Reanimated expansion value; add measured mini-player inset so scrollable screens pad correctly.
  • Add a virtual “Liked Songs” playlist backed by track_stats (with favorite_at migration) and update library folder import UX + i18n strings.

Reviewed changes

Copilot reviewed 76 out of 80 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tsconfig.json Reorders TS path mappings to avoid Jest moduleNameMapper collisions for @/assets/*.
src/services/stats/listenCycle.test.ts Adds regression test for starting a new repeat-one pass after closing the previous one.
src/services/playlists/order.ts Introduces LIKED_SONGS_ID constant in services layer to avoid DB import side effects in tests.
src/services/haptics/index.ts Updates docstrings for haptic feedback helpers.
src/services/audio/types.ts Extends FinishedListen to carry authoritative durationMs for play/skip classification.
src/services/audio/testing/playbackHarness.ts Adds an AudioEngine-driven harness to script playback status ticks for end-to-end listen counting tests.
src/services/audio/testing/fakeAudioPlayer.ts Adds a minimal fake expo-audio player to support the playback harness.
src/services/audio/queue.ts Extracts “restart current track” threshold logic into shouldRestartCurrentTrack + constant.
src/services/audio/queue.test.ts Adds tests for shouldRestartCurrentTrack ten-second threshold behavior.
src/services/audio/notificationArtwork.ts Adds cached async preparation of a file-backed placeholder artwork URI for lock screen/notification.
src/services/audio/listenRecording.test.ts Adds end-to-end listen counting matrix tests against the real AudioEngine wiring.
src/services/audio/AudioEngine.ts Fixes listen reporting duration source, repeat-one reopen behavior, previous threshold, and lock screen binding/metadata update behavior.
src/i18n/locales/tr.json Adds strings for folder import + liked songs; removes selection-related strings.
src/i18n/locales/en.json Adds strings for folder import + liked songs; removes selection-related strings.
src/features/stats/StatsScreen.tsx Applies measured mini-player bottom inset and switches ScrollView content container spacing to token-based styles.
src/features/settings/SettingsScreen.tsx Applies measured mini-player bottom inset and switches ScrollView content container spacing to token-based styles.
src/features/playlists/PlaylistsScreen.tsx Builds playlist rows via a shared function (including Liked Songs), fixes header count vs rendered rows, and adds mini-player bottom inset.
src/features/playlists/playlistRows.ts Adds pure helpers to build the playlist rows list + empty-state condition from one source of truth.
src/features/playlists/playlistRows.test.ts Adds unit tests covering Liked Songs row inclusion/ordering and empty-state consistency.
src/features/playlists/PlaylistDetailScreen.tsx Adds Liked Songs virtual playlist handling and disables rename/delete/reorder/remove for liked list; adds mini-player inset.
src/features/playlists/components/PlaylistEntryRow.tsx Makes onRemove optional and conditionally renders the remove button for Liked Songs.
src/features/playlists/components/PlaylistDetailHeader.tsx Makes rename/delete actions optional and removes “add tracks” affordance.
src/features/playlists/components/AddTracksSheet.tsx Removes the playlist-side “add tracks” picker sheet component.
src/features/playlists/components/AddToPlaylistSheet.tsx Refactors minor layout/props docs; formatting cleanup.
src/features/player/QueueScreen.tsx Converts queue to a non-route surface with onClose, and fixes safe-area insets for full-screen sheet.
src/features/player/PlayerScreen.tsx Converts Now Playing to a root overlay surface (no router), takes callbacks for expand + queue open.
src/features/player/playerLayerLayout.ts Adds a module-level store for tab bar height and mini-player height; exposes useMiniPlayerInset().
src/features/player/PlayerLayer.tsx Adds root stacking layer: mini-player strip + Now Playing overlay + Queue overlay, with back handling and measured strip height publication.
src/features/player/playerExpansion.ts Adds root-owned Reanimated mutable shared value for mini-player ↔ Now Playing expansion progress.
src/features/player/listenRecorder.ts Records listens using FinishedListen.durationMs (engine-authoritative), not track.durationMs.
src/features/player/components/QueueOverlay.tsx Adds root-level queue sheet overlay with reduced-motion-aware enter/exit animations.
src/features/player/components/NowPlayingOverlay.tsx Adds root-level Now Playing overlay driven by shared expansion value (opacity + translateY).
src/features/player/components/MiniPlayer.tsx Reworks gestures to drive shared expansion value, adds velocity handoff, and removes router navigation.
src/features/player/components/ArtworkCarousel.tsx Reworks vertical drag to drive shared expansion, and makes artwork sizing bounded by both width and available height.
src/features/player/artworkSize.ts Adds helper to compute bounded artwork square size from width + measured available height.
src/features/player/artworkSize.test.ts Adds unit tests for bounded artwork sizing behavior.
src/features/library/LibraryTracks.tsx Removes multi-select/selection handling and simplifies row press/long-press behavior.
src/features/library/LibraryScreen.tsx Adds confirm-then-import folder flow, shows full-screen import progress modal, and updates scan banner conditions.
src/features/library/hooks/useTrackActions.ts Updates docs to reflect removal of selection bar usage.
src/features/library/hooks/useSelection.ts Removes selection hook entirely.
src/features/library/hooks/useScan.ts Splits folder import into pickFolder + confirmed importFolder with isFolderImporting flag.
src/features/library/components/TrackRow.tsx Removes selection checkbox UI and accessibility role/state logic.
src/features/library/components/TrackList.tsx Updates swipe label key, removes selection props, and adds bottom inset padding via useMiniPlayerInset().
src/features/library/components/TrackActionSheet.tsx Removes selection action and updates add-to-queue label key.
src/features/library/components/SelectionBar.tsx Removes selection action bar component.
src/features/library/components/LibraryRow.tsx Removes selection gating logic; keeps memoization focused on primitives.
src/features/library/components/LibraryHeader.tsx Removes selection entry point button and updates docs accordingly.
src/features/library/components/FolderImportModal.tsx Adds a blocking full-screen progress modal for confirmed folder imports.
src/features/library/components/CollectionGrid.tsx Adds bottom inset padding and switches container padding to token-based styles.
src/db/schema.ts Adds nullable track_stats.favorite_at column for sorting favorites by recency.
src/db/queries/tracks.ts Updates setFavorite to store/clear favoriteAt timestamp; minor formatting cleanup.
src/db/queries/scanning.ts Documentation and formatting cleanup in scan query pipeline.
src/db/queries/playlists.ts Re-exports LIKED_SONGS_ID, factors entry selection, and adds useFavoriteEntries() query for virtual liked playlist.
src/db/migrations/migrations.js Adds migration 0003 to migrations list.
src/db/migrations/meta/0003_snapshot.json Adds Drizzle snapshot for migration 0003.
src/db/migrations/meta/_journal.json Records migration 0003 in Drizzle journal.
src/db/migrations/0003_numerous_cannonball.sql Adds SQL migration to add favorite_at column.
src/db/migrations.test.ts Adds test asserting favorite_at is nullable integer (doesn’t invalidate existing rows).
package.json Adds expo-asset dependency to support placeholder asset unpacking.
jest.config.js Ignores .claude worktrees at module resolution level to avoid Haste map duplicate package name conflicts.
HANDOFF.md Updates branching guidance and test suite counts; adds watchman note for Jest.
docs/stats.md Updates stats phase status, documents duration source bug + harness coverage, and corrects device verification claims.
docs/scanner.md Updates wording for user-initiated library scan + folder import flow.
docs/player.md Documents “claim session once, then update metadata” behavior and notification placeholder artwork approach.
docs/performance.md Adds device verification notes for this branch, including listen counting matrix and MediaSession checks.
docs/database.md Documents Liked Songs as virtual + favorite_at semantics; updates seek/rewind counting rule wording.
docs/components.md Updates component map to reflect removal of selection/add-tracks sheet and new player overlay architecture.
docs/adr/015-no-favourite-button-in-the-media-notification.md Adds ADR documenting inability to add a favourite action via expo-audio’s lock screen API.
docs/adr/014-queue-is-a-root-sheet-not-a-route.md Adds ADR documenting queue overlay decision and implications.
docs/adr/009-expo-audio-and-our-own-queue.md Updates “previous” restart rule wording to ten-second rule.
docs/adr/008-permission-is-asked-not-assumed.md Updates references from addFolder to pickFolder.
docs/adr/007-saf-folders-go-through-mediastore.md Updates references from addFolder to importFolder and documents full-screen import progress.
docs/adr/006-manual-add-is-first-class.md Notes partial supersession by ADR 010 and clarifies scans are user-initiated.
docs/01-TECH-STACK.md Updates routing architecture description and file tree to reflect Now Playing as overlay, not route.
assets.d.ts Adds TS module declarations for PNG/JPG imports (asset-registry numeric ids).
app/queue.tsx Removes queue route (queue is now a root overlay).
app/player.tsx Removes player route (Now Playing is now a root overlay).
app/(tabs)/_layout.tsx Measures tab bar height for overlay layout and removes MiniPlayer from tab bar (now owned by PlayerLayer).
app/_layout.tsx Wraps navigator with PlayerLayer and removes modal routes for player/queue.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +128 to +141
const query = db
.select({
position: sql<number>`row_number() over (order by ${trackStats.favoriteAt} desc, ${tracks.id} desc) - 1`,
...entrySelection,
})
.from(trackStats)
.innerJoin(tracks, eq(tracks.id, trackStats.trackId))
.leftJoin(artists, eq(artists.id, tracks.artistId))
.leftJoin(albums, eq(albums.id, tracks.albumId))
.where(and(eq(trackStats.isFavorite, 1), eq(tracks.isMissing, 0)))
.orderBy(desc(trackStats.favoriteAt), desc(tracks.id));

const { data } = useLiveQuery(query);
return data;
Comment on lines +75 to +81
<ScrollView
contentContainerStyle={{
gap: SPACING[8],
paddingHorizontal: SPACING[6],
paddingBottom: SPACING[16] + bottomInset,
}}
>
Comment on lines +113 to +119
<ScrollView
contentContainerStyle={{
gap: SPACING[8],
paddingHorizontal: SPACING[6],
paddingBottom: SPACING[16] + bottomInset,
}}
>
Comment on lines +57 to 65
/*
One style rather than a class plus a style. The bottom inset is a
runtime measurement — the transport strip's height — which no Tailwind
class can carry, and mixing `contentContainerClassName` with
`contentContainerStyle` leaves which padding wins to NativeWind's merge
order. The values are still design-system tokens.
*/
contentContainerStyle={{ paddingHorizontal: SPACING[4], paddingBottom: bottomInset }}
ListEmptyComponent={empty}
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.

2 participants