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:
1 parent
3e832e3125
commit
5b7fadb7f7
6 files changed
+122
-31
No files matched your search
@@ -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
|
||||
|
||||
@@ -1005,17 +1005,13 @@ struct TabBarScreen: View {
|
||||
.themedRow()
|
||||
.deleteDisabled(t == .Settings)
|
||||
}
|
||||
.onMove { from, to in
|
||||
shown.move(fromOffsets: from, toOffset: to)
|
||||
commit()
|
||||
}
|
||||
.onDelete { idx in
|
||||
let gone = Set(idx.map { shown[$0] }).subtracting([.Settings])
|
||||
shown.removeAll { gone.contains($0) }
|
||||
if shown.count < 2 { shown = Tab.defaultLayout }
|
||||
commit()
|
||||
}
|
||||
} header: { Header(text: "In the bar") } footer: { Footer(text: "Drag to reorder, swipe to remove. The bar holds \(Tab.maxVisible) tabs.") }
|
||||
} header: { Header(text: "In the bar") } footer: { Footer(text: "Swipe to remove. The bar holds \(Tab.maxVisible) tabs.") }
|
||||
if !hidden.isEmpty {
|
||||
Section {
|
||||
ForEach(hidden, id: \.self) { t in
|
||||
|
||||
Reference in new issue
Block a user