Stemify iOS 1.1 and WebUI updates, repo restructure into desktop/ios/server/web

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
cdnutterandClaude Sonnet 5.5 committed 2026-10-24 17:08:51 -07:00
1 parent 894dd5a695
commit df5d847169
157 files changed
+3935 -742

No files matched your search

+24 -10
View File
@@ -8,13 +8,17 @@ import UIKit
final class NowPlaying: @unchecked Sendable {
private let pm: PlaybackManager
private let plex: PlexClient
private let radio: WebRadio
private let settings: Settings
private let center = MPNowPlayingInfoCenter.default()
private var artKey: String?
private var artwork: MPMediaItemArtwork?
init(pm: PlaybackManager, plex: PlexClient) {
init(pm: PlaybackManager, plex: PlexClient, radio: WebRadio, settings: Settings) {
self.pm = pm
self.plex = plex
self.radio = radio
self.settings = settings
pm.addListener { [weak self] in self?.publish() }
wireCommands()
publish()
@@ -25,13 +29,14 @@ final class NowPlaying: @unchecked Sendable {
/// The mix shown next to the artist: nil unless stems are playing or the mix is Full.
var mixLabel: String? {
guard pm.source == .Stems else { return nil }
guard let p = MixPresets.current(pm.stemNames, pm.mix) else { return "Custom mix" }
guard let p = MixPresets.current(settings, pm.stemNames, pm.mix) else { return "Custom mix" }
return p == MixPresets.full ? nil : p.label
}
var vocalsAvailable: Bool { pm.source == .Stems && pm.stemNames.contains("vocals") }
var vocalsSilent: Bool { MixPresets.vocalsSilent(pm.stemNames, pm.mix) }
var nextMix: MixPreset { MixPresets.next(pm.stemNames, pm.mix) }
/// Follows the Stem Editor: deleted mixes are skipped and the user's own are in the rotation.
var nextMix: MixPreset { MixPresets.next(settings, pm.stemNames, pm.mix) }
/// One tap: hear the vocals again, or take them out.
func toggleVocals() {
@@ -50,7 +55,11 @@ final class NowPlaying: @unchecked Sendable {
// ---- info --------------------------------------------------------------------------
/// Puts the queue back on the lock screen (after the web radio stops).
func refresh() { publish() }
private func publish() {
if radio.active { return }
guard let t = pm.current?.track else {
center.nowPlayingInfo = nil
center.playbackState = .stopped
@@ -92,16 +101,21 @@ final class NowPlaying: @unchecked Sendable {
private func wireCommands() {
let c = MPRemoteCommandCenter.shared()
func on(_ cmd: MPRemoteCommand, _ f: @escaping (MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus) {
func on(_ cmd: MPRemoteCommand, _ f: @escaping @MainActor (MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus) {
cmd.isEnabled = true
cmd.addTarget { e in MainActor.assumeIsolated { f(e) } }
}
on(c.playCommand) { [pm] _ in pm.play(); return .success }
on(c.pauseCommand) { [pm] _ in pm.pause(); return .success }
on(c.stopCommand) { [pm] _ in pm.pause(); return .success }
on(c.togglePlayPauseCommand) { [pm] _ in pm.togglePlay(); return .success }
on(c.nextTrackCommand) { [pm] _ in pm.next(); return .success }
on(c.previousTrackCommand) { [pm] _ in pm.previous(); return .success }
// While a web radio station is loaded, play/pause drive it and skipping does nothing.
on(c.playCommand) { [pm, radio] _ in if radio.active { radio.resume() } else { pm.play() }; return .success }
on(c.pauseCommand) { [pm, radio] _ in if radio.active { radio.pause() } else { pm.pause() }; return .success }
on(c.stopCommand) { [pm, radio] _ in if radio.active { radio.stop() } else { pm.pause() }; return .success }
on(c.togglePlayPauseCommand) { [pm, radio] _ in if radio.active { radio.toggle() } else { pm.togglePlay() }; return .success }
on(c.nextTrackCommand) { [pm, radio] _ in if !radio.active { pm.next() }; return .success }
on(c.previousTrackCommand) { [weak self, pm, radio] _ in
guard !radio.active else { return .success }
if self?.settings.lockScreenMix == true && pm.source == .Stems { self?.cycleMix() } else { pm.previous() }
return .success
}
on(c.changePlaybackPositionCommand) { [pm] e in
guard let e = e as? MPChangePlaybackPositionCommandEvent else { return .commandFailed }
pm.seekTo(Int64(e.positionTime * 1000))