iOS: section reorder and visibility, CarPlay downloads fix, stem playlist refresh fix

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Sonnet 5.5 committed 2026-10-28 16:30:30 -07:00
1 parent 3e832e3125
commit 5b7fadb7f7
6 files changed
+122 -31

No files matched your search

+68 -18
View File
@@ -9,6 +9,7 @@ struct LibraryHome: View {
@State private var played: [Album] = []
@State private var added: [Track] = []
@State private var reload = 0
@Environment(\.scenePhase) private var scenePhase
@State private var editing = false
private var g: AppGraph { .shared }
@@ -28,22 +29,24 @@ struct LibraryHome: View {
g.settings.libraryOrder = (t + saved.filter { !t.contains($0) }).joined(separator: "|")
}
var body: some View {
let s = g.settings
let ready = s.libraryReady
private static let sectionNames = ["Recent Plays", "Recently Added", "On This Day", "Stations", "Browse"]
private var shownSections: [String] { ordered(Self.sectionNames) { $0 } }
private var hiddenSections: Set<String> { Set(g.settings.libraryHidden.split(separator: "|").map(String.init)) }
private func toggle(_ name: String) {
var h = hiddenSections
if h.contains(name) { h.remove(name) } else { h.insert(name) }
g.settings.libraryHidden = Self.sectionNames.filter(h.contains).joined(separator: "|")
}
@ViewBuilder private func section(_ name: String) -> some View {
let today = Date()
Page(title: "Library") {
if !ready {
EmptyState(icon: "server.rack", title: "Connect a Server",
message: "Add your Plex or Jellyfin server to browse your music. Stems come from stemserve on the same box.", action: "Connect") {
ui.push(.Servers)
}
}
if ready {
if !played.isEmpty {
switch name {
case "Recent Plays":
if !played.isEmpty {
SectionTitle(title: "Recent Plays")
RecentShelf(albums: played)
}
case "Recently Added":
if !added.isEmpty {
SectionTitle(title: "Recently Added")
ForEach(Array(added.enumerated()), id: \.element.ratingKey) { i, t in
@@ -51,8 +54,7 @@ struct LibraryHome: View {
.listRow(inset: MARGIN + 60, line: i < added.count - 1)
}
}
}
if ready {
case "On This Day":
SectionTitle(title: "On This Day")
Text(today.formatted(.dateTime.month(.wide).day())).font(.subheadline).foregroundStyle(p.label2)
.padding(.leading, MARGIN).padding(.bottom, 10)
@@ -76,8 +78,7 @@ struct LibraryHome: View {
}
}
}
}
if ready {
case "Stations":
SectionTitle(title: "Stations")
let stations: [(String, String, () async throws -> Void)] = [
("dot.radiowaves.left.and.right", "Library Radio", {
@@ -91,7 +92,7 @@ struct LibraryHome: View {
LibraryRow(icon: st.0, title: st.1, last: st.1 == shown.last?.1, editing: editing,
move: { move(st.1, $0, in: shown.map(\.1)) }) { play(st.2) }
}
}
default:
// Every tab except Settings (and this one).
SectionTitle(title: "Browse")
let rows: [(String, String, Route)] = [
@@ -105,6 +106,33 @@ struct LibraryHome: View {
move: { move(r.1, $0, in: shownRows.map(\.1)) }) { ui.push(r.2) }
}
}
}
var body: some View {
let s = g.settings
let ready = s.libraryReady
Page(title: "Library") {
if !ready {
EmptyState(icon: "server.rack", title: "Connect a Server",
message: "Add your Plex or Jellyfin server to browse your music. Stems come from stemserve on the same box.", action: "Connect") {
ui.push(.Servers)
}
}
if ready {
ForEach(shownSections, id: \.self) { name in
let off = hiddenSections.contains(name)
if editing {
SectionEditBar(title: name, off: off,
move: { move(name, $0, in: shownSections) },
toggle: { toggle(name) })
}
if !off || editing {
section(name).opacity(off ? 0.35 : 1).allowsHitTesting(!editing || !off)
}
}
}
}
.toolbar {
if ready {
ToolbarItem(placement: .topBarTrailing) {
@@ -112,6 +140,7 @@ struct LibraryHome: View {
}
}
}
.onChange(of: scenePhase) { _, ph in if ph == .active { reload += 1 } }
.task(id: "\(s.libraryKey)|\(reload)") {
guard ready else { return }
async let rp = try? g.plex.recentlyPlayed(count: 24)
@@ -119,7 +148,7 @@ struct LibraryHome: View {
await LoadState.load($albums) { try await g.plex.albums() }
played = Array((await rp ?? []).prefix(24))
added = Array((await ra ?? []).prefix(10))
if await g.stemPlaylists.refresh(albumTracks: { try await g.plex.albumTracks($0) }) { await g.downloads.refresh() }
if await g.stemPlaylists.refresh(albumTracks: { try await g.plex.albumTracks($0) }, search: { try await g.plex.searchTracks($0) }) { await g.downloads.refresh() }
}
}
@@ -188,6 +217,27 @@ private struct RecentShelf: View {
}
}
/// Edit-mode header for a whole section: move it or switch it on/off.
private struct SectionEditBar: View {
let title: String
let off: Bool
let move: (Int) -> Void
let toggle: () -> Void
@Environment(\.palette) private var p
var body: some View {
HStack(spacing: 18) {
Text(title).font(.headline).foregroundStyle(p.label)
Spacer()
Image(systemName: off ? "eye.slash" : "eye").onTapGesture { toggle() }
Image(systemName: "chevron.up").onTapGesture { move(-1) }
Image(systemName: "chevron.down").onTapGesture { move(1) }
}
.font(.system(size: 17, weight: .semibold)).foregroundStyle(p.accent)
.padding(.horizontal, MARGIN).padding(.vertical, 10)
.background(p.label3.opacity(0.12))
}
}
private struct LibraryRow: View {
let icon: String
let title: String