Better connection handling, favorites support

This commit is contained in:
2025-05-30 17:06:52 -07:00
parent 8807d6e621
commit 3775f2dc7c
5 changed files with 145 additions and 10 deletions

View File

@@ -63,6 +63,12 @@ struct API
.json() .json()
} }
public func fetchFavorites() async throws -> [MediaItem] {
try await request()
.path("/favorites")
.json()
}
public func play() async throws { public func play() async throws {
try await request() try await request()
.path("/play") .path("/play")
@@ -95,6 +101,13 @@ struct API
.post() .post()
} }
public func addFavorite(mediaURL: String) async throws {
try await request()
.path("/favorites")
.body([ "filename" : mediaURL ])
.post()
}
public func delete(index: Int) async throws { public func delete(index: Int) async throws {
try await request() try await request()
.path("/playlist/\(index)") .path("/playlist/\(index)")

View File

@@ -38,20 +38,32 @@ struct ContentView: View
} }
let addMediaModel = model.addMediaViewModel let addMediaModel = model.addMediaViewModel
addMediaModel.onAdd = { mediaURL in addMediaModel.onAdd = { [model] mediaURL in
let strippedURL = mediaURL.trimmingCharacters(in: .whitespacesAndNewlines) let strippedURL = mediaURL.trimmingCharacters(in: .whitespacesAndNewlines)
if !strippedURL.isEmpty { if !strippedURL.isEmpty {
addMediaModel.fieldContents = "" addMediaModel.fieldContents = ""
Task { try await api.add(mediaURL: strippedURL) } Task {
switch model.selectedTab {
case .playlist:
try await api.add(mediaURL: strippedURL)
case .favorites:
try await api.addFavorite(mediaURL: strippedURL)
}
}
} }
} }
let favoritesModel = model.favoritesModel
favoritesModel.onPlay = { item in
Task { try await api.add(mediaURL: item.filename) }
}
} }
} }
var body: some View { var body: some View {
MainView(model: model) MainView(model: model)
.task { await watchWebsocket() } .task { await watchWebsocket() }
.task { await refresh([.nowPlaying, .playlist]) } .task { await refresh([.nowPlaying, .playlist, .favorites]) }
} }
// MARK: - Types // MARK: - Types
@@ -62,6 +74,7 @@ struct ContentView: View
static let nowPlaying = RefreshType(rawValue: 1 << 0) static let nowPlaying = RefreshType(rawValue: 1 << 0)
static let playlist = RefreshType(rawValue: 1 << 1) static let playlist = RefreshType(rawValue: 1 << 1)
static let favorites = RefreshType(rawValue: 1 << 2)
} }
} }
@@ -99,6 +112,17 @@ extension ContentView
} }
} }
if what.contains(.favorites) {
let favorites = try await api.fetchFavorites()
model.favoritesModel.items = favorites.map { mediaItem in
FavoriteItem(
id: String(mediaItem.id),
title: mediaItem.title ?? mediaItem.filename ?? "<null>",
filename: mediaItem.filename ?? "<null>"
)
}
}
model.connectionError = nil model.connectionError = nil
} catch { } catch {
print("Error refreshing content: \(error)") print("Error refreshing content: \(error)")
@@ -133,19 +157,19 @@ extension ContentView
case .playlistUpdate: case .playlistUpdate:
await refresh(.playlist) await refresh(.playlist)
case .favoritesUpdate:
await refresh(.favorites)
case .metadataUpdate: fallthrough case .metadataUpdate: fallthrough
case .mpdUpdate: case .mpdUpdate:
await refresh([.playlist, .nowPlaying]) await refresh([.playlist, .nowPlaying, .favorites])
case .receivedWebsocketPong: case .receivedWebsocketPong:
// This means we're online. // This means we're online.
if model.connectionError != nil { if model.connectionError != nil {
model.connectionError = nil model.connectionError = nil
await refresh([.playlist, .nowPlaying]) await refresh([.playlist, .nowPlaying, .favorites])
} }
default:
break
} }
} }
} }
@@ -156,12 +180,19 @@ class MainViewModel
var api = API.fromSettings() var api = API.fromSettings()
var connectionError: Error? = nil var connectionError: Error? = nil
var selectedTab: MainTab = .playlist
var playlistModel = PlaylistViewModel() var playlistModel = PlaylistViewModel()
var favoritesModel = FavoritesViewModel()
var nowPlayingViewModel = NowPlayingViewModel() var nowPlayingViewModel = NowPlayingViewModel()
var addMediaViewModel = AddMediaBarViewModel() var addMediaViewModel = AddMediaBarViewModel()
} }
enum MainTab: String, CaseIterable {
case playlist
case favorites
}
struct MainView: View struct MainView: View
{ {
@State var model: MainViewModel @State var model: MainViewModel
@@ -206,8 +237,16 @@ struct MainView: View
Text(.connectionError) Text(.connectionError)
} }
} else { } else {
PlaylistView(model: model.playlistModel) TabView(selection: $model.selectedTab) {
.frame(maxWidth: 640.0) Tab(.playlist, systemImage: "list.bullet", value: .playlist) {
PlaylistView(model: model.playlistModel)
}
Tab(.favorites, systemImage: "heart.fill", value: .favorites) {
FavoritesView(model: model.favoritesModel)
}
}
.frame(maxWidth: 640.0)
} }
} }
.frame(minHeight: 0.0) .frame(minHeight: 0.0)

