iOS: stem playlist refresh and covers, download queue page, reorderable Library, whole-library radio
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7c8dfb54ca
commit
a4c870251c
18 files changed
+514
-30
No files matched your search
@@ -403,9 +403,8 @@ struct BackupCells: View {
|
||||
/// Said on both server screens: stems are found by file path, so libraries have to line up.
|
||||
private let matchNote = "For stems to work, every Plex or Jellyfin music library here has to match a stem library one-to-one: the same folders with the same files, as your stem server sees them. Stems are found by each song's file path, so a library with no matching stem folder plays without stems, and songs that are only on one side don't get stems at all."
|
||||
|
||||
struct ServersScreen: View {
|
||||
struct ServerListScreen: View {
|
||||
@Environment(\.palette) private var p
|
||||
@State private var libs: [String: [MusicSection]] = [:]
|
||||
private var g: AppGraph { .shared }
|
||||
|
||||
var body: some View {
|
||||
@@ -417,7 +416,49 @@ struct ServersScreen: View {
|
||||
NavCell(title: s.label, value: s.ready ? s.sectionTitles : (s.signedIn ? "No Libraries" : "Not Signed In"),
|
||||
icon: "server.rack", iconBg: s.kind == .plex ? 0xE5A00D : 0xAA5CC3, route: .Server(s.id))
|
||||
}
|
||||
} header: { Header(text: "Music Servers") } footer: {
|
||||
} header: { Header(text: "Servers") } footer: {
|
||||
Footer(text: "Every server's picked libraries are pooled into one library. Playlists stay on the server they were made on.")
|
||||
}
|
||||
Section {
|
||||
NavCell(title: "Add Plex Server", icon: "plus", iconBg: 0xE5A00D, route: .Server("new-plex"))
|
||||
NavCell(title: "Add Jellyfin Server", icon: "plus", iconBg: 0xAA5CC3, route: .Server("new-jellyfin"))
|
||||
} header: { Header(text: "Add") } footer: { Footer(text: matchNote) }
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.themedList()
|
||||
.navigationTitle("Music Servers")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
struct ServersScreen: View {
|
||||
@Environment(\.palette) private var p
|
||||
@Environment(\.openURL) private var openURL
|
||||
@State private var libs: [String: [MusicSection]] = [:]
|
||||
@State private var busy = false
|
||||
@State private var status: String?
|
||||
@State private var signInTask: Task<Void, Never>?
|
||||
private var g: AppGraph { .shared }
|
||||
|
||||
var body: some View {
|
||||
let list = g.settings.servers
|
||||
List {
|
||||
Section {
|
||||
if busy {
|
||||
HStack { ProgressView(); Text(status ?? "Working…").foregroundStyle(p.label2) }.themedRow()
|
||||
} else {
|
||||
ActionCell(title: "Sign in with Plex") { signIn() }
|
||||
if !g.settings.plexAccountToken.isEmpty {
|
||||
ActionCell(title: "Refresh Servers") { refresh(g.settings.plexAccountToken) }
|
||||
}
|
||||
if let status { Text(status).font(.footnote).foregroundStyle(p.label2).themedRow() }
|
||||
}
|
||||
} header: { Header(text: "Plex Account") } footer: {
|
||||
Footer(text: "Sign in at plex.tv to add every server on your account and all of their music libraries at once.")
|
||||
}
|
||||
Section {
|
||||
NavCell(title: "Music Servers", value: list.isEmpty ? "None" : "\(list.count)", icon: "server.rack", iconBg: 0xE5A00D, route: .ServerList)
|
||||
} footer: {
|
||||
Footer(text: "Every server's picked libraries are pooled into one library. Playlists stay on the server they were made on.")
|
||||
}
|
||||
ForEach(list.filter(\.signedIn)) { s in
|
||||
@@ -433,20 +474,64 @@ struct ServersScreen: View {
|
||||
}
|
||||
} header: { Header(text: s.label + " Libraries") }
|
||||
}
|
||||
Section {
|
||||
NavCell(title: "Add Plex Server", icon: "plus", iconBg: 0xE5A00D, route: .Server("new-plex"))
|
||||
NavCell(title: "Add Jellyfin Server", icon: "plus", iconBg: 0xAA5CC3, route: .Server("new-jellyfin"))
|
||||
} header: { Header(text: "Add") } footer: { Footer(text: matchNote) }
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.themedList()
|
||||
.navigationTitle("Music Servers")
|
||||
.navigationTitle("Plex Account")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.task {
|
||||
for s in list where s.signedIn && libs[s.id] == nil {
|
||||
libs[s.id] = (try? await g.plex.make(s).musicSections()) ?? []
|
||||
}
|
||||
}
|
||||
.onDisappear { signInTask?.cancel() }
|
||||
}
|
||||
|
||||
/// plex.tv sign-in in the browser, the same flow as the web app.
|
||||
private func signIn() {
|
||||
signInTask?.cancel()
|
||||
busy = true; status = "Waiting for plex.tv…"
|
||||
signInTask = Task {
|
||||
let tv = g.plex.plexTv()
|
||||
do {
|
||||
let pin = try await tv.createStrongPin()
|
||||
guard let url = tv.authUrl(code: pin.code) else { throw URLError(.badURL) }
|
||||
openURL(url)
|
||||
for _ in 0..<150 {
|
||||
try await Task.sleep(for: .seconds(2))
|
||||
if let t = try? await tv.checkPin(pin.id).authToken, !t.isEmpty {
|
||||
await sync(t); return
|
||||
}
|
||||
}
|
||||
status = "Sign-in timed out."
|
||||
} catch is CancellationError {
|
||||
} catch {
|
||||
status = "Sign-in failed: \(error.localizedDescription)"
|
||||
}
|
||||
busy = false
|
||||
}
|
||||
}
|
||||
|
||||
private func refresh(_ token: String) {
|
||||
busy = true; status = "Finding servers…"
|
||||
signInTask = Task { await sync(token) }
|
||||
}
|
||||
|
||||
private func sync(_ token: String) async {
|
||||
status = "Finding servers…"
|
||||
do {
|
||||
let r = try await g.plex.syncAccount(token: token)
|
||||
g.settings.plexAccountToken = token
|
||||
libs = [:]
|
||||
status = r.saved == 0 && r.unreachable == 0 ? "No Plex servers on this account."
|
||||
: "\(r.saved) server\(r.saved == 1 ? "" : "s") added or updated" + (r.unreachable > 0 ? ", \(r.unreachable) unreachable." : ".")
|
||||
for s in g.settings.servers where s.signedIn {
|
||||
libs[s.id] = (try? await g.plex.make(s).musicSections()) ?? []
|
||||
}
|
||||
} catch {
|
||||
status = "Sync failed: \(error.localizedDescription)"
|
||||
}
|
||||
busy = false
|
||||
}
|
||||
|
||||
/// Turns one library of a server on or off. All off keeps the sign-in.
|
||||
@@ -698,6 +783,7 @@ struct StemServerScreen: View {
|
||||
case .unknown: "—"
|
||||
}
|
||||
}()).themedRow()
|
||||
ValueCell(title: "stemd", value: g.stems.liveLine ?? (g.stems.failed.isEmpty ? "Idle" : "\(g.stems.failed.count) failed")).themedRow()
|
||||
ValueCell(title: "Opus Transcoding", value: {
|
||||
if case .online(_, let t) = status { return t ? "Available" : "No ffmpeg" }
|
||||
return "—"
|
||||
|
||||
Reference in new issue
Block a user