Files
QueueCube/ios/QueueCube/Views/NowPlayingView.swift

174 lines
5.7 KiB
Swift

//
// NowPlayingView.swift
// QueueCube
//
// Created by James Magahern on 3/3/25.
//
import SwiftUI
@Observable
class NowPlayingViewModel
{
var onPlayPause: (NowPlayingViewModel) -> Void = { _ in }
var onStop: (NowPlayingViewModel) -> Void = { _ in }
var onNext: (NowPlayingViewModel) -> Void = { _ in }
var onPrev: (NowPlayingViewModel) -> Void = { _ in }
var onSheetDismiss: (NowPlayingViewModel) -> Void = { _ in }
var isPlaying: Bool = false
var title: String? = ""
var subtitle: String? = ""
var volume: Double = 0.5
fileprivate var isSettingVolume: Bool = false
fileprivate var settingVolume: Double = 0.0 {
didSet { volume = settingVolume }
}
}
struct NowPlayingView: View
{
@State var model: NowPlayingViewModel
private var nothingQueued: Bool { model.title == nil && model.subtitle == nil }
var body: some View {
NavigationStack {
VStack {
Spacer()
.frame(height: 1.0)
VStack {
if let title = model.title {
Text(title)
.font(.title3)
.lineLimit(1)
.bold()
}
if let subtitle = model.subtitle {
Text(subtitle)
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(2)
}
if nothingQueued {
Text(.notPlaying)
.font(.title2)
.foregroundStyle(.secondary)
}
}
Spacer(minLength: 24.0)
VStack {
HStack {
ForEach(Buttons.allCases) { button in
Spacer()
Button(action: button.action(model: model)) {
Image(systemName: button.imageName(isPlaying: model.isPlaying))
.resizable()
.aspectRatio(1.0, contentMode: .fit)
.scaleEffect(button.scale, anchor: .center)
.tint(button.tintColor)
}
.disabled(nothingQueued)
Spacer()
}
}
.imageScale(.large)
.frame(height: 34.0)
.tint(.label)
Spacer()
Slider(
value: model.isSettingVolume ? $model.settingVolume : $model.volume,
in: 0.0...1.0,
onEditingChanged: { editing in
if model.isSettingVolume != editing {
model.settingVolume = model.volume
model.isSettingVolume = editing
}
}
)
.padding(.horizontal, 18.0)
.padding(.bottom, -12.0) // intrinsic sizing bug workaround?
}
.padding(.vertical, 44.0)
.padding(.horizontal, 12.0)
.frame(maxWidth: .infinity)
.background(
RoundedRectangle(cornerRadius: 14.0)
.fill(.ultraThinMaterial)
.stroke(Color.label.opacity(0.08))
)
}
.padding(.horizontal, 15.0)
.padding(.bottom, 10.0)
.toolbar {
ToolbarItemGroup(placement: .topBarTrailing) {
Button {
model.onSheetDismiss(model)
} label: {
Image(systemName: "xmark.circle.fill")
.tint(.secondary)
}
}
}
}
}
// MARK: - Types
private enum Buttons: Int, CaseIterable, Identifiable {
case backward
case stop
case playPause
case forward
var id: Int { rawValue }
var scale: Double {
switch self {
case .backward: 0.7
case .forward: 0.7
case .playPause: 1.0
case .stop: 0.8
}
}
var tintColor: Color {
switch self {
case .backward: .label.mix(with: .gray, by: 0.5)
case .forward: .label.mix(with: .gray, by: 0.5)
case .playPause: .label
case .stop: .label
}
}
func imageName(isPlaying: Bool) -> String {
switch self {
case .backward: "backward.fill"
case .stop: "stop.fill"
case .playPause: isPlaying ? "pause.fill" : "play.fill"
case .forward: "forward.fill"
}
}
func action(model: NowPlayingViewModel) -> () -> Void {
switch self {
case .backward: { model.onPrev(model) }
case .stop: { model.onStop(model) }
case .playPause: { model.onPlayPause(model) }
case .forward: { model.onNext(model) }
}
}
}
}