ios: Liquid Glass mini player, adjustable karaoke softness (1.0.7)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cdnutterandClaude Opus 5.5 committed 2026-10-20 09:39:02 -07:00
1 parent e98d595365
commit d52a3e257f
6 files changed
+163 -56

No files matched your search

+90 -46
View File
@@ -794,56 +794,100 @@ struct MiniPlayer: View {
private var g: AppGraph { .shared }
var body: some View {
if let t = g.playback.current?.track {
if #available(iOS 26.1, *) { GlassMiniPlayer(track: t) } else { card(t) }
}
}
/// Before iOS 26: a card floating over each tab.
@ViewBuilder private func card(_ t: Track) -> some View {
let pm = g.playback
if let t = pm.current?.track {
HStack(spacing: 0) {
Artwork(thumb: t.thumb, size: 44, corner: 8).padding(.trailing, 12)
VStack(alignment: .leading, spacing: 1) {
Text(t.title).font(.subheadline.weight(.semibold)).foregroundStyle(p.label).lineLimit(1)
Text(t.artist).font(.footnote).foregroundStyle(p.label2).lineLimit(1)
}
Spacer(minLength: 6)
Button(action: pm.togglePlay) {
ZStack {
if pm.buffering && pm.playWhenReady { ProgressView() }
else { Image(systemName: pm.playWhenReady ? "pause.fill" : "play.fill").font(.system(size: 22)).contentTransition(.symbolEffect(.replace)) }
}
.animation(.snappy(duration: 0.25), value: pm.playWhenReady)
.foregroundStyle(p.label).frame(width: 44, height: 44).contentShape(Rectangle())
}
.buttonStyle(PressFade())
Button(action: pm.next) {
Image(systemName: "forward.fill").font(.system(size: 20)).foregroundStyle(p.label).frame(width: 44, height: 44).contentShape(Rectangle())
}
.buttonStyle(PressFade())
HStack(spacing: 0) {
Artwork(thumb: t.thumb, size: 44, corner: 8).padding(.trailing, 12)
VStack(alignment: .leading, spacing: 1) {
Text(t.title).font(.subheadline.weight(.semibold)).foregroundStyle(p.label).lineLimit(1)
Text(t.artist).font(.footnote).foregroundStyle(p.label2).lineLimit(1)
}
.padding(.leading, 8).padding(.trailing, 6)
.frame(height: 60)
.background {
// A solid card in the app's own colours instead of system glass.
let shape = RoundedRectangle(cornerRadius: 14, style: .continuous)
ZStack {
shape.fill(p.cell)
shape.fill(p.fill.opacity(p.dark ? 0.5 : 0))
shape.strokeBorder(p.separator, lineWidth: 1 / scale)
}
.shadow(color: .black.opacity(p.dark ? 0.5 : 0.12), radius: 12, y: 4)
Spacer(minLength: 6)
MiniTransport(color: p.label, next: true)
}
.padding(.leading, 8).padding(.trailing, 6)
.frame(height: 60)
.background {
// A solid card in the app's own colours instead of system glass.
let shape = RoundedRectangle(cornerRadius: 14, style: .continuous)
ZStack {
shape.fill(p.cell)
shape.fill(p.fill.opacity(p.dark ? 0.5 : 0))
shape.strokeBorder(p.separator, lineWidth: 1 / scale)
}
.overlay(alignment: .bottom) {
// Thin progress line along the bottom of the card.
TimelineView(.animation(minimumInterval: 0.25, paused: !pm.playWhenReady)) { _ in
let d = pm.durationMs
let f = d > 0 ? min(max(Double(pm.positionMs()) / Double(d), 0), 1) : 0
GeometryReader { geo in p.accent.frame(width: geo.size.width * f, height: 2) }.frame(height: 2)
}
.padding(.horizontal, 14)
.shadow(color: .black.opacity(p.dark ? 0.5 : 0.12), radius: 12, y: 4)
}
.overlay(alignment: .bottom) {
// Thin progress line along the bottom of the card.
TimelineView(.animation(minimumInterval: 0.25, paused: !pm.playWhenReady)) { _ in
let d = pm.durationMs
let f = d > 0 ? min(max(Double(pm.positionMs()) / Double(d), 0), 1) : 0
GeometryReader { geo in p.accent.frame(width: geo.size.width * f, height: 2) }.frame(height: 2)
}
.padding(.horizontal, 8)
.padding(.bottom, 6)
.contentShape(Rectangle())
.onTapGesture { ui.openNowPlaying() }
.gesture(DragGesture(minimumDistance: 20).onEnded { v in if v.translation.height < -30 { ui.openNowPlaying() } })
.actionsMenu(ui.menu.track(t))
.padding(.horizontal, 14)
}
.padding(.horizontal, 8)
.padding(.bottom, 6)
.contentShape(Rectangle())
.onTapGesture { ui.openNowPlaying() }
.gesture(DragGesture(minimumDistance: 20).onEnded { v in if v.translation.height < -30 { ui.openNowPlaying() } })
.actionsMenu(ui.menu.track(t))
}
}
/// iOS 26: lives in the tab bar's Liquid Glass accessory, like Music. Shrinks to title and play when the bar collapses.
@available(iOS 26.1, *)
private struct GlassMiniPlayer: View {
let track: Track
@Environment(Ui.self) private var ui
@Environment(\.tabViewBottomAccessoryPlacement) private var placement
private var inline: Bool { placement == .inline }
var body: some View {
HStack(spacing: 10) {
Artwork(thumb: track.thumb, size: inline ? 28 : 34, corner: 6)
VStack(alignment: .leading, spacing: 0) {
Text(track.title).font(.subheadline.weight(.semibold)).lineLimit(1)
if !inline { Text(track.artist).font(.caption).foregroundStyle(.secondary).lineLimit(1) }
}
Spacer(minLength: 4)
MiniTransport(color: .primary, next: !inline)
}
.padding(.leading, 10).padding(.trailing, 6)
.contentShape(Rectangle())
.onTapGesture { ui.openNowPlaying() }
.actionsMenu(ui.menu.track(track))
}
}
/// Play/pause (spinner while buffering) and next, for the mini player.
private struct MiniTransport: View {
let color: Color
let next: Bool
private var g: AppGraph { .shared }
var body: some View {
let pm = g.playback
Button(action: pm.togglePlay) {
ZStack {
if pm.buffering && pm.playWhenReady { ProgressView() }
else { Image(systemName: pm.playWhenReady ? "pause.fill" : "play.fill").font(.system(size: 22)).contentTransition(.symbolEffect(.replace)) }
}
.animation(.snappy(duration: 0.25), value: pm.playWhenReady)
.foregroundStyle(color).frame(width: 44, height: 44).contentShape(Rectangle())
}
.buttonStyle(PressFade())
if next {
Button(action: pm.next) {
Image(systemName: "forward.fill").font(.system(size: 20)).foregroundStyle(color).frame(width: 44, height: 44).contentShape(Rectangle())
}
.buttonStyle(PressFade())
}
}
}