iOS 1.0 RC 2: Plex streams under upload caps, shuffle and album radio stations, Recently Added list, photo covers, crossfade

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-11-06 08:40:37 -08:00
1 parent c0e33b5e3e
commit 2fb8040388
17 files changed
+412 -74

No files matched your search

+54 -2
View File
@@ -1,5 +1,6 @@
import Foundation
import Observation
import UIKit
/// One song in a stem playlist, saved with the mix it plays with.
struct StemPlaylistItem: Codable, Hashable, Identifiable {
@@ -20,7 +21,12 @@ struct StemPlaylist: Codable, Hashable, Identifiable {
var tracks: [Track] { items.map(\.track) }
var mixes: [[String: StemSetting]] { items.map(\.mix) }
var thumb: String? { artThumb ?? items.lazy.compactMap(\.track.thumb).first }
/// A cover picked from Photos ("local:<file>") falls back to the songs' art if its file is gone
/// (an imported list, say).
var thumb: String? {
if let a = artThumb, !a.hasPrefix(StemPlaylists.localPrefix) || StemPlaylists.localArt(a) != nil { return a }
return items.lazy.compactMap(\.track.thumb).first
}
var durationMs: Int64 { items.reduce(0) { $0 + $1.track.durationMs } }
}
@@ -53,7 +59,52 @@ final class StemPlaylists {
edit(id) { $0.items += tracks.map { StemPlaylistItem(track: $0, mix: MixPresets.normalized(mix)) } }
}
func setArt(_ id: String, _ thumb: String?) { edit(id) { $0.artThumb = thumb } }
func setArt(_ id: String, _ thumb: String?) {
let old = list(id)?.artThumb
edit(id) { $0.artThumb = thumb }
if old != thumb { Self.removeLocalArt(old) }
}
// ---- covers from Photos -----------------------------------------------------------------
nonisolated static let localPrefix = "local:"
nonisolated static var artDir: URL {
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0].appendingPathComponent("playlist-art", isDirectory: true)
}
/// The file behind a "local:" thumb, if it's still there.
nonisolated static func localArt(_ thumb: String) -> URL? {
guard thumb.hasPrefix(localPrefix) else { return nil }
let name = String(thumb.dropFirst(localPrefix.count))
guard !name.isEmpty, !name.contains("/") else { return nil }
let u = artDir.appendingPathComponent(name)
return FileManager.default.fileExists(atPath: u.path) ? u : nil
}
nonisolated private static func removeLocalArt(_ thumb: String?) {
if let thumb, let u = localArt(thumb) { try? FileManager.default.removeItem(at: u) }
}
/// Saves [data] (any image format Photos hands over) as the list's cover, scaled to 1200 px.
/// Each pick gets a new file name so cached art doesn't show the old one.
@discardableResult
func setPhotoArt(_ id: String, _ data: Data) -> Bool {
guard let img = UIImage(data: data) else { return false }
let side: CGFloat = 1200
let scale = min(1, side / max(img.size.width, img.size.height, 1))
let size = CGSize(width: (img.size.width * scale).rounded(), height: (img.size.height * scale).rounded())
let fmt = UIGraphicsImageRendererFormat.default(); fmt.scale = 1
let jpeg = UIGraphicsImageRenderer(size: size, format: fmt).jpegData(withCompressionQuality: 0.85) { _ in
img.draw(in: CGRect(origin: .zero, size: size))
}
let name = "\(id)-\(UUID().uuidString.prefix(8).lowercased()).jpg"
do {
try FileManager.default.createDirectory(at: Self.artDir, withIntermediateDirectories: true)
try jpeg.write(to: Self.artDir.appendingPathComponent(name), options: .atomic)
} catch { return false }
setArt(id, Self.localPrefix + name)
return true
}
/// Re-reads every song from the library so renamed, retagged or re-added songs match again.
/// Keeps each item's uid and mix, and leaves songs the library can't find as they are.
@@ -100,6 +151,7 @@ final class StemPlaylists {
func rename(_ id: String, _ title: String) { edit(id) { $0.title = title } }
func delete(_ id: String) {
Self.removeLocalArt(list(id)?.artThumb)
lists.removeAll { $0.id == id }
save()
}