View File

@@ -51,6 +51,16 @@
} }
} }
}, },
"FAVORITES" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Favorites"
}
}
}
},
"General" : { "General" : {
}, },
@@ -75,6 +85,16 @@
} }
} }
}, },
"PLAYLIST" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Playlist"
}
}
}
},
"SERVER_IS_ONLINE" : { "SERVER_IS_ONLINE" : {
"localizations" : { "localizations" : {
"en" : { "en" : {

View File

@@ -22,4 +22,6 @@ extension LocalizedStringKey
static let validating = LocalizedStringKey("VALIDATING") static let validating = LocalizedStringKey("VALIDATING")
static let general = LocalizedStringKey("GENERAL") static let general = LocalizedStringKey("GENERAL")
static let connectionError = LocalizedStringKey("CONNECTION_ERROR") static let connectionError = LocalizedStringKey("CONNECTION_ERROR")
static let playlist = LocalizedStringKey("PLAYLIST")
static let favorites = LocalizedStringKey("FAVORITES")
} }

View File

@@ -16,6 +16,13 @@ struct PlaylistItem: Identifiable
let isCurrent: Bool let isCurrent: Bool
} }
struct FavoriteItem: Identifiable
{
let id: String
let title: String
let filename: String
}
@Observable @Observable
class PlaylistViewModel class PlaylistViewModel
{ {
@@ -26,6 +33,14 @@ class PlaylistViewModel
var onDelete: (PlaylistItem) -> Void = { _ in } var onDelete: (PlaylistItem) -> Void = { _ in }
} }
@Observable
class FavoritesViewModel
{
var items: [FavoriteItem] = []
var onPlay: (FavoriteItem) -> Void = { _ in }
}
struct PlaylistView: View struct PlaylistView: View
{ {
var model: PlaylistViewModel var model: PlaylistViewModel
@@ -44,6 +59,21 @@ struct PlaylistView: View
} }
} }
struct FavoritesView: View
{
var model: FavoritesViewModel
var body: some View {
List(model.items) { item in
FavoriteItemCell(
title: item.title,
subtitle: item.filename,
onPlayButtonClick: { model.onPlay(item) }
)
}
}
}
struct PlaylistItemCell: View struct PlaylistItemCell: View
{ {
let title: String let title: String
@@ -97,3 +127,34 @@ struct PlaylistItemCell: View
case paused case paused
} }
} }
struct FavoriteItemCell: View
{
let title: String
let subtitle: String
let onPlayButtonClick: () -> Void
var body: some View {
HStack {
Button(action: onPlayButtonClick) {
Image(systemName: "play.fill")
}
.buttonStyle(BorderlessButtonStyle())
.tint(Color.primary)
.frame(width: 15.0)
VStack(alignment: .leading) {
Text(title)
.font(.body.bold())
.lineLimit(1)
Text(subtitle)
.foregroundColor(.secondary)
.lineLimit(1)
}
Spacer()
}
.padding([.top, .bottom], 8.0)
}
}