Skip to content

Show playlists in their folders - #104

Open
ralph wants to merge 21 commits into
mainfrom
playlist-folders
Open

ralph wants to merge 21 commits into
mainfrom
playlist-folders

Conversation

@ralph

@ralph ralph commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Closes plans/open/playlist-folder-hierarchy.md: the Playlists section shows the folders your playlists are in on Spotify.

What was measured

From the web player's session, the app's own libraryV3 query, recording counts and kinds only. Your account: 38 playlists, 4 folders, none nested.

Variables Answer
flatten: false 18: 14 playlists, 4 folders (closed), all depth 0
+ folderUri: <folder> that folder's 2 playlists
+ expandedFolders: [<one>] 20: that folder's playlists after it, at depth 1
+ expandedFolders: [<all four>] 42: the whole tree in order, each folder followed by what it holds
the same, offset: 10, limit: 10 the same sequence from its eleventh entry
flatten: true + folders included 42, but the folders bunched together and every entry at depth 0

A folder is __typename: "Folder", with {name, uri, playlistCount, folderCount}, and every entry carries a depth. So the tree arrives as one offset-paged list once every folder is named in expandedFolders; the flattened list can't carry it.

What changed

  • The flat list is unchanged, and it stays what the add-to-playlist menus and the rest read.
  • The outline is loaded beside it (PlaylistService.loadPlaylistOutline):
    • the top level, then again with its folders named, and again while a pass finds new folders (a folder inside a folder), six passes at most;
    • an account without folders costs one pass;
    • rows are PlaylistOutlineRows: a folder by its full uri, or a playlist by its id, each with its depth.
  • The section shows it when there are folders. LibraryListView takes an optional outline:
    • a folder is a row with a folder glyph and a chevron, closed until clicked, with what it holds indented under it;
    • open folders are kept across launches;
    • with no folders, the section is exactly as before.
  • The outline is the section's list whenever there is one. It is loaded whole, while the flat list pages as you scroll.
    • The store's library changes edit it: a created or followed playlist goes at the top, and a deleted or unfollowed one leaves.
    • There's no load-more spinner under it.
    • Refresh reloads both lists side by side.
  • The trap the plan warned of stays shut. PathfinderPlaylist.id still refuses a folder's uri. Folders are recognised by SpotifyURI.folderUri.

Rendered in the test host (a temporary test): a playlist added at the top, the selected one, an open folder with its playlist indented and the chevron down, a closed folder with its playlist hidden and the chevron right, then a last playlist.

Reviews

  • /simplify, 4 reviewers:

    • Applied:
      • The altitude reviewer's main point. My first version merged the outline with the flat list, which pages as you scroll, so with over 50 playlists it would have hidden the rest. The outline is now the truth, and the store's library changes edit it.
      • First selection from the visible rows, and no spinner under the outline.
      • One rendering path, accessors on the row item, and SpotifyURI.folderUri.
      • The playlists stored once per load, not per page, and a parallel refresh.
    • Skipped:
      • a shared shell for folder and playlist rows;
      • one generic row type;
      • PaginationState in the pass loop.
  • /code-review medium: two findings, both fixed.

    • A load already in flight when you create a playlist used to replace the outline that had it. The store now puts back what a load missed.
    • The first selection, made from the flat list before the outline arrives, could be inside a closed folder. The list now opens the folders around the selection.
  • Tests:

    • the passes: a folder found, then named;
    • no folders means one pass;
    • the store's add and remove edit the outline;
    • the race above;
    • the folders around a nested entry.

    All 449 unit tests pass, and swiftformat --swiftversion 6.4 --lint . exits 0.

Manual testing

After signing in again (see #88):

  1. Open Playlists. Your 4 folders show, closed, among your playlists in Spotify's order.
  2. Click a folder: its playlists appear indented under it, and the chevron turns down. Click again: they're hidden.
  3. Leave a folder open and relaunch. It's still open.
  4. Select a playlist inside a folder. It opens as any other.
  5. Create a playlist: it appears at the top. Delete it: it's gone. Press Refresh: the folders are still there.
  6. Add a track to a playlist inside a closed folder from a track's menu. The menu lists it.

Tested (2026-09-30)

Brought up to date with main; the conflict with #96 in PlaylistService keeps both sides.

  • Steps 1 to 4 pass: four folders show, closed, among the playlists. A folder opens with its playlists indented and the chevron down, closes again, stays open across a relaunch, and its playlists open as any other.
  • Refresh keeps the folders and the selection.
  • Step 6's menu lists "Offline" and "yay?", which sit inside a closed folder. Nothing was added.
  • Left for you: creating and deleting a playlist (step 5), and actually adding a track (step 6). Both change the library.

Updated 2026-10-02

I brought the branch up to date with main, which had moved a lot. Three changes on top:

  • Merge:
  • The folder row is a Button. It was a tap gesture with a button trait, which announced itself to accessibility as a button and did nothing when pressed. VoiceOver couldn't open a folder. It now opens and closes when pressed, and says "Geöffnet" or "Geschlossen" (two new strings in each language).
  • One load for the section: PlaylistService.loadSection(forceRefresh:). A forced load reloads the outline beside the flat list, and it's what the toolbar's refresh, pull-to-refresh and Try again all call. Before, only the toolbar reloaded the outline.
  • Smaller tidy-ups from the review:
    • the folder glyph is Artwork's placeholder;
    • the open folders have one setter.

Tested again (2026-10-02)

  • The four folders show closed.
  • One opens through accessibility, with "Sprint" indented under it. "Sprint" opens like any playlist.
  • The folder stays open after a relaunch, and closes again.
  • The toolbar's refresh makes the flat request plus the outline's two passes, and keeps the folders and the selection.
  • Unit tests: 619 pass.

Reviews of the new commits

  • /simplify, 4 reviewers.
    • Applied: the single section load, Artwork, and the setter.
    • Skipped: seeding a refresh with the known folders to save a pass. What the API does with a deleted folder is unmeasured.
    • Recorded as a plan of its own: the playlist, album and artist rows have the same accessibility gap. Their selection is a tap gesture.
  • My review of the two commits: no findings.

Later the same night: #180 made the playlist, album and artist rows buttons too, for the same accessibility reason. I merged main again. The folder row and the entry row now share one label view (LibraryRowLabel) instead of writing the layout twice. Both kinds of row look as before, a folder still opens and closes, and 619 tests pass.

Still yours, as before: creating and deleting a playlist (step 5) and adding a track through the menu (step 6). Both change the library, and step 6 needs a context menu.

🤖 Generated with Claude Code

ralph and others added 3 commits October 3, 2026 00:10
The toolbar's refresh no longer restores the selection (#169), and the list's rows read
the playback view model from the environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It was a tap gesture with a button trait, which announced a button and did nothing when
pressed. It now says whether the folder is open, too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PlaylistService.loadSection is what the toolbar's refresh, pull-to-refresh and Try again
call; only the toolbar reloaded the outline. The folder glyph is Artwork's placeholder,
and the open folders have one setter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ralph and others added 6 commits October 3, 2026 00:21
Both had become plain buttons with the same layout written out twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	Spotifly/Views/LoggedInView.swift
The folders' changelog entry moves back under Unreleased: main released
1.3.0 by renaming that section, and the merge left the entry in it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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