Add connected device candidate details
This commit is contained in:
parent
bd177832c0
commit
ba6edf6cc4
@ -115,7 +115,7 @@ nonisolated struct MinecraftSource: Identifiable, Hashable, Sendable {
|
||||
}
|
||||
|
||||
switch selection {
|
||||
case .sourceCandidate:
|
||||
case .sourceCandidate, .connectedDevice:
|
||||
return []
|
||||
case .source(let sourceID), .allContent(let sourceID):
|
||||
guard sourceID == id else {
|
||||
|
||||
@ -101,6 +101,22 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePer
|
||||
visibleSources
|
||||
}
|
||||
|
||||
var sidebarConnectedDevices: [ConnectedDeviceSidebarEntry] {
|
||||
connectedDevices.filter { entry in
|
||||
guard entry.matchedSourceID == nil else {
|
||||
return false
|
||||
}
|
||||
|
||||
return !sources.contains { source in
|
||||
guard case .connectedDevice(let device, _) = source.origin else {
|
||||
return false
|
||||
}
|
||||
|
||||
return device.udid == entry.device.udid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sourceID(forItemID itemID: URL) -> URL? {
|
||||
sourceIDByItemID[itemID]
|
||||
}
|
||||
|
||||
@ -0,0 +1,133 @@
|
||||
// SPDX-FileCopyrightText: 2026 John Burwell and contributors
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ConnectedDeviceDetailView: View {
|
||||
let entry: ConnectedDeviceSidebarEntry
|
||||
let addAction: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 24) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(entry.device.name)
|
||||
.font(.largeTitle.weight(.semibold))
|
||||
|
||||
Text("Available connected device")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
if let addAction {
|
||||
Button("Add Source") {
|
||||
addAction()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
|
||||
sourceSection(title: "Overview", rows: overviewRows)
|
||||
sourceSection(title: "Minecraft Access", rows: minecraftRows)
|
||||
sourceSection(title: "Technical Details", rows: technicalRows)
|
||||
}
|
||||
.frame(maxWidth: 760, alignment: .leading)
|
||||
.padding(28)
|
||||
}
|
||||
}
|
||||
|
||||
private var overviewRows: [(String, String)] {
|
||||
var rows: [(String, String)] = [
|
||||
("Connection", connectionLabel),
|
||||
("Trust State", trustStateLabel),
|
||||
("Availability", entry.hasMinecraftContainer ? "Ready to add" : "Not ready")
|
||||
]
|
||||
|
||||
if let productType = entry.device.productType, !productType.isEmpty {
|
||||
rows.append(("Product Type", productType))
|
||||
}
|
||||
if let osVersion = entry.device.osVersion, !osVersion.isEmpty {
|
||||
rows.append(("OS Version", osVersion))
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
private var minecraftRows: [(String, String)] {
|
||||
if let error = entry.discoveryErrorDescription, !error.isEmpty {
|
||||
return [("Discovery Error", error)]
|
||||
}
|
||||
|
||||
guard let container = entry.minecraftContainer else {
|
||||
return [("Minecraft Container", "Not found")]
|
||||
}
|
||||
|
||||
var rows: [(String, String)] = [
|
||||
("Minecraft Container", container.appName),
|
||||
("App ID", container.appID),
|
||||
("Access Mode", container.accessMode.rawValue)
|
||||
]
|
||||
|
||||
if let relativePath = container.minecraftFolderRelativePath, !relativePath.isEmpty {
|
||||
rows.append(("Minecraft Path", relativePath))
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
private var technicalRows: [(String, String)] {
|
||||
[
|
||||
("UDID", entry.device.udid),
|
||||
("Device ID", entry.id)
|
||||
]
|
||||
}
|
||||
|
||||
private var connectionLabel: String {
|
||||
switch entry.device.connection {
|
||||
case .usb:
|
||||
return "USB"
|
||||
case .network:
|
||||
return "Network"
|
||||
}
|
||||
}
|
||||
|
||||
private var trustStateLabel: String {
|
||||
switch entry.device.trustState {
|
||||
case .trusted:
|
||||
return "Trusted"
|
||||
case .locked:
|
||||
return "Locked"
|
||||
case .untrusted:
|
||||
return "Untrusted"
|
||||
case .unavailable:
|
||||
return "Unavailable"
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func sourceSection(title: String, rows: [(String, String)]) -> some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text(title)
|
||||
.appSectionTitleStyle(.section)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
ForEach(rows, id: \.0) { title, value in
|
||||
detailRow(title: title, value: value)
|
||||
}
|
||||
}
|
||||
.appDetailSectionCard()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func detailRow(title: String, value: String) -> some View {
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Text(title)
|
||||
.appTextStyle(.fieldLabel)
|
||||
.frame(width: 150, alignment: .leading)
|
||||
|
||||
Text(value)
|
||||
.textSelection(.enabled)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@ struct ItemDetailColumnView: View {
|
||||
let item: MinecraftContentItem?
|
||||
let source: MinecraftSource?
|
||||
let sourceCandidate: SourceCandidate?
|
||||
let connectedDevice: ConnectedDeviceSidebarEntry?
|
||||
let showsSourceDetails: Bool
|
||||
let behaviorPacks: [ContentPackReference]
|
||||
let resourcePacks: [ContentPackReference]
|
||||
@ -25,11 +26,11 @@ struct ItemDetailColumnView: View {
|
||||
let shareAction: (NSView?) -> Void
|
||||
let addCandidateSourceAction: (SourceCandidate) -> Void
|
||||
let revealCandidateAction: (SourceCandidate) -> Void
|
||||
let addConnectedDeviceAction: (ConnectedDeviceSidebarEntry) -> Void
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if isEmpty {
|
||||
} else if let item {
|
||||
if let item {
|
||||
ItemDetailView(
|
||||
item: item,
|
||||
source: source,
|
||||
@ -59,6 +60,14 @@ struct ItemDetailColumnView: View {
|
||||
revealCandidateAction(sourceCandidate)
|
||||
}
|
||||
)
|
||||
} else if let connectedDevice {
|
||||
ConnectedDeviceDetailView(
|
||||
entry: connectedDevice,
|
||||
addAction: connectedDevice.hasMinecraftContainer ? {
|
||||
addConnectedDeviceAction(connectedDevice)
|
||||
} : nil
|
||||
)
|
||||
} else if isEmpty {
|
||||
} else {
|
||||
Text("Select a world or pack to see details")
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
@ -356,6 +356,7 @@ struct ItemDetailColumnPreviewContainer: View {
|
||||
item: PreviewFixtures.featuredWorld,
|
||||
source: PreviewFixtures.primarySource,
|
||||
sourceCandidate: nil,
|
||||
connectedDevice: nil,
|
||||
showsSourceDetails: false,
|
||||
behaviorPacks: PreviewFixtures.primarySource.resolvedPackReferences(for: PreviewFixtures.featuredWorld.id, type: .behaviorPack),
|
||||
resourcePacks: PreviewFixtures.primarySource.resolvedPackReferences(for: PreviewFixtures.featuredWorld.id, type: .resourcePack),
|
||||
@ -372,7 +373,8 @@ struct ItemDetailColumnPreviewContainer: View {
|
||||
revealAction: {},
|
||||
shareAction: { _ in },
|
||||
addCandidateSourceAction: { _ in },
|
||||
revealCandidateAction: { _ in }
|
||||
revealCandidateAction: { _ in },
|
||||
addConnectedDeviceAction: { _ in }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,9 +43,10 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let isEmptyLibrary = library.visibleSources.isEmpty && library.connectedDevices.isEmpty
|
||||
let isEmptyLibrary = library.visibleSources.isEmpty && library.sidebarConnectedDevices.isEmpty && library.sourceCandidates.isEmpty
|
||||
let resolvedCurrentSource = currentSource
|
||||
let resolvedCurrentSourceCandidate = currentSourceCandidate
|
||||
let resolvedCurrentConnectedDevice = currentConnectedDevice
|
||||
let currentProjectionRequest = ItemCollectionProjectionRequest(
|
||||
selection: selectedSidebarSelection,
|
||||
searchText: searchText,
|
||||
@ -75,7 +76,7 @@ struct ContentView: View {
|
||||
NavigationSplitView(columnVisibility: $columnVisibility) {
|
||||
SourcesSidebarView(
|
||||
sources: library.sidebarSources,
|
||||
connectedDevices: library.connectedDevices,
|
||||
connectedDevices: library.sidebarConnectedDevices,
|
||||
sourceCandidates: library.sourceCandidates,
|
||||
isDiscoveringSourceCandidates: library.isDiscoveringSourceCandidates,
|
||||
selection: sidebarSelectionBinding,
|
||||
@ -124,6 +125,7 @@ struct ContentView: View {
|
||||
item: resolvedCurrentSelectedItem,
|
||||
source: resolvedCurrentSource,
|
||||
sourceCandidate: resolvedCurrentSourceCandidate,
|
||||
connectedDevice: resolvedCurrentConnectedDevice,
|
||||
showsSourceDetails: resolvedCurrentSelectedItem == nil && isSourceOverviewSelection,
|
||||
behaviorPacks: resolvedCurrentSelectedItem.map { logicalPackReferences(for: $0, type: .behaviorPack) } ?? [],
|
||||
resourcePacks: resolvedCurrentSelectedItem.map { logicalPackReferences(for: $0, type: .resourcePack) } ?? [],
|
||||
@ -158,7 +160,8 @@ struct ContentView: View {
|
||||
shareItem(item, from: anchorView)
|
||||
},
|
||||
addCandidateSourceAction: addCandidateSource(_:),
|
||||
revealCandidateAction: revealCandidateInFinder(_:)
|
||||
revealCandidateAction: revealCandidateInFinder(_:),
|
||||
addConnectedDeviceAction: addConnectedDeviceSource(from:)
|
||||
)
|
||||
.frame(minWidth: 450)
|
||||
}
|
||||
@ -193,7 +196,7 @@ struct ContentView: View {
|
||||
.onChange(of: library.sources.map(\.id)) { _, _ in
|
||||
syncSelection(with: library.visibleSources.map(\.id))
|
||||
}
|
||||
.onChange(of: library.connectedDevices.map { "\($0.id)::\($0.matchedSourceID?.absoluteString ?? "nil")" }) { _, _ in
|
||||
.onChange(of: library.sidebarConnectedDevices.map { "\($0.id)::\($0.matchedSourceID?.absoluteString ?? "nil")" }) { _, _ in
|
||||
syncSelection(with: library.visibleSources.map(\.id))
|
||||
}
|
||||
.task(id: currentProjectionRequest) {
|
||||
@ -267,6 +270,14 @@ struct ContentView: View {
|
||||
return library.sourceCandidates.first { $0.id == candidateID }
|
||||
}
|
||||
|
||||
private var currentConnectedDevice: ConnectedDeviceSidebarEntry? {
|
||||
guard case .connectedDevice(let deviceID) = selectedSidebarSelection else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return library.sidebarConnectedDevices.first { $0.id == deviceID }
|
||||
}
|
||||
|
||||
private func currentSelectedItem(in source: MinecraftSource?) -> MinecraftContentItem? {
|
||||
guard let selectedItemID else {
|
||||
return nil
|
||||
@ -676,6 +687,10 @@ struct ContentView: View {
|
||||
if !library.sourceCandidates.contains(where: { $0.id == candidateID }) {
|
||||
self.selectedSidebarSelection = sourceIDs.first.map { .source(sourceID: $0) }
|
||||
}
|
||||
case .connectedDevice(let deviceID):
|
||||
if !library.sidebarConnectedDevices.contains(where: { $0.id == deviceID }) {
|
||||
self.selectedSidebarSelection = sourceIDs.first.map { .source(sourceID: $0) }
|
||||
}
|
||||
case .source, .allContent, .contentType, .contentKind:
|
||||
if let selectedSourceID = selectedSidebarSelection.sourceID,
|
||||
!sourceIDs.contains(selectedSourceID) {
|
||||
|
||||
@ -79,6 +79,8 @@ enum ItemCollectionProjector {
|
||||
switch selection {
|
||||
case .sourceCandidate:
|
||||
return "Source Candidate"
|
||||
case .connectedDevice:
|
||||
return "Connected Device"
|
||||
case .source, .allContent:
|
||||
return "All Items"
|
||||
case .contentType(_, let contentType):
|
||||
@ -90,7 +92,7 @@ enum ItemCollectionProjector {
|
||||
|
||||
nonisolated static func searchPrompt(for selection: SidebarSelection?, source: MinecraftSource?) -> String {
|
||||
switch selection {
|
||||
case .some(.sourceCandidate):
|
||||
case .some(.sourceCandidate), .some(.connectedDevice):
|
||||
return "Search Library"
|
||||
case .some(.source):
|
||||
return "Search \(source?.displayName ?? "Library")"
|
||||
@ -109,6 +111,8 @@ enum ItemCollectionProjector {
|
||||
switch selection {
|
||||
case .some(.sourceCandidate):
|
||||
return "Source Candidate"
|
||||
case .some(.connectedDevice):
|
||||
return "Connected Device"
|
||||
case .some(.source):
|
||||
return "Library"
|
||||
case .some(.allContent):
|
||||
@ -128,7 +132,7 @@ enum ItemCollectionProjector {
|
||||
}
|
||||
|
||||
switch selection {
|
||||
case .sourceCandidate:
|
||||
case .sourceCandidate, .connectedDevice:
|
||||
return "items"
|
||||
case .source, .allContent:
|
||||
return scopedItemCount == 1 ? "item" : "items"
|
||||
|
||||
@ -6,6 +6,7 @@ import SwiftUI
|
||||
enum SidebarSelection: Hashable, Sendable {
|
||||
case source(sourceID: URL)
|
||||
case sourceCandidate(candidateID: String)
|
||||
case connectedDevice(deviceID: String)
|
||||
case allContent(sourceID: URL)
|
||||
case contentType(sourceID: URL, contentType: MinecraftContentType)
|
||||
case contentKind(sourceID: URL, contentKind: MinecraftContentKind)
|
||||
@ -14,7 +15,7 @@ enum SidebarSelection: Hashable, Sendable {
|
||||
switch self {
|
||||
case .source(let sourceID), .allContent(let sourceID), .contentType(let sourceID, _), .contentKind(let sourceID, _):
|
||||
return sourceID
|
||||
case .sourceCandidate:
|
||||
case .sourceCandidate, .connectedDevice:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -152,10 +153,14 @@ struct SourcesSidebarView: View {
|
||||
private func connectedDeviceSectionRows(for entry: ConnectedDeviceSidebarEntry) -> some View {
|
||||
ConnectedDeviceRow(
|
||||
entry: entry,
|
||||
onSelect: {
|
||||
selection = .connectedDevice(deviceID: entry.id)
|
||||
},
|
||||
addAction: entry.hasMinecraftContainer ? {
|
||||
addConnectedDeviceAction(entry)
|
||||
} : nil
|
||||
)
|
||||
.tag(SidebarSelection.connectedDevice(deviceID: entry.id) as SidebarSelection?)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(EdgeInsets(top: 6, leading: 8, bottom: 0, trailing: 8))
|
||||
}
|
||||
@ -185,9 +190,9 @@ private struct SourceCandidateRow: View {
|
||||
Spacer(minLength: 8)
|
||||
|
||||
Button(action: addAction) {
|
||||
Image(systemName: "plus")
|
||||
Text("Add")
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.appMiniProminentButton()
|
||||
.help("Add Source")
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
@ -395,6 +400,7 @@ private struct CircularScanProgressView: View {
|
||||
|
||||
private struct ConnectedDeviceRow: View {
|
||||
let entry: ConnectedDeviceSidebarEntry
|
||||
let onSelect: () -> Void
|
||||
let addAction: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
@ -424,6 +430,8 @@ private struct ConnectedDeviceRow: View {
|
||||
}
|
||||
}
|
||||
.opacity(addAction == nil ? 0.68 : 1)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture(perform: onSelect)
|
||||
}
|
||||
|
||||
private var iconName: String {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user