Implements a few more api endpoints
This commit is contained in:
@@ -66,6 +66,40 @@ struct API
|
||||
.post()
|
||||
}
|
||||
|
||||
public func skip(_ to: Int? = nil) async throws {
|
||||
let path = if let to { "/skip/\(to)" } else { "/skip" }
|
||||
try await request()
|
||||
.path(path)
|
||||
.post()
|
||||
}
|
||||
|
||||
public func previous() async throws {
|
||||
try await request()
|
||||
.path("/previous")
|
||||
.post()
|
||||
}
|
||||
|
||||
public func add(mediaURL: String) async throws {
|
||||
try await request()
|
||||
.path("/playlist")
|
||||
.body([ "url" : mediaURL ])
|
||||
.post()
|
||||
}
|
||||
|
||||
public func delete(index: Int) async throws {
|
||||
try await request()
|
||||
.path("/playlist/\(index)")
|
||||
.method(.delete)
|
||||
.execute()
|
||||
}
|
||||
|
||||
public func setVolume(_ value: Double) async throws {
|
||||
try await request()
|
||||
.path("/volume")
|
||||
.body([ "volume" : Int(value * 100) ])
|
||||
.post()
|
||||
}
|
||||
|
||||
public func events() async throws -> AsyncStream<Event> {
|
||||
return AsyncStream { continuation in
|
||||
let url = request()
|
||||
|
||||
@@ -12,8 +12,8 @@ class AddMediaBarViewModel
|
||||
{
|
||||
var fieldContents: String = ""
|
||||
|
||||
let onAdd: (String) -> Void = { _ in }
|
||||
let onSearch: () -> Void = {}
|
||||
var onAdd: (String) -> Void = { _ in }
|
||||
var onSearch: () -> Void = {}
|
||||
}
|
||||
|
||||
struct AddMediaBarView: View
|
||||
|
||||
@@ -15,9 +15,37 @@ struct ContentView: View
|
||||
self.model = MainViewModel()
|
||||
|
||||
let api = self.model.api
|
||||
self.model.nowPlayingViewModel.onPlayPause = { model in
|
||||
|
||||
let nowPlayingModel = self.model.nowPlayingViewModel
|
||||
nowPlayingModel.onPlayPause = { model in
|
||||
Task { model.isPlaying ? try await api.pause() : try await api.play() }
|
||||
}
|
||||
nowPlayingModel.onNext = { _ in
|
||||
Task { try await api.skip() }
|
||||
}
|
||||
nowPlayingModel.onPrev = { _ in
|
||||
Task { try await api.previous() }
|
||||
}
|
||||
nowPlayingModel.onVolumeChange = { model in
|
||||
Task { try await api.setVolume(model.volume) }
|
||||
}
|
||||
|
||||
let playlistModel = model.playlistModel
|
||||
playlistModel.onSeek = { item in
|
||||
Task { try await api.skip(item.index) }
|
||||
}
|
||||
playlistModel.onDelete = { item in
|
||||
Task { try await api.delete(index: item.index) }
|
||||
}
|
||||
|
||||
let addMediaModel = model.addMediaViewModel
|
||||
addMediaModel.onAdd = { mediaURL in
|
||||
let strippedURL = mediaURL.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !strippedURL.isEmpty {
|
||||
addMediaModel.fieldContents = ""
|
||||
Task { try await api.add(mediaURL: strippedURL) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -46,13 +74,15 @@ extension ContentView
|
||||
model.nowPlayingViewModel.title = nowPlaying.playingItem?.title ?? "??"
|
||||
model.nowPlayingViewModel.subtitle = nowPlaying.playingItem?.filename ?? "??"
|
||||
model.nowPlayingViewModel.isPlaying = !nowPlaying.isPaused
|
||||
model.nowPlayingViewModel.volume = Double(nowPlaying.volume) / 100.0
|
||||
model.playlistModel.isPlaying = !nowPlaying.isPaused
|
||||
}
|
||||
|
||||
if what.contains(.playlist) {
|
||||
let playlist = try await model.api.fetchPlaylist()
|
||||
model.playlistModel.items = playlist.map { mediaItem in
|
||||
model.playlistModel.items = playlist.enumerated().map { (idx, mediaItem) in
|
||||
PlaylistItem(
|
||||
index: idx,
|
||||
id: String(mediaItem.id),
|
||||
title: mediaItem.title ?? mediaItem.filename,
|
||||
filename: mediaItem.filename,
|
||||
@@ -68,6 +98,7 @@ extension ContentView
|
||||
private func watchWebsocket() async {
|
||||
do {
|
||||
for await event in try await model.api.events() {
|
||||
print("Got event: \(event.type)")
|
||||
await handle(event: event)
|
||||
}
|
||||
} catch {
|
||||
@@ -81,6 +112,9 @@ extension ContentView
|
||||
case .nowPlayingUpdate:
|
||||
await refresh(.nowPlaying)
|
||||
|
||||
case .playlistUpdate:
|
||||
await refresh(.playlist)
|
||||
|
||||
case .metadataUpdate: fallthrough
|
||||
case .mpdUpdate:
|
||||
await refresh([.playlist, .nowPlaying])
|
||||
@@ -109,11 +143,13 @@ struct MainView: View
|
||||
VStack {
|
||||
VStack {
|
||||
NowPlayingView(model: model.nowPlayingViewModel)
|
||||
.padding()
|
||||
|
||||
PlaylistView(model: model.playlistModel)
|
||||
.frame(maxWidth: 640.0)
|
||||
}
|
||||
.frame(minHeight: 0.0)
|
||||
.layoutPriority(1.0)
|
||||
.padding()
|
||||
|
||||
AddMediaBarView(model: model.addMediaViewModel)
|
||||
.layoutPriority(2.0)
|
||||
|
||||
@@ -13,6 +13,7 @@ class NowPlayingViewModel
|
||||
var onPlayPause: (NowPlayingViewModel) -> Void = { _ in }
|
||||
var onNext: (NowPlayingViewModel) -> Void = { _ in }
|
||||
var onPrev: (NowPlayingViewModel) -> Void = { _ in }
|
||||
var onVolumeChange: (NowPlayingViewModel) -> Void = { _ in }
|
||||
|
||||
var isPlaying: Bool = false
|
||||
var title: String = "Loading…"
|
||||
@@ -56,8 +57,11 @@ struct NowPlayingView: View
|
||||
let playPauseImageName = model.isPlaying ? "pause.fill" : "play.fill"
|
||||
|
||||
HStack {
|
||||
Slider(value: $model.volume, in: 0.0...1.0)
|
||||
.frame(maxWidth: 100.0)
|
||||
Slider(
|
||||
value: $model.volume,
|
||||
in: 0.0...1.0,
|
||||
onEditingChanged: { _ in model.onVolumeChange(model) }
|
||||
).frame(maxWidth: 100.0)
|
||||
|
||||
Button(action: { model.onPrev(model) } ) { Image(systemName: "arrow.left.to.line.compact") }
|
||||
Button(action: { model.onPlayPause(model) }) { Image(systemName: playPauseImageName) }
|
||||
|
||||
@@ -9,6 +9,7 @@ import SwiftUI
|
||||
|
||||
struct PlaylistItem: Identifiable
|
||||
{
|
||||
let index: Int
|
||||
let id: String
|
||||
let title: String
|
||||
let filename: String
|
||||
@@ -20,6 +21,9 @@ class PlaylistViewModel
|
||||
{
|
||||
var isPlaying: Bool = false
|
||||
var items: [PlaylistItem] = []
|
||||
|
||||
var onSeek: (PlaylistItem) -> Void = { _ in }
|
||||
var onDelete: (PlaylistItem) -> Void = { _ in }
|
||||
}
|
||||
|
||||
struct PlaylistView: View
|
||||
@@ -32,7 +36,9 @@ struct PlaylistView: View
|
||||
title: item.title,
|
||||
subtitle: item.filename,
|
||||
state: item.isCurrent ? (model.isPlaying ? PlaylistItemCell.State.playing : PlaylistItemCell.State.paused)
|
||||
: .queued
|
||||
: .queued,
|
||||
onLeadingIconClick: { model.onSeek(item) },
|
||||
onDeleteButtonClick: { model.onDelete(item) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -44,6 +50,9 @@ struct PlaylistItemCell: View
|
||||
let subtitle: String
|
||||
let state: State
|
||||
|
||||
let onLeadingIconClick: () -> Void
|
||||
let onDeleteButtonClick: () -> Void
|
||||
|
||||
var body: some View {
|
||||
let icon: String = switch state {
|
||||
case .queued: "play.fill"
|
||||
@@ -52,7 +61,7 @@ struct PlaylistItemCell: View
|
||||
}
|
||||
|
||||
HStack {
|
||||
Button(action: {}) { Image(systemName: icon) }
|
||||
Button(action: onLeadingIconClick) { Image(systemName: icon) }
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
.tint(Color.primary)
|
||||
.frame(width: 15.0)
|
||||
@@ -70,7 +79,7 @@ struct PlaylistItemCell: View
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
Button(action: {}) {
|
||||
Button(action: onDeleteButtonClick) {
|
||||
Image(systemName: "xmark")
|
||||
.tint(.red)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ struct RequestBuilder
|
||||
{
|
||||
let url: URL
|
||||
private var httpMethod: HTTPMethod = .get
|
||||
private var body: Data? = nil
|
||||
|
||||
init(url: URL) {
|
||||
self.url = url
|
||||
@@ -26,9 +27,20 @@ struct RequestBuilder
|
||||
return RequestBuilder(url: self.url.appending(path: path))
|
||||
}
|
||||
|
||||
public func body(_ data: Codable) -> Self {
|
||||
var copy = self
|
||||
copy.body = try! JSONEncoder().encode(data)
|
||||
return copy
|
||||
}
|
||||
|
||||
public func build() -> URLRequest {
|
||||
var request = URLRequest(url: self.url)
|
||||
request.httpMethod = self.httpMethod.rawValue
|
||||
if let body {
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = body
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
@@ -39,8 +51,17 @@ struct RequestBuilder
|
||||
}
|
||||
|
||||
public func post() async throws {
|
||||
let urlRequest = self.method(.post).build()
|
||||
(_, _) = try await URLSession.shared.data(for: urlRequest)
|
||||
try await self.method(.post).execute()
|
||||
}
|
||||
|
||||
public func execute() async throws {
|
||||
let urlRequest = self.build()
|
||||
let (data, response) = try await URLSession.shared.data(for: urlRequest)
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
if httpResponse.statusCode != 200 {
|
||||
print("POST error \(httpResponse.statusCode): \(String(data: data, encoding: .utf8)!)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func websocket() -> URL {
|
||||
@@ -52,5 +73,6 @@ struct RequestBuilder
|
||||
enum HTTPMethod: String {
|
||||
case get = "GET"
|
||||
case post = "POST"
|
||||
case delete = "DELETE"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user