Better connection handling, favorites support
This commit is contained in:
@@ -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)")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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" : {
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user