174 lines
5.7 KiB
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) }
|
|
}
|
|
}
|
|
}
|
|
}
|