Changes
3 changed files (+104/-35)
-
-
@@ -24,6 +24,17 @@ case loaded(BrowseService.List, [BrowseService.Item])case failed(any Error) } extension BrowsePage { func isLoading() -> Bool { switch self { case .loading: true default: false } } } @MainActor @Observable final class BrowsingDataModel {
-
@@ -208,7 +219,7 @@ }} } private func loadPage(item: BrowseService.Item) { func loadPage(item: BrowseService.Item) { guard let hierarchy = hierarchy else { return }
-
@@ -217,14 +228,10 @@ if let loading = self.loading {loading.cancel() } guard let item = self.stack.last else { logger.warning("Attempt to load page ") return } self.loading = Task { pages[item] = .loading let list: BrowseService.List do { logger.debug( "Moving browse cursor to \(item.title)/\(item.itemKey ?? "<nil>")/\(self.stack.count)"
-
@@ -249,22 +256,39 @@ )throw LoadError.nonListResponse } guard let list = browse.list else { guard let responseList = browse.list else { logger.error( "Roon server returned list action, but list property is empty" ) throw LoadError.nonListResponse } // Some items do automatically pop server stack. Clients have to guess that by // inspecting list's level and comparing that to the locally maintained stack. if list.level < _stack.count - 1 { logger.debug("Detected server pop, shrinking local stack") _stack = Array(_stack[0..<Int(list.level)]) pages[item] = nil list = responseList } catch { logger.error("Unable to browse page: \(error)") pages[item] = .failed(error) return } var itemToLoad: BrowseService.Item = item // Some items do automatically pop server stack. Clients have to guess that by // inspecting list's level and comparing that to the locally maintained stack. if list.level < _stack.count { _stack = Array(_stack[0..<Int(list.level)]) pages[item] = nil guard let lastItem = _stack.last else { logger.debug("Server-side pop to root page") return } itemToLoad = lastItem } let item = itemToLoad do { logger.debug( "Loading page at \(item.title)/\(item.itemKey ?? "<nil>")/\(self.stack.count)" )
-
-
-
@@ -52,27 +52,35 @@ }} case .some(.loaded(let list, let items)): List(items, id: \.itemKey) { item in NavigationLink(value: item) { HStack { if let imageKey = item.imageKey { Artwork(imageKey: imageKey, width: 64, height: 64) .frame(width: 32, height: 32) } VStack(alignment: .leading) { Text(item.title) .font(.headline) .lineLimit(1) switch item.hint { case .list, .actionList: NavigationLink(value: item) { Row(item) } case .action: Button { model.loadPage(item: item) } label: { Row(item) .frame(maxWidth: .infinity, alignment: .leading) if let subtitle = item.subtitle { Text(subtitle) .font(.subheadline) .lineLimit(1) } if model.pages[item]?.isLoading() ?? false { ProgressView() // Row height in macOS is way shorter than other platforms. #if os(macOS) .controlSize(.small) #endif } .frame(maxWidth: .infinity, alignment: .leading) } .padding(.vertical, 2) .buttonStyle(.borderless) // ".borderless" on macOS somehow renders in gray foreground color, // which looks like the button is disabled. #if os(macOS) .foregroundStyle(.foreground) #endif .disabled(model.pages[item]?.isLoading() ?? false) default: Row(item) } } .navigationTitle(list.title)
-
@@ -95,3 +103,34 @@ self.model = modelself.item = item } } private struct Row: View { private let item: BrowseService.Item fileprivate init(_ item: BrowseService.Item) { self.item = item } var body: some View { HStack { if let imageKey = item.imageKey { Artwork(imageKey: imageKey, width: 64, height: 64) .frame(width: 32, height: 32) } VStack(alignment: .leading) { Text(item.title) .font(.headline) .lineLimit(1) if let subtitle = item.subtitle { Text(subtitle) .font(.subheadline) .lineLimit(1) } } .frame(maxWidth: .infinity, alignment: .leading) } .padding(.vertical, 2) } }
-
-
-
@@ -168,7 +168,8 @@ {"action": "list", "list": { "title": "Mock Page", "subtitle": "Various responses" "subtitle": "Various responses", "level": 0 } } """,
-
@@ -221,7 +222,8 @@ browse: """{ "action": "list", "list": { "title": "List" "title": "List", "level": 1 } } """,
-
@@ -303,9 +305,13 @@ case "mock-slow-list":try await Task.sleep(for: .milliseconds(500)) stack.append(list) case "mock-action": break try await Task.sleep(for: .milliseconds(300)) print("Got action: \(itemKey)") stack = [] case "mock-list-action": break try await Task.sleep(for: .milliseconds(300)) print("Got action: \(itemKey)") stack.removeLast() default: return Moo(verb: "COMPLETE", service: "UnknownItemKey") }
-