ios: add error handling

This commit is contained in:
2025-11-15 18:16:03 -08:00
parent bc54735d1f
commit cfc6e6c411
6 changed files with 104 additions and 25 deletions

View File

@@ -16,6 +16,7 @@ struct MediaItem: Codable
let current: Bool? let current: Bool?
let playing: Bool? let playing: Bool?
let metadata: Metadata? let metadata: Metadata?
let playbackError: String?
var displayTitle: String { var displayTitle: String {
metadata?.title ?? title ?? displayFilename ?? "item \(id)" metadata?.title ?? title ?? displayFilename ?? "item \(id)"
@@ -280,6 +281,7 @@ actor API
case favoritesUpdate = "favorites_update" case favoritesUpdate = "favorites_update"
case metadataUpdate = "metadata_update" case metadataUpdate = "metadata_update"
case mpdUpdate = "mpd_update" case mpdUpdate = "mpd_update"
case playbackError = "playback_error"
// Private UI events // Private UI events
case receivedWebsocketPong case receivedWebsocketPong

View File

@@ -104,6 +104,16 @@
} }
} }
}, },
"DELETE" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Delete"
}
}
}
},
"DISCOVERED" : { "DISCOVERED" : {
"localizations" : { "localizations" : {
"en" : { "en" : {
@@ -247,6 +257,16 @@
}, },
"Nothing here yet." : { "Nothing here yet." : {
},
"PLAYBACK_ERROR" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Playback Error"
}
}
}
}, },
"PLAYLIST" : { "PLAYLIST" : {
"localizations" : { "localizations" : {
@@ -360,6 +380,16 @@
} }
} }
}, },
"Unknown error" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Unknown Error"
}
}
}
},
"URL" : { "URL" : {
"localizations" : { "localizations" : {
"en" : { "en" : {

View File

@@ -46,4 +46,6 @@ extension LocalizedStringKey
static let notPlaying = LocalizedStringKey("NOT_PLAYING") static let notPlaying = LocalizedStringKey("NOT_PLAYING")
static let url = LocalizedStringKey("URL") static let url = LocalizedStringKey("URL")
static let title = LocalizedStringKey("TITLE") static let title = LocalizedStringKey("TITLE")
static let playbackError = LocalizedStringKey("PLAYBACK_ERROR")
static let delete = LocalizedStringKey("DELETE")
} }

View File

@@ -29,6 +29,8 @@ struct AutofocusingTextField: UIViewRepresentable
tf.delegate = context.coordinator tf.delegate = context.coordinator
tf.returnKeyType = .done tf.returnKeyType = .done
tf.setContentHuggingPriority(.defaultHigh, for: .vertical) tf.setContentHuggingPriority(.defaultHigh, for: .vertical)
tf.autocorrectionType = .no
tf.autocapitalizationType = .none
return tf return tf
} }

View File

@@ -87,7 +87,8 @@ extension ContentView
title: mediaItem.displayTitle, title: mediaItem.displayTitle,
filename: mediaItem.filename ?? "<null>", filename: mediaItem.filename ?? "<null>",
index: idx, index: idx,
isCurrent: mediaItem.current ?? false isCurrent: mediaItem.current ?? false,
playbackError: mediaItem.playbackError
) )
} }
} }
@@ -100,7 +101,8 @@ extension ContentView
id: String(mediaItem.id), id: String(mediaItem.id),
title: mediaItem.displayTitle, title: mediaItem.displayTitle,
filename: mediaItem.filename ?? "<null>", filename: mediaItem.filename ?? "<null>",
isCurrent: nowPlaying.playingItem?.filename == mediaItem.filename isCurrent: nowPlaying.playingItem?.filename == mediaItem.filename,
playbackError: mediaItem.playbackError
) )
} }
} }
@@ -160,7 +162,8 @@ extension ContentView
case .websocketReconnected: fallthrough case .websocketReconnected: fallthrough
case .metadataUpdate: fallthrough case .metadataUpdate: fallthrough
case .mpdUpdate: case .mpdUpdate: fallthrough
case .playbackError:
await refresh([.playlist, .nowPlaying, .favorites]) await refresh([.playlist, .nowPlaying, .favorites])
case .receivedWebsocketPong: case .receivedWebsocketPong:

View File

