iOS 1.0.8: stem playlists, karaoke softness slider in lyrics
- Playlists tab splits into Plex and Stem. Plex playlists are unchanged and now show their custom poster when one is set. - Stem playlists live on the device; each song keeps its own mix. Add from any song or collection menu, or copy a whole Plex playlist. - Queue items can carry a mix, which wins over the song's saved mix. - Softness slider under the Vocals picker in lyrics when set to Soft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6f634522e5
commit
e2cf62cb07
10 files changed
+374
-27
No files matched your search
@@ -17,6 +17,7 @@ struct MediaMenu {
|
||||
a.append(SheetAction("Remove Download", icon: "trash", destructive: true) { g.downloads.remove([t]) })
|
||||
}
|
||||
a.append(SheetAction("Add to Playlist…", icon: "text.badge.plus") { addToPlaylist([t]) })
|
||||
a.append(SheetAction("Add to Stem Playlist…", icon: "slider.horizontal.3") { addToStemPlaylist([t]) })
|
||||
if t.partFile != nil, g.stems.index.size > 0, g.stems.find(t) == nil { a.append(stemNow { [t] }) }
|
||||
if allowGoTo, let k = t.albumKey {
|
||||
a.append(SheetAction("Go to Album", icon: "square.stack") {
|
||||
@@ -52,10 +53,49 @@ struct MediaMenu {
|
||||
|
||||
func playlist(_ pl: Playlist, extra: [SheetAction] = []) -> [SheetAction] {
|
||||
let plex = g.plex
|
||||
return collection(pl.title, [], false, { try await plex.playlistTracks(pl.ratingKey) }, playlistKey: pl.ratingKey, extra: extra,
|
||||
let copy = SheetAction("Copy to Stem Playlist…", icon: "doc.on.doc") { copyToStems(pl) }
|
||||
return collection(pl.title, [], false, { try await plex.playlistTracks(pl.ratingKey) }, playlistKey: pl.ratingKey, extra: [copy] + extra,
|
||||
confirmDownload: pl.trackCount > 50)
|
||||
}
|
||||
|
||||
func stemPlaylist(_ pl: StemPlaylist, extra: [SheetAction] = [], onPage: Bool = false) -> [SheetAction] {
|
||||
var a = [
|
||||
SheetAction("Add to Front of Queue", icon: "text.line.first.and.arrowtriangle.forward") {
|
||||
g.playback.playNext(pl.tracks, mixes: pl.mixes); overlays.hud("Playing Next", "text.line.first.and.arrowtriangle.forward")
|
||||
},
|
||||
SheetAction("Add to End of Queue", icon: "text.line.last.and.arrowtriangle.forward") {
|
||||
g.playback.addToQueue(pl.tracks, mixes: pl.mixes); overlays.hud("Added to Queue", "text.line.last.and.arrowtriangle.forward")
|
||||
},
|
||||
]
|
||||
let missing = pl.tracks.filter { !g.downloads.isDownloaded($0.ratingKey) }
|
||||
if !missing.isEmpty {
|
||||
a.append(SheetAction("Download", icon: "arrow.down.circle") { g.downloads.download(missing); overlays.hud("Downloading", "arrow.down.circle") })
|
||||
}
|
||||
if pl.items.isEmpty { a = [] }
|
||||
let lists = g.stemPlaylists
|
||||
a.append(SheetAction("Delete Stem Playlist", icon: "trash", destructive: true) {
|
||||
overlays.confirm("Delete “\(pl.title)”?", "Stem playlists are only on this device.", "Delete") { lists.delete(pl.id); if onPage { ui.pop() } }
|
||||
})
|
||||
return extra + a
|
||||
}
|
||||
|
||||
/// A song in a stem playlist: queue it with its mix, change the mix, or take it out.
|
||||
func stemItem(_ item: StemPlaylistItem, in listId: String) -> [SheetAction] {
|
||||
let lists = g.stemPlaylists
|
||||
return [
|
||||
SheetAction("Add to Front of Queue", icon: "text.line.first.and.arrowtriangle.forward") {
|
||||
g.playback.playNext([item.track], mixes: [item.mix]); overlays.hud("Playing Next", "text.line.first.and.arrowtriangle.forward")
|
||||
},
|
||||
SheetAction("Add to End of Queue", icon: "text.line.last.and.arrowtriangle.forward") {
|
||||
g.playback.addToQueue([item.track], mixes: [item.mix]); overlays.hud("Added to Queue", "text.line.last.and.arrowtriangle.forward")
|
||||
},
|
||||
SheetAction("Change Mix…", icon: "slider.horizontal.3") {
|
||||
chooseMix([item.track]) { lists.setMix(listId, item: item.uid, $0) }
|
||||
},
|
||||
SheetAction("Remove from Stem Playlist", icon: "minus.circle", destructive: true) { lists.remove(listId, item: item.uid) },
|
||||
]
|
||||
}
|
||||
|
||||
/// For songs already in hand (downloads with no Plex album behind them).
|
||||
func tracks(_ title: String, _ tracks: [Track], extra: [SheetAction] = []) -> [SheetAction] {
|
||||
let have = tracks.filter { g.downloads.isDownloaded($0.ratingKey) }
|
||||
@@ -90,6 +130,7 @@ struct MediaMenu {
|
||||
})
|
||||
}
|
||||
a.append(SheetAction("Add to Playlist…", icon: "text.badge.plus") { withTracks(load) { addToPlaylist($0, skip: playlistKey) } })
|
||||
a.append(SheetAction("Add to Stem Playlist…", icon: "slider.horizontal.3") { withTracks(load) { addToStemPlaylist($0) } })
|
||||
if !stemmed { a.append(stemNow(load)) }
|
||||
return a + extra
|
||||
}
|
||||
@@ -160,6 +201,66 @@ struct MediaMenu {
|
||||
}
|
||||
}
|
||||
|
||||
/// Picks the mix the songs get in a stem playlist: what's playing now, the song's saved mix, or a preset.
|
||||
func chooseMix(_ tracks: [Track], _ then: @escaping ([String: StemSetting]) -> Void) {
|
||||
let pm = g.playback
|
||||
var acts: [SheetAction] = []
|
||||
if tracks.count == 1, let t = tracks.first {
|
||||
if pm.current?.track.ratingKey == t.ratingKey {
|
||||
let m = pm.mix
|
||||
acts.append(SheetAction("Current Mix (\(StemPlaylists.label(m)))", icon: "play.circle") { then(m) })
|
||||
}
|
||||
if let m = pm.songMixes[t.ratingKey] {
|
||||
acts.append(SheetAction("This Song’s Mix (\(StemPlaylists.label(m)))", icon: "bookmark") { then(m) })
|
||||
}
|
||||
}
|
||||
for p in MixPresets.all {
|
||||
guard let m = p.build(StemPlaylists.allStems) else { continue }
|
||||
acts.append(SheetAction(StemPlaylists.label(m)) { then(m) })
|
||||
}
|
||||
overlays.actions("Choose a Mix", acts)
|
||||
}
|
||||
|
||||
func addToStemPlaylist(_ tracks: [Track]) {
|
||||
let lists = g.stemPlaylists
|
||||
overlays.actions("Add \(songs(tracks.count)) to Stem Playlist", [SheetAction("New Stem Playlist…") { newStemPlaylist(tracks) }] + lists.lists.map { pl in
|
||||
SheetAction(pl.title) {
|
||||
chooseMix(tracks) { m in
|
||||
lists.add(pl.id, tracks: tracks, mix: m)
|
||||
overlays.hud("Added to \(pl.title)")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func newStemPlaylist(_ tracks: [Track] = [], onCreated: @escaping (StemPlaylist) -> Void = { _ in }) {
|
||||
let lists = g.stemPlaylists
|
||||
overlays.prompt("New Stem Playlist", "Every song in it plays with the mix you pick.", "Create", placeholder: "Playlist name") { name in
|
||||
if tracks.isEmpty {
|
||||
onCreated(lists.create(name))
|
||||
return
|
||||
}
|
||||
chooseMix(tracks) { m in
|
||||
onCreated(lists.create(name, tracks: tracks, mix: m))
|
||||
overlays.hud("Created “\(name)”")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes a stem playlist from a Plex one, every song with the same mix.
|
||||
func copyToStems(_ pl: Playlist, onCreated: @escaping (StemPlaylist) -> Void = { _ in }) {
|
||||
let plex = g.plex
|
||||
let lists = g.stemPlaylists
|
||||
overlays.prompt("Copy to Stem Playlist", "Every song gets the mix you pick next; change single songs later.", initial: pl.title, "Next") { name in
|
||||
withTracks({ try await plex.playlistTracks(pl.ratingKey) }) { tracks in
|
||||
chooseMix(tracks) { m in
|
||||
onCreated(lists.create(name, tracks: tracks, mix: m))
|
||||
overlays.hud("Copied \(songs(tracks.count))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newPlaylist(_ tracks: [Track], onCreated: @escaping () -> Void = {}) {
|
||||
let plex = g.plex
|
||||
overlays.prompt("New Playlist", "Name it; you can add more songs later.", "Create", placeholder: "Playlist name") { name in
|
||||
|
||||
Reference in new issue
Block a user