Web player overhaul; iOS random sort, stems-only filter, pooled libraries

Web: live Stem Now status that tracks every queued song (albums too),
right-click menus with Stem Now, Songs and Genres pages, a Stems Only
filter in place of the sidebar section, Random sort, several pooled
Plex servers and libraries, themes plus custom CSS, a reorganised
settings page that loads without a refresh, saved mixes, karaoke and
synced lyrics, stem playlists and offline downloads.

iOS: Random in every sort menu, a Stems Only toggle (sort menu and
Settings), and several music libraries pooled from the signed-in server.
Settings now persists karaoke and speed across launches.

stemd writes a status file that stemserve serves at /api/queue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-20 18:29:45 -07:00
1 parent 48faa6682f
commit 7cdfbef160
16 files changed
+2006 -730

No files matched your search

+8 -4
View File
@@ -160,6 +160,7 @@ struct SettingsScreen: View {
}
Section {
NavCell(title: "Appearance", value: s.theme.label, icon: "paintbrush.fill", iconBg: 0x007AFF, route: .Appearance)
ToggleCell(title: "Only Show Songs With Stems", icon: "waveform", iconBg: 0xAF52DE, isOn: $s.stemsOnly)
} header: { Header(text: "Display") } footer: {
Footer(text: "Stemify \(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.1") · stems by stemd on your server · \(UIScreen.main.maximumFramesPerSecond) Hz")
}
@@ -270,8 +271,11 @@ struct PlexAccountScreen: View {
if list.isEmpty { Text("No music libraries found").foregroundStyle(p.label).themedRow() }
ForEach(list) { sec in
Button {
s.musicSection = sec.key
s.musicSectionTitle = sec.title
// Tapping adds or drops a library; the picked ones are pooled. One always stays.
var keys = s.musicSection.split(separator: ",").map(String.init)
if let i = keys.firstIndex(of: sec.key) { if keys.count > 1 { keys.remove(at: i) } } else { keys.append(sec.key) }
s.musicSection = keys.joined(separator: ",")
s.musicSectionTitle = keys.compactMap { k in list.first { $0.key == k }?.title }.joined(separator: " + ")
} label: {
HStack {
VStack(alignment: .leading, spacing: 2) {
@@ -281,7 +285,7 @@ struct PlexAccountScreen: View {
}
}
Spacer()
if sec.key == s.musicSection { Image(systemName: "checkmark").font(.body.weight(.semibold)).foregroundStyle(p.accent) }
if s.musicSection.split(separator: ",").contains(Substring(sec.key)) { Image(systemName: "checkmark").font(.body.weight(.semibold)).foregroundStyle(p.accent) }
}
.contentShape(Rectangle())
}
@@ -291,7 +295,7 @@ struct PlexAccountScreen: View {
} else {
ProgressView().frame(maxWidth: .infinity).themedRow()
}
} header: { Header(text: "Music Library") }
} header: { Header(text: "Music Libraries") } footer: { Footer(text: "Pick more than one to pool them into one library.") }
Section {
ActionCell(title: "Sign Out", color: p.red, centered: true) {
ui.overlays.confirm("Sign Out of Plex?", "Downloads stay on this phone.", "Sign Out") {