@@ -14,17 +14,19 @@ struct MediaListItem: Identifiable
let filename: String let filename: String
let index: Int? let index: Int?
let isCurrent: Bool let isCurrent: Bool
let playbackError: String?
var id: String { var id: String {
_id + filename // temporary: we get duplicate ids from the server sometimes... _id + filename // temporary: we get duplicate ids from the server sometimes...
} }
init(id: String, title: String, filename: String, index: Int? = nil, isCurrent: Bool = false) { init(id: String, title: String, filename: String, index: Int? = nil, isCurrent: Bool = false, playbackError: String? = nil) {
self._id = id self._id = id
self.title = title self.title = title
self.filename = filename self.filename = filename
self.index = index self.index = index
self.isCurrent = isCurrent self.isCurrent = isCurrent
self.playbackError = playbackError
} }
} }
@@ -45,6 +47,7 @@ class MediaListViewModel
var onQueue: (MediaListItem) -> Void = { _ in } var onQueue: (MediaListItem) -> Void = { _ in }
var onEdit: (MediaListItem) -> Void = { _ in } var onEdit: (MediaListItem) -> Void = { _ in }
var onFavorite: (MediaListItem) -> Void = { _ in } var onFavorite: (MediaListItem) -> Void = { _ in }
var onDelete: (MediaListItem) -> Void = { _ in }
init(mode: MediaListMode) { init(mode: MediaListMode) {
self.mode = mode self.mode = mode
@@ -54,6 +57,7 @@ class MediaListViewModel
struct MediaListView: View struct MediaListView: View
{ {
@Binding var model: MediaListViewModel @Binding var model: MediaListViewModel
@State private var errorAlertItem: MediaListItem? = nil
var body: some View { var body: some View {
VStack { VStack {
@@ -70,20 +74,28 @@ struct MediaListView: View
let state = item.isCurrent ? (model.isPlaying ? MediaItemCell.State.playing : MediaItemCell.State.paused) : .queued let state = item.isCurrent ? (model.isPlaying ? MediaItemCell.State.playing : MediaItemCell.State.paused) : .queued
Button { Button {
switch model.mode { if let _ = item.playbackError {
case .playlist: errorAlertItem = item
model.onSeek(item) } else {
case .favorites: switch model.mode {
model.onPlay(item) case .playlist:
model.onSeek(item)
case .favorites:
model.onPlay(item)
}
} }
} label: { } label: {
MediaItemCell( MediaItemCell(
title: item.title, title: item.title,
subtitle: item.filename, subtitle: item.filename,
state: state state: state,
playbackError: item.playbackError
) )
} }
.listRowBackground((model.mode == .playlist && state != .queued) ? Color.accentColor.opacity(0.10) : nil) .listRowBackground(
item.playbackError != nil ? Color.red.opacity(0.15) :
(model.mode == .playlist && state != .queued ? Color.accentColor.opacity(0.10) : nil)
)
.contextMenu { .contextMenu {
Button(.copyTitle) { Button(.copyTitle) {
UIPasteboard.general.string = item.title UIPasteboard.general.string = item.title
@@ -123,6 +135,28 @@ struct MediaListView: View
} }
} }
} }
.alert(.playbackError, isPresented: Binding(get: {
errorAlertItem != nil
}, set: { newValue in
if !newValue { errorAlertItem = nil }
}), actions: {
Button(.cancel, role: .cancel) {
errorAlertItem = nil
}
if let item = errorAlertItem {
Button(.delete, role: .destructive) {
model.items.removeAll { $0.id == item.id }
model.onDelete(item)
errorAlertItem = nil
}
}
}, message: {
if let message = errorAlertItem?.playbackError {
Text(message)
} else {
Text("Unknown error")
}
})
} }
} }
} }
@@ -134,17 +168,12 @@ struct MediaItemCell: View
let title: String let title: String
let subtitle: String let subtitle: String
let state: State let state: State
let playbackError: String?
var body: some View { var body: some View {
let icon: String = switch state {
case .queued: "play.fill"
case .playing: "speaker.wave.3.fill"
case .paused: "speaker.fill"
}
HStack { HStack {
Image(systemName: icon) Image(systemName: iconName)
.tint(Color.primary) .tint(playbackError == nil ? Color.primary : Color.orange)
.frame(width: 15.0) .frame(width: 15.0)
.padding(.trailing, 10.0) .padding(.trailing, 10.0)
@@ -166,6 +195,18 @@ struct MediaItemCell: View
.padding([.top, .bottom], 4.0) .padding([.top, .bottom], 4.0)
} }
private var iconName: String {
if let playbackError {
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 // MARK: - Types
enum State { enum State {
@@ -174,4 +215,3 @@ struct MediaItemCell: View
case paused case paused
} }
} }