Extract connected device source helpers
This commit is contained in:
parent
1e0447a2b1
commit
3a30b94369
@ -0,0 +1,220 @@
|
|||||||
|
//
|
||||||
|
// SourceConnectedDevices.swift
|
||||||
|
// World Manager for Minecraft
|
||||||
|
//
|
||||||
|
// Created by OpenAI Codex on 2026-05-28.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct ConnectedDeviceSidebarEntry: Identifiable, Hashable {
|
||||||
|
let device: ConnectedDevice
|
||||||
|
let containers: [DeviceAppContainer]
|
||||||
|
let matchedSourceID: URL?
|
||||||
|
let discoveryErrorDescription: String?
|
||||||
|
|
||||||
|
var id: String { device.id }
|
||||||
|
|
||||||
|
var minecraftContainer: DeviceAppContainer? {
|
||||||
|
containers.first(where: { $0.appID == "com.mojang.minecraftpe" })
|
||||||
|
?? containers.first(where: { $0.minecraftFolderRelativePath != nil })
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasMinecraftContainer: Bool {
|
||||||
|
minecraftContainer != nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CachedConnectedDeviceDiscovery {
|
||||||
|
let device: ConnectedDevice
|
||||||
|
let containers: [DeviceAppContainer]
|
||||||
|
let discoveryErrorDescription: String?
|
||||||
|
let refreshedAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ConnectedDeviceRefreshContext {
|
||||||
|
let existingSources: [MinecraftSource]
|
||||||
|
let sourceFactory: ConnectedDeviceSourceFactory
|
||||||
|
|
||||||
|
func knownSourceIDs(for device: ConnectedDevice) -> [URL] {
|
||||||
|
existingSources.compactMap { source -> URL? in
|
||||||
|
guard case .connectedDevice(let expectedDevice, _) = source.origin else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return expectedDevice.udid == device.udid ? source.id : nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasKnownSource(for device: ConnectedDevice) -> Bool {
|
||||||
|
!knownSourceIDs(for: device).isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchingSourceID(
|
||||||
|
for device: ConnectedDevice,
|
||||||
|
containers: [DeviceAppContainer]
|
||||||
|
) -> URL? {
|
||||||
|
for container in containers {
|
||||||
|
let sourceID = sourceFactory.makeSourceIdentifier(
|
||||||
|
device: device,
|
||||||
|
container: container
|
||||||
|
)
|
||||||
|
if existingSources.contains(where: { $0.id == sourceID }) {
|
||||||
|
return sourceID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ConnectedDeviceDiscoveryCachePolicy {
|
||||||
|
private static let usbCacheTTL: TimeInterval = 60.0
|
||||||
|
private static let networkCacheTTL: TimeInterval = 180.0
|
||||||
|
|
||||||
|
static func cachedDiscovery(
|
||||||
|
for device: ConnectedDevice,
|
||||||
|
cache: [String: CachedConnectedDeviceDiscovery],
|
||||||
|
isActivelyScanning: Bool,
|
||||||
|
now: Date = Date()
|
||||||
|
) -> CachedConnectedDeviceDiscovery? {
|
||||||
|
guard let cachedDiscovery = cache[device.udid] else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if isActivelyScanning {
|
||||||
|
return cachedDiscovery
|
||||||
|
}
|
||||||
|
|
||||||
|
let age = now.timeIntervalSince(cachedDiscovery.refreshedAt)
|
||||||
|
guard age <= discoveryCacheTTL(for: device) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard cachedDiscovery.device.connection == device.connection,
|
||||||
|
cachedDiscovery.device.trustState == device.trustState,
|
||||||
|
cachedDiscovery.device.name == device.name else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedDiscovery
|
||||||
|
}
|
||||||
|
|
||||||
|
static func cacheDiscovery(
|
||||||
|
for device: ConnectedDevice,
|
||||||
|
containers: [DeviceAppContainer],
|
||||||
|
discoveryErrorDescription: String?,
|
||||||
|
now: Date = Date()
|
||||||
|
) -> CachedConnectedDeviceDiscovery {
|
||||||
|
CachedConnectedDeviceDiscovery(
|
||||||
|
device: device,
|
||||||
|
containers: containers,
|
||||||
|
discoveryErrorDescription: discoveryErrorDescription,
|
||||||
|
refreshedAt: now
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func discoveryCacheTTL(for device: ConnectedDevice) -> TimeInterval {
|
||||||
|
switch device.connection {
|
||||||
|
case .usb:
|
||||||
|
return usbCacheTTL
|
||||||
|
case .network:
|
||||||
|
return networkCacheTTL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ConnectedDeviceSourcePolicy {
|
||||||
|
static func availability(for device: ConnectedDevice, hasMinecraftContainer: Bool) -> SourceAvailability {
|
||||||
|
guard hasMinecraftContainer else {
|
||||||
|
return .unavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
switch device.trustState {
|
||||||
|
case .trusted:
|
||||||
|
return .available
|
||||||
|
case .locked, .untrusted:
|
||||||
|
return .limited
|
||||||
|
case .unavailable:
|
||||||
|
return .disconnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func preferredDeviceName(
|
||||||
|
currentName: String,
|
||||||
|
fallbackDeviceName: String,
|
||||||
|
fallbackDisplayName: String
|
||||||
|
) -> String {
|
||||||
|
if let sanitizedCurrentName = sanitizedDeviceName(currentName) {
|
||||||
|
return sanitizedCurrentName
|
||||||
|
}
|
||||||
|
|
||||||
|
if let sanitizedFallbackName = sanitizedDeviceName(fallbackDeviceName) {
|
||||||
|
return sanitizedFallbackName
|
||||||
|
}
|
||||||
|
|
||||||
|
if let displayNamePrefix = fallbackDisplayName.components(separatedBy: " • ").first,
|
||||||
|
let sanitizedDisplayName = sanitizedDeviceName(displayNamePrefix) {
|
||||||
|
return sanitizedDisplayName
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Unknown Device"
|
||||||
|
}
|
||||||
|
|
||||||
|
static func sanitizedDeviceName(_ candidate: String) -> String? {
|
||||||
|
let trimmedCandidate = candidate.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !trimmedCandidate.isEmpty else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalizedCandidate = trimmedCandidate.lowercased()
|
||||||
|
guard normalizedCandidate != "unknown device",
|
||||||
|
normalizedCandidate != "unknown device..." else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmedCandidate
|
||||||
|
}
|
||||||
|
|
||||||
|
static func shouldRefreshSource(_ source: MinecraftSource) -> Bool {
|
||||||
|
guard !source.isScanning else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasRefreshDebt(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func shouldRefreshSourceOnReconnect(_ source: MinecraftSource, now: Date = Date()) -> Bool {
|
||||||
|
guard !source.isScanning else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasRefreshDebt(source) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let lastScanDate = source.lastScanDate else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
let reconnectGracePeriod: TimeInterval = 5 * 60
|
||||||
|
if now.timeIntervalSince(lastScanDate) < reconnectGracePeriod {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldRefreshSource(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func hasRefreshDebt(_ source: MinecraftSource) -> Bool {
|
||||||
|
guard source.origin.kind == .connectedDevice else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !source.rawItems.isEmpty else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemCount = source.rawItems.count
|
||||||
|
return source.previewLoadedCount < itemCount || source.sizeLoadedCount < itemCount
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,31 +9,6 @@ import Combine
|
|||||||
import Foundation
|
import Foundation
|
||||||
import OSLog
|
import OSLog
|
||||||
|
|
||||||
struct ConnectedDeviceSidebarEntry: Identifiable, Hashable {
|
|
||||||
let device: ConnectedDevice
|
|
||||||
let containers: [DeviceAppContainer]
|
|
||||||
let matchedSourceID: URL?
|
|
||||||
let discoveryErrorDescription: String?
|
|
||||||
|
|
||||||
var id: String { device.id }
|
|
||||||
|
|
||||||
var minecraftContainer: DeviceAppContainer? {
|
|
||||||
containers.first(where: { $0.appID == "com.mojang.minecraftpe" })
|
|
||||||
?? containers.first(where: { $0.minecraftFolderRelativePath != nil })
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasMinecraftContainer: Bool {
|
|
||||||
minecraftContainer != nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct CachedConnectedDeviceDiscovery {
|
|
||||||
let device: ConnectedDevice
|
|
||||||
let containers: [DeviceAppContainer]
|
|
||||||
let discoveryErrorDescription: String?
|
|
||||||
let refreshedAt: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class SourceLibrary: ObservableObject {
|
final class SourceLibrary: ObservableObject {
|
||||||
private static let enrichmentWorkerCount = 4
|
private static let enrichmentWorkerCount = 4
|
||||||
@ -45,8 +20,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
private static let connectedDeviceRefreshIntervalWhileScanning: TimeInterval = 5.0
|
private static let connectedDeviceRefreshIntervalWhileScanning: TimeInterval = 5.0
|
||||||
private static let usbConnectedDeviceAutoRefreshInterval: TimeInterval = 45.0
|
private static let usbConnectedDeviceAutoRefreshInterval: TimeInterval = 45.0
|
||||||
private static let networkConnectedDeviceAutoRefreshInterval: TimeInterval = 120.0
|
private static let networkConnectedDeviceAutoRefreshInterval: TimeInterval = 120.0
|
||||||
private static let usbConnectedDeviceDiscoveryCacheTTL: TimeInterval = 60.0
|
|
||||||
private static let networkConnectedDeviceDiscoveryCacheTTL: TimeInterval = 180.0
|
|
||||||
private static let performanceLogger = Logger(
|
private static let performanceLogger = Logger(
|
||||||
subsystem: Bundle.main.bundleIdentifier ?? "WorldManagerForMinecraft",
|
subsystem: Bundle.main.bundleIdentifier ?? "WorldManagerForMinecraft",
|
||||||
category: "ConnectedDevicePerformance"
|
category: "ConnectedDevicePerformance"
|
||||||
@ -955,6 +928,10 @@ final class SourceLibrary: ObservableObject {
|
|||||||
|
|
||||||
var entries: [ConnectedDeviceSidebarEntry] = []
|
var entries: [ConnectedDeviceSidebarEntry] = []
|
||||||
var matchedSourceIDs = Set<URL>()
|
var matchedSourceIDs = Set<URL>()
|
||||||
|
let refreshContext = ConnectedDeviceRefreshContext(
|
||||||
|
existingSources: sources,
|
||||||
|
sourceFactory: connectedDeviceSourceFactory
|
||||||
|
)
|
||||||
let activeScanningDeviceUDIDs = Set(
|
let activeScanningDeviceUDIDs = Set(
|
||||||
sources.compactMap { source -> String? in
|
sources.compactMap { source -> String? in
|
||||||
guard source.isScanning, case .connectedDevice(let device, _) = source.origin else {
|
guard source.isScanning, case .connectedDevice(let device, _) = source.origin else {
|
||||||
@ -968,7 +945,7 @@ final class SourceLibrary: ObservableObject {
|
|||||||
cachedDeviceDiscoveryByUDID = cachedDeviceDiscoveryByUDID.filter { currentDeviceUDIDs.contains($0.key) }
|
cachedDeviceDiscoveryByUDID = cachedDeviceDiscoveryByUDID.filter { currentDeviceUDIDs.contains($0.key) }
|
||||||
|
|
||||||
for device in devices {
|
for device in devices {
|
||||||
let knownSourceIDs = knownConnectedDeviceSourceIDs(for: device)
|
let knownSourceIDs = refreshContext.knownSourceIDs(for: device)
|
||||||
if !knownSourceIDs.isEmpty {
|
if !knownSourceIDs.isEmpty {
|
||||||
matchedSourceIDs.formUnion(knownSourceIDs)
|
matchedSourceIDs.formUnion(knownSourceIDs)
|
||||||
let cachedContainers = cachedDeviceDiscoveryByUDID[device.udid]?.containers ?? []
|
let cachedContainers = cachedDeviceDiscoveryByUDID[device.udid]?.containers ?? []
|
||||||
@ -985,7 +962,11 @@ final class SourceLibrary: ObservableObject {
|
|||||||
|
|
||||||
let containers: [DeviceAppContainer]
|
let containers: [DeviceAppContainer]
|
||||||
let discoveryErrorDescription: String?
|
let discoveryErrorDescription: String?
|
||||||
if let cachedDiscovery = cachedDiscovery(for: device, isActivelyScanning: activeScanningDeviceUDIDs.contains(device.udid)) {
|
if let cachedDiscovery = ConnectedDeviceDiscoveryCachePolicy.cachedDiscovery(
|
||||||
|
for: device,
|
||||||
|
cache: cachedDeviceDiscoveryByUDID,
|
||||||
|
isActivelyScanning: activeScanningDeviceUDIDs.contains(device.udid)
|
||||||
|
) {
|
||||||
containers = cachedDiscovery.containers
|
containers = cachedDiscovery.containers
|
||||||
discoveryErrorDescription = cachedDiscovery.discoveryErrorDescription
|
discoveryErrorDescription = cachedDiscovery.discoveryErrorDescription
|
||||||
} else {
|
} else {
|
||||||
@ -1023,10 +1004,7 @@ final class SourceLibrary: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let matchedSourceID = matchingConnectedDeviceSourceID(
|
let matchedSourceID = refreshContext.matchingSourceID(for: device, containers: containers)
|
||||||
device: device,
|
|
||||||
containers: containers
|
|
||||||
)
|
|
||||||
|
|
||||||
if let matchedSourceID {
|
if let matchedSourceID {
|
||||||
matchedSourceIDs.insert(matchedSourceID)
|
matchedSourceIDs.insert(matchedSourceID)
|
||||||
@ -1039,7 +1017,7 @@ final class SourceLibrary: ObservableObject {
|
|||||||
|
|
||||||
let shouldDisplayEntry =
|
let shouldDisplayEntry =
|
||||||
matchedSourceID == nil
|
matchedSourceID == nil
|
||||||
&& !hasKnownConnectedDeviceSource(for: device)
|
&& !refreshContext.hasKnownSource(for: device)
|
||||||
&& (!containers.isEmpty || device.trustState != .trusted)
|
&& (!containers.isEmpty || device.trustState != .trusted)
|
||||||
|
|
||||||
if shouldDisplayEntry {
|
if shouldDisplayEntry {
|
||||||
@ -1075,88 +1053,27 @@ final class SourceLibrary: ObservableObject {
|
|||||||
lastMatchedConnectedSourceIDs = matchedSourceIDs
|
lastMatchedConnectedSourceIDs = matchedSourceIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
private func cachedDiscovery(for device: ConnectedDevice, isActivelyScanning: Bool) -> CachedConnectedDeviceDiscovery? {
|
|
||||||
guard let cachedDiscovery = cachedDeviceDiscoveryByUDID[device.udid] else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if isActivelyScanning {
|
|
||||||
return cachedDiscovery
|
|
||||||
}
|
|
||||||
|
|
||||||
let age = Date().timeIntervalSince(cachedDiscovery.refreshedAt)
|
|
||||||
guard age <= discoveryCacheTTL(for: device) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
guard cachedDiscovery.device.connection == device.connection,
|
|
||||||
cachedDiscovery.device.trustState == device.trustState,
|
|
||||||
cachedDiscovery.device.name == device.name else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return cachedDiscovery
|
|
||||||
}
|
|
||||||
|
|
||||||
private func discoveryCacheTTL(for device: ConnectedDevice) -> TimeInterval {
|
|
||||||
switch device.connection {
|
|
||||||
case .usb:
|
|
||||||
return Self.usbConnectedDeviceDiscoveryCacheTTL
|
|
||||||
case .network:
|
|
||||||
return Self.networkConnectedDeviceDiscoveryCacheTTL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func cacheDeviceDiscovery(
|
private func cacheDeviceDiscovery(
|
||||||
device: ConnectedDevice,
|
device: ConnectedDevice,
|
||||||
containers: [DeviceAppContainer],
|
containers: [DeviceAppContainer],
|
||||||
discoveryErrorDescription: String?
|
discoveryErrorDescription: String?
|
||||||
) {
|
) {
|
||||||
cachedDeviceDiscoveryByUDID[device.udid] = CachedConnectedDeviceDiscovery(
|
cachedDeviceDiscoveryByUDID[device.udid] = ConnectedDeviceDiscoveryCachePolicy.cacheDiscovery(
|
||||||
device: device,
|
for: device,
|
||||||
containers: containers,
|
containers: containers,
|
||||||
discoveryErrorDescription: discoveryErrorDescription,
|
discoveryErrorDescription: discoveryErrorDescription
|
||||||
refreshedAt: Date()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func matchingConnectedDeviceSourceID(
|
|
||||||
device: ConnectedDevice,
|
|
||||||
containers: [DeviceAppContainer]
|
|
||||||
) -> URL? {
|
|
||||||
for container in containers {
|
|
||||||
let sourceID = connectedDeviceSourceFactory.makeSourceIdentifier(
|
|
||||||
device: device,
|
|
||||||
container: container
|
|
||||||
)
|
|
||||||
if sources.contains(where: { $0.id == sourceID }) {
|
|
||||||
return sourceID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func knownConnectedDeviceSourceIDs(for device: ConnectedDevice) -> [URL] {
|
|
||||||
sources.compactMap { source -> URL? in
|
|
||||||
guard case .connectedDevice(let expectedDevice, _) = source.origin else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return expectedDevice.udid == device.udid ? source.id : nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func hasKnownConnectedDeviceSource(for device: ConnectedDevice) -> Bool {
|
|
||||||
!knownConnectedDeviceSourceIDs(for: device).isEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
private func refreshMatchedConnectedDeviceSource(
|
private func refreshMatchedConnectedDeviceSource(
|
||||||
sourceID: URL,
|
sourceID: URL,
|
||||||
device: ConnectedDevice,
|
device: ConnectedDevice,
|
||||||
containers: [DeviceAppContainer]
|
containers: [DeviceAppContainer]
|
||||||
) {
|
) {
|
||||||
let nextAvailability = availability(for: device, hasMinecraftContainer: true)
|
let nextAvailability = ConnectedDeviceSourcePolicy.availability(
|
||||||
|
for: device,
|
||||||
|
hasMinecraftContainer: true
|
||||||
|
)
|
||||||
updateSource(sourceID) { source in
|
updateSource(sourceID) { source in
|
||||||
guard case .connectedDevice(let previousDevice, let previousContainer) = source.origin else {
|
guard case .connectedDevice(let previousDevice, let previousContainer) = source.origin else {
|
||||||
return
|
return
|
||||||
@ -1167,7 +1084,7 @@ final class SourceLibrary: ObservableObject {
|
|||||||
}) ?? previousContainer
|
}) ?? previousContainer
|
||||||
|
|
||||||
var resolvedDevice = device
|
var resolvedDevice = device
|
||||||
resolvedDevice.name = preferredConnectedDeviceName(
|
resolvedDevice.name = ConnectedDeviceSourcePolicy.preferredDeviceName(
|
||||||
currentName: device.name,
|
currentName: device.name,
|
||||||
fallbackDeviceName: previousDevice.name,
|
fallbackDeviceName: previousDevice.name,
|
||||||
fallbackDisplayName: source.displayName
|
fallbackDisplayName: source.displayName
|
||||||
@ -1187,7 +1104,7 @@ final class SourceLibrary: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if transition.becameAvailable {
|
if transition.becameAvailable {
|
||||||
if shouldRefreshConnectedDeviceOnReconnect(source, device: device) {
|
if ConnectedDeviceSourcePolicy.shouldRefreshSourceOnReconnect(source) {
|
||||||
queueAutomaticSync(
|
queueAutomaticSync(
|
||||||
for: sourceID,
|
for: sourceID,
|
||||||
reason: source.hasCachedContent
|
reason: source.hasCachedContent
|
||||||
@ -1198,7 +1115,7 @@ final class SourceLibrary: ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if shouldRefreshConnectedDeviceSource(source, device: device) {
|
if ConnectedDeviceSourcePolicy.shouldRefreshSource(source) {
|
||||||
queueAutomaticSync(for: sourceID, reason: "Refreshing device library...")
|
queueAutomaticSync(for: sourceID, reason: "Refreshing device library...")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1215,57 +1132,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func availability(for device: ConnectedDevice, hasMinecraftContainer: Bool) -> SourceAvailability {
|
|
||||||
guard hasMinecraftContainer else {
|
|
||||||
return .unavailable
|
|
||||||
}
|
|
||||||
|
|
||||||
switch device.trustState {
|
|
||||||
case .trusted:
|
|
||||||
return .available
|
|
||||||
case .locked, .untrusted:
|
|
||||||
return .limited
|
|
||||||
case .unavailable:
|
|
||||||
return .disconnected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func preferredConnectedDeviceName(
|
|
||||||
currentName: String,
|
|
||||||
fallbackDeviceName: String,
|
|
||||||
fallbackDisplayName: String
|
|
||||||
) -> String {
|
|
||||||
if let sanitizedCurrentName = sanitizedConnectedDeviceName(currentName) {
|
|
||||||
return sanitizedCurrentName
|
|
||||||
}
|
|
||||||
|
|
||||||
if let sanitizedFallbackName = sanitizedConnectedDeviceName(fallbackDeviceName) {
|
|
||||||
return sanitizedFallbackName
|
|
||||||
}
|
|
||||||
|
|
||||||
if let displayNamePrefix = fallbackDisplayName.components(separatedBy: " • ").first,
|
|
||||||
let sanitizedDisplayName = sanitizedConnectedDeviceName(displayNamePrefix) {
|
|
||||||
return sanitizedDisplayName
|
|
||||||
}
|
|
||||||
|
|
||||||
return "Unknown Device"
|
|
||||||
}
|
|
||||||
|
|
||||||
private func sanitizedConnectedDeviceName(_ candidate: String) -> String? {
|
|
||||||
let trimmedCandidate = candidate.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
guard !trimmedCandidate.isEmpty else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let normalizedCandidate = trimmedCandidate.lowercased()
|
|
||||||
guard normalizedCandidate != "unknown device",
|
|
||||||
normalizedCandidate != "unknown device..." else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return trimmedCandidate
|
|
||||||
}
|
|
||||||
|
|
||||||
private func restorePersistedSources() async {
|
private func restorePersistedSources() async {
|
||||||
defer {
|
defer {
|
||||||
isRestoringPersistedSources = false
|
isRestoringPersistedSources = false
|
||||||
@ -1289,14 +1155,14 @@ final class SourceLibrary: ObservableObject {
|
|||||||
)
|
)
|
||||||
if case .connectedDevice(let device, let container) = source.origin {
|
if case .connectedDevice(let device, let container) = source.origin {
|
||||||
var repairedDevice = device
|
var repairedDevice = device
|
||||||
repairedDevice.name = preferredConnectedDeviceName(
|
repairedDevice.name = ConnectedDeviceSourcePolicy.preferredDeviceName(
|
||||||
currentName: device.name,
|
currentName: device.name,
|
||||||
fallbackDeviceName: "",
|
fallbackDeviceName: "",
|
||||||
fallbackDisplayName: record.displayName
|
fallbackDisplayName: record.displayName
|
||||||
)
|
)
|
||||||
source.origin = .connectedDevice(device: repairedDevice, container: container)
|
source.origin = .connectedDevice(device: repairedDevice, container: container)
|
||||||
let persistedDeviceName = record.displayName.components(separatedBy: " • ").first ?? record.displayName
|
let persistedDeviceName = record.displayName.components(separatedBy: " • ").first ?? record.displayName
|
||||||
if sanitizedConnectedDeviceName(persistedDeviceName) == nil {
|
if ConnectedDeviceSourcePolicy.sanitizedDeviceName(persistedDeviceName) == nil {
|
||||||
source.displayName = connectedDeviceSourceFactory.displayName(
|
source.displayName = connectedDeviceSourceFactory.displayName(
|
||||||
for: repairedDevice,
|
for: repairedDevice,
|
||||||
container: container
|
container: container
|
||||||
@ -1654,52 +1520,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
return diagnostic.localizedCaseInsensitiveContains("showing cached results")
|
return diagnostic.localizedCaseInsensitiveContains("showing cached results")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func shouldRefreshConnectedDeviceSource(_ source: MinecraftSource, device: ConnectedDevice) -> Bool {
|
|
||||||
guard !source.isScanning else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if connectedDeviceSourceHasRefreshDebt(source) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
_ = device
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private func shouldRefreshConnectedDeviceOnReconnect(_ source: MinecraftSource, device: ConnectedDevice) -> Bool {
|
|
||||||
guard !source.isScanning else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if connectedDeviceSourceHasRefreshDebt(source) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let lastScanDate = source.lastScanDate else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
let reconnectGracePeriod: TimeInterval = 5 * 60
|
|
||||||
if Date().timeIntervalSince(lastScanDate) < reconnectGracePeriod {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return shouldRefreshConnectedDeviceSource(source, device: device)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func connectedDeviceSourceHasRefreshDebt(_ source: MinecraftSource) -> Bool {
|
|
||||||
guard source.origin.kind == .connectedDevice else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
guard !source.rawItems.isEmpty else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
let itemCount = source.rawItems.count
|
|
||||||
return source.previewLoadedCount < itemCount || source.sizeLoadedCount < itemCount
|
|
||||||
}
|
|
||||||
|
|
||||||
private func shouldPreferPackItem(_ candidate: MinecraftContentItem, over existing: MinecraftContentItem) -> Bool {
|
private func shouldPreferPackItem(_ candidate: MinecraftContentItem, over existing: MinecraftContentItem) -> Bool {
|
||||||
let candidateEmbedded = isEmbeddedWorldPack(candidate)
|
let candidateEmbedded = isEmbeddedWorldPack(candidate)
|
||||||
let existingEmbedded = isEmbeddedWorldPack(existing)
|
let existingEmbedded = isEmbeddedWorldPack(existing)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user