Better connection handling, favorites support
This commit is contained in:
@@ -63,6 +63,12 @@ struct API
|
||||
.json()
|
||||
}
|
||||
|
||||
public func fetchFavorites() async throws -> [MediaItem] {
|
||||
try await request()
|
||||
.path("/favorites")
|
||||
.json()
|
||||
}
|
||||
|
||||
public func play() async throws {
|
||||
try await request()
|
||||
.path("/play")
|
||||
@@ -95,6 +101,13 @@ struct API
|
||||
.post()
|
||||
}
|
||||
|
||||
public func addFavorite(mediaURL: String) async throws {
|
||||
try await request()
|
||||
.path("/favorites")
|
||||
.body([ "filename" : mediaURL ])
|
||||
.post()
|
||||
}
|
||||
|
||||
public func delete(index: Int) async throws {
|
||||
try await request()
|
||||
.path("/playlist/\(index)")
|
||||
|
||||
@@ -38,20 +38,32 @@ struct ContentView: View
|
||||
}
|
||||
|
||||
let addMediaModel = model.addMediaViewModel
|
||||
addMediaModel.onAdd = { mediaURL in
|
||||
addMediaModel.onAdd = { [model] mediaURL in
|
||||
let strippedURL = mediaURL.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !strippedURL.isEmpty {
|
||||
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 {
|
||||
MainView(model: model)
|
||||
.task { await watchWebsocket() }
|
||||
.task { await refresh([.nowPlaying, .playlist]) }
|
||||
.task { await refresh([.nowPlaying, .playlist, .favorites]) }
|
||||
}
|
||||
|
||||
// MARK: - Types
|
||||
@@ -62,6 +74,7 @@ struct ContentView: View
|
||||
|
||||
static let nowPlaying = RefreshType(rawValue: 1 << 0)
|
||||
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
|
||||
} catch {
|
||||
print("Error refreshing content: \(error)")
|
||||
@@ -133,19 +157,19 @@ extension ContentView
|
||||
case .playlistUpdate:
|
||||
await refresh(.playlist)
|
||||
|
||||
case .favoritesUpdate:
|
||||
await refresh(.favorites)
|
||||
|
||||
case .metadataUpdate: fallthrough
|
||||
case .mpdUpdate:
|
||||
await refresh([.playlist, .nowPlaying])
|
||||
await refresh([.playlist, .nowPlaying, .favorites])
|
||||
|
||||
case .receivedWebsocketPong:
|
||||
// This means we're online.
|
||||
if 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 connectionError: Error? = nil
|
||||
var selectedTab: MainTab = .playlist
|
||||
|
||||
var playlistModel = PlaylistViewModel()
|
||||
var favoritesModel = FavoritesViewModel()
|
||||
var nowPlayingViewModel = NowPlayingViewModel()
|
||||
var addMediaViewModel = AddMediaBarViewModel()
|
||||
}
|
||||
|
||||
enum MainTab: String, CaseIterable {
|
||||
case playlist
|
||||
case favorites
|
||||
}
|
||||
|
||||
struct MainView: View
|
||||
{
|
||||
@State var model: MainViewModel
|
||||
@@ -206,7 +237,15 @@ struct MainView: View
|
||||
Text(.connectionError)
|
||||
}
|
||||
} else {
|
||||
TabView(selection: $model.selectedTab) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"FAVORITES" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Favorites"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"General" : {
|
||||
|
||||
},
|
||||
@@ -75,6 +85,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"PLAYLIST" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Playlist"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"SERVER_IS_ONLINE" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
|
||||
@@ -22,4 +22,6 @@ extension LocalizedStringKey
|
||||
static let validating = LocalizedStringKey("VALIDATING")
|
||||
static let general = LocalizedStringKey("GENERAL")
|
||||
static let connectionError = LocalizedStringKey("CONNECTION_ERROR")
|
||||
static let playlist = LocalizedStringKey("PLAYLIST")
|
||||
static let favorites = LocalizedStringKey("FAVORITES")
|
||||
}
|
||||
|
||||
@@ -16,6 +16,13 @@ struct PlaylistItem: Identifiable
|
||||
let isCurrent: Bool
|
||||
}
|
||||
|
||||
struct FavoriteItem: Identifiable
|
||||
{
|
||||
let id: String
|
||||
let title: String
|
||||
let filename: String
|
||||
}
|
||||
|
||||
@Observable
|
||||
class PlaylistViewModel
|
||||
{
|
||||
@@ -26,6 +33,14 @@ class PlaylistViewModel
|
||||
var onDelete: (PlaylistItem) -> Void = { _ in }
|
||||
}
|
||||
|
||||
@Observable
|
||||
class FavoritesViewModel
|
||||
{
|
||||
var items: [FavoriteItem] = []
|
||||
|
||||
var onPlay: (FavoriteItem) -> Void = { _ in }
|
||||
}
|
||||
|
||||
struct PlaylistView: View
|
||||
{
|
||||
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
|
||||
{
|
||||
let title: String
|
||||
@@ -97,3 +127,34 @@ struct PlaylistItemCell: View
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user