Changes
6 changed files (+194/-56)
-
-
@@ -100,6 +100,22 @@ case itemKey = "item_key"case hint = "hint" case inputPrompt = "input_prompt" } public init( title: String, subtitle: String? = nil, imageKey: ImageService.Key? = nil, itemKey: String? = nil, hint: ItemHint? = nil, inputPrompt: InputPrompt? = nil ) { self.title = title self.subtitle = subtitle self.imageKey = imageKey self.itemKey = itemKey self.hint = hint self.inputPrompt = inputPrompt } } public enum ListHint: LosslessStringConvertible, Codable, Sendable {
-
-
-
@@ -18,9 +18,62 @@ import OSLogimport RoonKit import SwiftUI struct BrowseItemGroup: Identifiable, Hashable { let title: String? let items: [BrowseService.Item] var id: Int { self.hashValue } init(title: String? = nil, items: [BrowseService.Item]) { self.title = title self.items = items } static func groupInto(items: [BrowseService.Item]) -> [BrowseItemGroup] { var groups: [BrowseItemGroup] = [] var currentTitle: String? var currentItems: [BrowseService.Item] = [] for item in items { if let last = currentItems.last { let isProbablyDifferentItemKind = ((last.imageKey == nil) != (item.imageKey == nil)) || ((last.subtitle == nil) != (item.subtitle == nil)) if isProbablyDifferentItemKind { groups.append(.init(title: currentTitle, items: currentItems)) currentTitle = nil currentItems = [] } } switch item.hint { case .header: if currentTitle != nil || currentItems.count > 0 { groups.append(.init(title: currentTitle, items: currentItems)) } currentItems = [] currentTitle = item.title default: currentItems.append(item) } } if currentTitle != nil || currentItems.count > 0 { groups.append(.init(title: currentTitle, items: currentItems)) } return groups } } enum BrowsePage { case loading case loaded(BrowseService.List, [BrowseService.Item]) case loaded(BrowseService.List, [BrowseItemGroup]) case failed(any Error) }
-
@@ -136,7 +189,10 @@ )logger.debug("Got root page for \(hierarchy.rawValue)") rootPage = .loaded(load.list, load.items) rootPage = .loaded( load.list, BrowseItemGroup.groupInto(items: load.items) ) } catch { logger.error( "Unable to load root page of \(hierarchy.rawValue): \(error)"
-
@@ -201,9 +257,15 @@ )) if let item = item { pages[item] = .loaded(load.list, load.items) pages[item] = .loaded( load.list, BrowseItemGroup.groupInto(items: load.items) ) } else { rootPage = .loaded(load.list, load.items) rootPage = .loaded( load.list, BrowseItemGroup.groupInto(items: load.items) ) } } catch { logger.error("Unable to load topmost page after pop: \(error)")
-
@@ -307,7 +369,10 @@ )logger.debug("Got page for \(item.title)") pages[item] = .loaded(load.list, load.items) pages[item] = .loaded( load.list, BrowseItemGroup.groupInto(items: load.items) ) } catch { logger.error("Unable to load page: \(error)") pages[item] = .failed(error)
-
-
-
@@ -50,37 +50,49 @@ Button("Retry") {model.reload() } } case .some(.loaded(let list, let items)): List(items, id: \.itemKey) { item in 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) case .some(.loaded(let list, let groups)): List { ForEach(groups) { group in Section { ForEach(group.items, id: \.itemKey) { item in 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 model.pages[item]?.isLoading() ?? false { ProgressView() // Row height in macOS is way shorter than other platforms. if model.pages[item]?.isLoading() ?? false { ProgressView() // Row height in macOS is way shorter than other platforms. #if os(macOS) .controlSize(.small) #endif } } .buttonStyle(.borderless) // ".borderless" on macOS somehow renders in gray foreground color, // which looks like the button is disabled. #if os(macOS) .controlSize(.small) .foregroundStyle(.foreground) #endif .disabled(model.pages[item]?.isLoading() ?? false) default: Row(item) } } } header: { if let title = group.title { Text(title) } else { EmptyView() } } .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)
-
-
-
@@ -242,21 +242,25 @@ "hint": "action"}, { "title": "Item 1", "subtitle": "This is 1st item", "item_key": "mock-list-item1", "hint": "action_list" }, { "title": "Item 2", "subtitle": "This is 2nd item", "item_key": "mock-list-item2", "hint": "action_list" }, { "title": "Item 3", "subtitle": "This is 3rd item", "item_key": "mock-list-item3", "hint": "action_list" }, { "title": "Item 4", "subtitle": "This is 4th item", "item_key": "mock-list-item4", "hint": "action_list" }
-
-
-
@@ -0,0 +1,66 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 import RoonKit import Testing @testable import plac struct BrowsingDataModelTests { @Test func groupItemsByPresenseOfImageKey() async throws { let items: [BrowseService.Item] = [ .init(title: "Foo"), .init(title: "Bar", imageKey: "bar"), .init(title: "Baz", imageKey: "baz"), ] let groups = BrowseItemGroup.groupInto(items: items) #expect( groups == [ .init(items: [ .init(title: "Foo") ]), .init(items: [ .init(title: "Bar", imageKey: "bar"), .init(title: "Baz", imageKey: "baz"), ]), ] ) } @Test func groupItemsByPresenceOfSubtitle() async throws { let items: [BrowseService.Item] = [ .init(title: "Foo", subtitle: "foo-foo"), .init(title: "Bar"), .init(title: "Baz"), ] let groups = BrowseItemGroup.groupInto(items: items) #expect( groups == [ .init(items: [ .init(title: "Foo", subtitle: "foo-foo") ]), .init(items: [ .init(title: "Bar"), .init(title: "Baz"), ]), ] ) } }
-
-
macos/placTests/placTests.swift (deleted)
-
@@ -1,25 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 import Testing struct placTests { @Test func example() async throws { // Write your test here and use APIs like `#expect(...)` to check expected conditions. } }
-