{#snippet row(icon, t, m, fn)}
{t}
{#if m}
{m}
{/if}
{/snippet}
{#if kind === "track" && trackArg} {@const album = trackArg.album} {@const track = trackArg.track}
{#if isCatalog(track) && (track.catalog?.cover || album?.art_path)}
{:else}
{/if}
{track.title}
{track.artist || album?.artist} · {album?.title}
{#if isCatalog(track)} {@render row("play", "Play", "streams from the best source", () => playCatalog(track, album))} {@render row(saveNote.startsWith("Saving") ? "check" : "download", saveNote || "Save to library", saveNote ? "" : "the best copy by your download rules", () => { if (!saveNote) saveTrack(track, album); })} {@render row("playlist", "Add to playlist", "", () => openHalf("pickPlaylist", { entries: [{ t: track, album }] }))}
{#if album?.catalogId != null} {@render row("disc", "Go to record", album.title, act(() => push("catalogAlbum", { id: album.catalogId, artist: album.artist, title: album.title })))} {/if} {#if album?.artist || track.artist} {@render row("users", "Go to artist", track.artist || album.artist, act(() => push("artist", { name: track.artist || album.artist })))} {/if} {:else} {@render row("play", "Play next", "", act(() => playNext([{ t: track, album }])))} {@render row("list-plus", "Add to queue", after ? after + " after this" : "", act(() => enqueue([{ t: track, album }])))} {@render row("playlist", "Add to playlist", "", () => openHalf("pickPlaylist", { entries: [{ t: track, album }] }))}
{#if album?.id != null && !onThisAlbum(album.id)} {@render row("disc", "Go to album", album.title, act(() => push("album", { id: album.id })))} {/if} {#if album?.artist || track.artist} {@render row("users", "Go to artist", track.artist || album.artist, act(() => push("artist", { name: track.artist || album.artist })))} {/if} {/if}
{#if !isCatalog(track)}
Format
{fmtOf(track) || "—"}
{#if track.size}
Size
{fmtBytes(track.size)}
{/if}
File
{basename(track.path)}
Folder
{dirname(track.path)}
{/if} {:else if (kind === "album" || kind === "album-more") && albumArg} {@const album = albumArg.album} {@const tracks = albumArg.tracks || []}
{album.title}
{album.artist} · {album.year ?? "—"} · {tracks.length || album.track_count} tracks
{@render row("play", "Play next", "", act(() => playNext(tracks.map((t) => ({ t, album })))))} {@render row("list-plus", "Add to queue", tracks.length + " tracks", act(() => enqueue(tracks.map((t) => ({ t, album })))))} {#if tracks.length} {@render row("playlist", "Add to playlist", tracks.length + " tracks", () => openHalf("pickPlaylist", { entries: tracks.map((t) => ({ t, album })) }))} {/if}
{@render row("users", "Go to artist", album.artist, act(() => push("artist", { name: album.artist })))}
Format
{albumFmt(tracks) || "—"}
Length
{longDur(tracks.reduce((n, t) => n + (t.duration_secs || 0), 0))}
{#if tracks[0]?.path}
Folder
{dirname(tracks[0].path)}
{/if}
{:else if (kind === "playlist" || kind === "playlist-more") && plArg} {@const p = plArg.playlist} {@const rows = plArg.items || []} {@const unres = rows.filter((i) => i.kind === "unresolved")}
{#if coverFor(p)}
{:else}
{/if}
{p.name}
{p.kind === "setlist" ? "Setlist" : "Playlist"} · {p.track_count} track{p.track_count === 1 ? "" : "s"}{p.duration_secs ? " · " + longDur(p.duration_secs) : ""}{p.pinned ? " · pinned" : ""}
{#if kind === "playlist"} {@render row("play", "Play", "", act(() => playPl(p)))} {@render row("shuffle", "Shuffle", "", act(() => playPl(p, true)))}
{/if} {@render row("pin", p.pinned ? "Unpin" : "Pin", p.pinned ? "" : "keeps it at the top of the shelf", act(() => togglePin(p).catch(oops)))} {@render row("pencil", "Rename", "", () => renamePrompt(p))} {#if kind === "playlist-more"} {@render row("file-text", p.description ? "Edit description" : "Add a description", p.description || "", () => describePrompt(p))} {/if} {@render row("layers", "Duplicate", "a copy, “" + p.name + " copy”", act(() => duplicatePlaylist(p).then((id) => push("playlist", { id })).catch(oops)))} {#if kind === "playlist-more" && unres.length} {@render row("x", "Remove unresolved entries", unres.length + " that never matched a track", act(() => removeItems(p, rows, unres).catch(oops)))} {/if} {#if kind === "playlist-more" && bulkable(rows) && !bulk.active}
{@render row("library", "Add to library", "files every already-streamed track — nothing is downloaded", act(() => { void runBulk(p, rows, "cached"); }))} {@render row("download", "Download all tracks", bulkable(rows) + " not in the library yet, by your download rules", act(() => { void runBulk(p, rows, "download"); }))} {/if}
{@render row("trash", "Delete playlist", "", () => confirmDelete(p))}
{:else if kind === "pltrack" && shown?.arg?.item} {@const p = shown.arg.playlist} {@const rows = shown.arg.items || []} {@const item = shown.arg.item}
{#if item.path && item.art_path}
{:else if item.art_url}
{:else}
{/if}
{item.title}
{item.artist}{item.album ? " · " + item.album : ""}{item.kind === "unresolved" ? " · unresolved" : item.missing ? " · file missing" : item.kind === "stream" ? " · streams from the network" : ""}
{#if queueable(item)} {@render row("play", "Play from here", "the rest of the playlist follows", act(() => playFrom(p, rows, item)))} {@render row("skip-forward", "Play next", "", act(() => playNext([entryOf(item, p)])))} {@render row("list-plus", "Add to queue", after ? after + " after this" : "", act(() => enqueue([entryOf(item, p)])))} {#if !item.path} {@render row(saveNote.startsWith("Saving") ? "check" : "download", saveNote || "Save to library", saveNote ? "" : "the best copy by your download rules", () => { if (!saveNote) saveTrack({ title: item.title, artist: item.artist }, null); })} {/if} {:else if item.kind === "unresolved"}
This row never matched a track{item.source_line ? " — it came in as “" + item.source_line + "”" : ""}. Repair it on the desktop, or remove it.
{:else if item.missing}
The file is no longer where the library last saw it.
{/if} {#if item.artist} {@render row("users", "Go to artist", item.artist, act(() => push("artist", { name: item.artist })))} {/if}
{@render row("x", "Remove from playlist", "undo for a few seconds after", act(() => removeItems(p, rows, [item]).catch(oops)))}
{:else if kind === "pickPlaylist" && shown?.arg?.entries} {@const entries = shown.arg.entries}
Add to playlist
· {entries.length === 1 ? entries[0].t.title : entries.length + " tracks"}
{@render row("plus", "New playlist", "", () => newPlaylistThenAdd(entries))}
{#each lib.playlists as p (p.id)}
addEntries(p, entries).catch(oops))}>
{#if coverFor(p)}
{:else}
{/if}
{p.name}
{p.track_count} track{p.track_count === 1 ? "" : "s"}{p.pinned ? " · pinned" : ""}
{/each} {#if !lib.playlists.length}
No playlists yet — make one above.
{/if}
{:else if kind === "prompt" && shown?.arg} {@const a = shown.arg}
{a.title}
{#if a.multiline}
{:else}
{ if (e.key === "Enter") submitPrompt(); }} use:focusLater /> {/if}
Cancel
{a.ok || "OK"}
{:else if kind === "confirm" && shown?.arg} {@const a = shown.arg}
{a.title}
{#if a.body}
{a.body}
{/if}
Cancel
{ const f = a.onconfirm; closeHalf(); f?.(); }}>{a.ok || "OK"}
{:else if kind === "import"}
Import a playlist
{#each DOORS as [door, icon, t, m] (door)} {@render row(icon, t, m, () => importPrompt(door))} {/each}
A playlist file or a folder of audio files imports on the desktop, where the files are.
{:else if kind === "librarysort"}
Sort & view
Order
{#each SORTS as [k, l] (k)}
patch({ sort: k, tab: "albums" })}>{l}
{/each}
View
{#each [["grid", "Grid"], ["list", "List"]] as [k, l] (k)}
patch({ view: k, tab: "albums" })}>{l}
{/each}
Done
{:else if kind === "library-more"}
Library
{@render row("layers", "Sort and view", "", () => openHalf("librarysort"))} {@render row("playlist", "Playlists", "", act(() => patch({ tab: "playlists" })))}
{@render row("refresh", "Rescan library", "looks for new files in the folder", act(rescan))}
{:else if kind === "home-more"}
SoulStream
{#if player.track} {@render row("list-plus", "Queue", after + " after this", act(() => { nav.face = "queue"; openSheet(null); }))} {/if} {@render row("clock", "Sleep timer", sleepLabel(), () => openHalf("sleep"))} {@render row("settings", "Settings", "", act(() => go("you")))}
{:else if kind === "community"}
Community
{@render row("refresh", "Refresh the rooms", slsk.rooms.length + " on the server", act(refreshRooms))} {@render row("x", "Sign out", slsk.username, act(disconnect))}
Spaces, holdings and the sealed inbox wait for the private layer on the phone.
{:else if kind === "roomusers" && shown?.arg?.name} {@const room = slsk.joined[shown.arg.name]}
#{shown.arg.name}
· {room?.users.length ?? 0} here
{#each (room?.users ?? []).slice(0, 120) as u (u.username)} {@const buddy = slsk.buddies.includes(u.username)}
{ openThread(u.username); push("thread", { username: u.username }); })} oncontextmenu={(e) => { e.preventDefault(); (buddy ? removeBuddy : addBuddy)(u.username).catch(() => {}); }}>
{u.username}{#if buddy}
BUDDY
{/if}
{u.file_count ? u.file_count.toLocaleString() + " files" : ""}
{/each}
{ leaveRoom(shown.arg.name).catch(() => {}); back(); })}>Leave #{shown.arg.name}
{:else if kind === "sleep"}
Sleep timer
Pause after
{#each [[0, "Off"], [15, "15 min"], [30, "30 min"], [45, "45 min"], [60, "1 hour"], [90, "1½ hours"]] as [m, l] (m)}
setSleep(m))}>{l}
{/each}
{#if player.sleepAt}
Playback pauses in {sleepLabel().replace(" left", "")}.
{/if} {/if}