Files
QueueCube/ios/QueueCube/Views/PlaylistView.swift

213 lines
6.8 KiB
Swift

//
// PlaylistView.swift
// QueueCube
//
// Created by James Magahern on 3/3/25.
//
import SwiftUI
struct MediaListItem: Identifiable
{
let _id: String
let title: String
let filename: String
let index: Int?
let isCurrent: Bool
let playbackError: String?
var id: String {
_id + filename // temporary: we get duplicate ids from the server sometimes...
}
init(id: String, title: String, filename: String, index: Int? = nil, isCurrent: Bool = false, playbackError: String? = nil) {
self._id = id
self.title = title
self.filename = filename
self.index = index
self.isCurrent = isCurrent
self.playbackError = playbackError
}
}
enum MediaListMode {
case playlist
case favorites
}
@Observable
class MediaListViewModel
{
let mode: MediaListMode
var isPlaying: Bool = false
var items: [MediaListItem] = []
var onSeek: (MediaListItem) -> Void = { _ in }
var onPlay: (MediaListItem) -> Void = { _ in }
var onQueue: (MediaListItem) -> Void = { _ in }
var onEdit: (MediaListItem) -> Void = { _ in }
var onFavorite: (MediaListItem) -> Void = { _ in }
var onDelete: (MediaListItem) -> Void = { _ in }
init(mode: MediaListMode) {
self.mode = mode
}
}
struct MediaListView: View
{
@Binding var model: MediaListViewModel
@State private var errorAlertItem: MediaListItem? = nil
@State private var isShowingErrorAlert: Bool = false
var body: some View {
VStack {
if model.items.isEmpty {
let title: LocalizedStringKey = switch model.mode {
case .playlist: .playlistEmpty
case .favorites: .favoritesEmpty
}
contentPlaceholderView(title: title, systemImage: "list.bullet")
} else {
List($model.items, editActions: .delete) { item in
let item = item.wrappedValue
let state = item.isCurrent ? (model.isPlaying ? MediaItemCell.State.playing : MediaItemCell.State.paused) : .queued
Button {
if let _ = item.playbackError {
errorAlertItem = item
isShowingErrorAlert = true
} else {
switch model.mode {
case .playlist:
model.onSeek(item)
case .favorites:
model.onPlay(item)
}
}
} label: {
MediaItemCell(
title: item.title,
subtitle: item.filename,
state: state,
playbackError: item.playbackError
)
}
.listRowBackground(
item.playbackError != nil ? Color.red.opacity(0.15) :
(model.mode == .playlist && state != .queued ? Color.accentColor.opacity(0.10) : nil)
)
.contextMenu {
Button(.copyTitle) {
UIPasteboard.general.string = item.title
}
Button(.copyURL) {
if let url = URL(string: item.filename) {
UIPasteboard.general.url = url
} else {
UIPasteboard.general.string = item.filename
}
}
if model.mode == .favorites {
Button(.edit) {
model.onEdit(item)
}
}
}
.swipeActions(edge: .leading) {
if model.mode == .favorites {
Button {
model.onQueue(item)
} label: {
Image(systemName: "plus.square.on.square")
Text(.addToQueue)
}
.tint(.blue)
} else if model.mode == .playlist {
Button {
model.onFavorite(item)
} label: {
Image(systemName: "star")
Text(.favorite)
}
.tint(.yellow)
}
}
}
.alert(.playbackError, isPresented: $isShowingErrorAlert, presenting: errorAlertItem) { item in
Button(.cancel, role: .cancel) {
errorAlertItem = nil
isShowingErrorAlert = false
}
Button(.delete, role: .destructive) {
model.items.removeAll { $0.id == item.id }
model.onDelete(item)
errorAlertItem = nil
isShowingErrorAlert = false
}
} message: { item in
Text(item.playbackError ?? "Unknown error")
}
}
}
}
}
struct MediaItemCell: View
{
let title: String
let subtitle: String
let state: State
let playbackError: String?
var body: some View {
HStack {
Image(systemName: iconName)
.tint(playbackError == nil ? Color.primary : Color.orange)
.frame(width: 15.0)
.padding(.trailing, 10.0)
VStack(alignment: .leading) {
Text(title)
.bold()
.font(.subheadline)
.tint(.primary)
.lineLimit(1)
Text(subtitle)
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(1)
}
Spacer()
}
.padding([.top, .bottom], 4.0)
.frame(minHeight: 44.0)
}
private var iconName: String {
if playbackError != nil {
return "exclamationmark.triangle.fill"
}
switch state {
case .queued: return "play.fill"
case .playing: return "speaker.wave.3.fill"
case .paused: return "speaker.fill"
}
}
// MARK: - Types
enum State {
case queued
case playing
case paused
}
}