mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
refactor(apple): type core event phase/kind/direction as enums
Replaces the stringly-typed transfer-event phase/kind/direction values throughout the progress-derivation logic with EventPhase, EventKind and EventDirection enums (String-backed to match the core's wire values). CoreEventModel keeps the raw wire strings as a faithful boundary DTO but exposes typed eventPhase/eventKind/eventDirection accessors; all logic — progressForTransfer/Receiver, humanProgressLabel, aggregateReceiverProgress, the refresh trigger, and the SendScreen snapshots — now compares enum cases instead of literals. TransferProgress.phase/kind are the enums directly, so constructions read `phase: .transfer, kind: .progress`. The two ad-hoc phase/kind Sets collapse into "is a recognized case" (non-nil) checks.
This commit is contained in:
@@ -57,7 +57,7 @@ final class ProgressDerivationTests: XCTestCase {
|
|||||||
remoteEndpointId: "peer-a",
|
remoteEndpointId: "peer-a",
|
||||||
totalSizeHint: 100
|
totalSizeHint: 100
|
||||||
)
|
)
|
||||||
XCTAssertEqual(progress?.kind, "completed")
|
XCTAssertEqual(progress?.kind, .completed)
|
||||||
XCTAssertEqual(progress?.labelKey, L10n.Progress.completed)
|
XCTAssertEqual(progress?.labelKey, L10n.Progress.completed)
|
||||||
XCTAssertEqual(progress?.progress, 1)
|
XCTAssertEqual(progress?.progress, 1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,56 @@ struct CoreEventModel: Equatable, Identifiable, Sendable {
|
|||||||
let timestamp: Int64
|
let timestamp: Int64
|
||||||
let scope: String
|
let scope: String
|
||||||
let transferId: UInt64?
|
let transferId: UInt64?
|
||||||
|
/// Raw wire values as emitted by the core. Interpret them through the typed
|
||||||
|
/// `eventDirection` / `eventPhase` / `eventKind` accessors below — logic code
|
||||||
|
/// should never compare these strings directly.
|
||||||
let direction: String?
|
let direction: String?
|
||||||
let phase: String
|
let phase: String
|
||||||
let kind: String
|
let kind: String
|
||||||
let dataJson: String
|
let dataJson: String
|
||||||
|
|
||||||
|
var eventDirection: EventDirection? { direction.flatMap(EventDirection.init(rawValue:)) }
|
||||||
|
var eventPhase: EventPhase? { EventPhase(rawValue: phase) }
|
||||||
|
var eventKind: EventKind? { EventKind(rawValue: kind) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Direction of a core event, matching the wire strings the core emits.
|
||||||
|
enum EventDirection: String, Equatable, Sendable {
|
||||||
|
case send
|
||||||
|
case receive
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Phase of a core progress event (the `phase` wire field).
|
||||||
|
enum EventPhase: String, Equatable, Sendable {
|
||||||
|
case importing = "import"
|
||||||
|
case ticket
|
||||||
|
case access
|
||||||
|
case transfer
|
||||||
|
case download
|
||||||
|
case export
|
||||||
|
case lifecycle
|
||||||
|
case network
|
||||||
|
case handshake
|
||||||
|
case error
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kind of a core progress event (the `kind` wire field).
|
||||||
|
enum EventKind: String, Equatable, Sendable {
|
||||||
|
case started
|
||||||
|
case copyProgress = "copy-progress"
|
||||||
|
case copyDone = "copy-done"
|
||||||
|
case outboardProgress = "outboard-progress"
|
||||||
|
case done
|
||||||
|
case created
|
||||||
|
case progress
|
||||||
|
case completed
|
||||||
|
case aborted
|
||||||
|
case failed
|
||||||
|
case connecting
|
||||||
|
case connected
|
||||||
|
case foundCollection = "found-collection"
|
||||||
|
case cancelled
|
||||||
|
case shareStopped = "share-stopped"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ShareAccessPolicy: Equatable, Sendable {
|
enum ShareAccessPolicy: Equatable, Sendable {
|
||||||
|
|||||||
@@ -298,14 +298,15 @@ private extension CoreEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let refreshPhases: Set<String> = ["lifecycle", "error", "ticket", "import", "download", "export", "handshake"]
|
private let refreshPhases: Set<EventPhase> = [.lifecycle, .error, .ticket, .importing, .download, .export, .handshake]
|
||||||
private let refreshKinds: Set<String> = [
|
private let refreshKinds: Set<EventKind> = [
|
||||||
"started", "done", "created", "failed", "cancelled", "share-stopped", "found-collection", "connected",
|
.started, .done, .created, .failed, .cancelled, .shareStopped, .foundCollection, .connected,
|
||||||
]
|
]
|
||||||
|
|
||||||
private extension CoreEventModel {
|
private extension CoreEventModel {
|
||||||
var shouldRefreshTransfers: Bool {
|
var shouldRefreshTransfers: Bool {
|
||||||
refreshPhases.contains(phase) && refreshKinds.contains(kind)
|
guard let eventPhase, let eventKind else { return false }
|
||||||
|
return refreshPhases.contains(eventPhase) && refreshKinds.contains(eventKind)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ func windowClassFor(width: Double) -> WindowClass {
|
|||||||
/// resolved at the view layer.
|
/// resolved at the view layer.
|
||||||
struct TransferProgress: Equatable {
|
struct TransferProgress: Equatable {
|
||||||
let transferId: UInt64?
|
let transferId: UInt64?
|
||||||
let phase: String
|
let phase: EventPhase
|
||||||
let kind: String
|
let kind: EventKind
|
||||||
let labelKey: String.LocalizationValue
|
let labelKey: String.LocalizationValue
|
||||||
let progress: Double?
|
let progress: Double?
|
||||||
var detail: String? = nil
|
var detail: String? = nil
|
||||||
@@ -40,32 +40,19 @@ func statusLabelKey(_ status: TransferStatus) -> String.LocalizationValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let progressPhases: Set<String> = [
|
/// Latest progress snapshot for a transfer. Events are newest-first. Only events
|
||||||
"import", "ticket", "access", "transfer", "download", "export",
|
/// whose `phase` and `kind` map to known cases participate.
|
||||||
"lifecycle", "network", "handshake", "error",
|
|
||||||
]
|
|
||||||
|
|
||||||
private let progressKinds: Set<String> = [
|
|
||||||
"started", "copy-progress", "copy-done", "outboard-progress", "done",
|
|
||||||
"created", "progress", "completed", "aborted", "failed",
|
|
||||||
"connecting", "connected", "found-collection",
|
|
||||||
"cancelled", "share-stopped",
|
|
||||||
]
|
|
||||||
|
|
||||||
/// Latest progress snapshot for a transfer. Events are newest-first.
|
|
||||||
func progressForTransfer(events: [CoreEventModel], transferId: UInt64) -> TransferProgress? {
|
func progressForTransfer(events: [CoreEventModel], transferId: UInt64) -> TransferProgress? {
|
||||||
let relevant = events.filter { event in
|
let relevant = events.filter { event in
|
||||||
event.transferId == transferId
|
event.transferId == transferId && event.eventPhase != nil && event.eventKind != nil
|
||||||
&& progressPhases.contains(event.phase)
|
|
||||||
&& progressKinds.contains(event.kind)
|
|
||||||
}
|
}
|
||||||
guard let latest = relevant.first else { return nil }
|
guard let latest = relevant.first, let phase = latest.eventPhase, let kind = latest.eventKind else { return nil }
|
||||||
let sizeHint = findKnownSize(events: events, transferId: transferId)
|
let sizeHint = findKnownSize(events: events, transferId: transferId)
|
||||||
return TransferProgress(
|
return TransferProgress(
|
||||||
transferId: transferId,
|
transferId: transferId,
|
||||||
phase: latest.phase,
|
phase: phase,
|
||||||
kind: latest.kind,
|
kind: kind,
|
||||||
labelKey: humanProgressLabel(latest),
|
labelKey: humanProgressLabel(phase: phase, kind: kind),
|
||||||
progress: parseProgress(latest.dataJson, sizeHint: sizeHint),
|
progress: parseProgress(latest.dataJson, sizeHint: sizeHint),
|
||||||
detail: progressDetail(latest)
|
detail: progressDetail(latest)
|
||||||
)
|
)
|
||||||
@@ -80,32 +67,31 @@ func progressForReceiver(
|
|||||||
) -> TransferProgress? {
|
) -> TransferProgress? {
|
||||||
if remoteEndpointId.isEmpty { return nil }
|
if remoteEndpointId.isEmpty { return nil }
|
||||||
let connectionIds = connectionIdsForEndpoint(events: events, remoteEndpointId: remoteEndpointId)
|
let connectionIds = connectionIdsForEndpoint(events: events, remoteEndpointId: remoteEndpointId)
|
||||||
|
let receiverKinds: Set<EventKind> = [.started, .progress, .completed, .aborted]
|
||||||
let transferEvents = events.filter { event in
|
let transferEvents = events.filter { event in
|
||||||
event.transferId == transferId
|
event.transferId == transferId
|
||||||
&& event.direction == "send"
|
&& event.eventDirection == .send
|
||||||
&& event.phase == "transfer"
|
&& event.eventPhase == .transfer
|
||||||
&& ["started", "progress", "completed", "aborted"].contains(event.kind)
|
&& (event.eventKind.map(receiverKinds.contains) ?? false)
|
||||||
&& eventBelongsToReceiver(event, remoteEndpointId: remoteEndpointId, connectionIds: connectionIds)
|
&& eventBelongsToReceiver(event, remoteEndpointId: remoteEndpointId, connectionIds: connectionIds)
|
||||||
}
|
}
|
||||||
if transferEvents.isEmpty { return nil }
|
guard let latest = transferEvents.first, let latestKind = latest.eventKind else { return nil }
|
||||||
|
if latestKind == .aborted {
|
||||||
let latest = transferEvents[0]
|
|
||||||
if latest.kind == "aborted" {
|
|
||||||
return TransferProgress(
|
return TransferProgress(
|
||||||
transferId: transferId, phase: "transfer", kind: "aborted",
|
transferId: transferId, phase: .transfer, kind: .aborted,
|
||||||
labelKey: L10n.Progress.interrupted, progress: nil, detail: nil
|
labelKey: L10n.Progress.interrupted, progress: nil, detail: nil
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if latest.kind == "completed" && !transferEvents.contains(where: { $0.kind == "progress" || $0.kind == "started" }) {
|
if latestKind == .completed && !transferEvents.contains(where: { $0.eventKind == .progress || $0.eventKind == .started }) {
|
||||||
return TransferProgress(
|
return TransferProgress(
|
||||||
transferId: transferId, phase: "transfer", kind: "completed",
|
transferId: transferId, phase: .transfer, kind: .completed,
|
||||||
labelKey: L10n.Progress.completed, progress: 1, detail: nil
|
labelKey: L10n.Progress.completed, progress: 1, detail: nil
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
let progress = aggregateReceiverProgress(events: transferEvents, totalSizeHint: totalSizeHint)
|
let progress = aggregateReceiverProgress(events: transferEvents, totalSizeHint: totalSizeHint)
|
||||||
|
|
||||||
return TransferProgress(
|
return TransferProgress(
|
||||||
transferId: transferId, phase: "transfer", kind: latest.kind,
|
transferId: transferId, phase: .transfer, kind: latestKind,
|
||||||
labelKey: L10n.Progress.sending, progress: progress, detail: progressDetail(latest)
|
labelKey: L10n.Progress.sending, progress: progress, detail: progressDetail(latest)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -127,25 +113,25 @@ func formatBytes(_ size: UInt64) -> String {
|
|||||||
|
|
||||||
// MARK: - Internals (ported literally from AppUiModels.kt)
|
// MARK: - Internals (ported literally from AppUiModels.kt)
|
||||||
|
|
||||||
private func humanProgressLabel(_ event: CoreEventModel) -> String.LocalizationValue {
|
private func humanProgressLabel(phase: EventPhase, kind: EventKind) -> String.LocalizationValue {
|
||||||
switch (event.phase, event.kind) {
|
switch (phase, kind) {
|
||||||
case ("import", "copy-progress"), ("import", "outboard-progress"), ("import", "started"):
|
case (.importing, .copyProgress), (.importing, .outboardProgress), (.importing, .started):
|
||||||
return L10n.Progress.preparing
|
return L10n.Progress.preparing
|
||||||
case ("import", "done"): return L10n.Progress.ready
|
case (.importing, .done): return L10n.Progress.ready
|
||||||
case ("ticket", "created"): return L10n.Progress.shareReady
|
case (.ticket, .created): return L10n.Progress.shareReady
|
||||||
case ("network", "connecting"): return L10n.Progress.connecting
|
case (.network, .connecting): return L10n.Progress.connecting
|
||||||
case ("network", "connected"): return L10n.Progress.connected
|
case (.network, .connected): return L10n.Progress.connected
|
||||||
case ("download", "found-collection"): return L10n.Progress.gettingReady
|
case (.download, .foundCollection): return L10n.Progress.gettingReady
|
||||||
case ("download", "progress"): return L10n.Progress.downloading
|
case (.download, .progress): return L10n.Progress.downloading
|
||||||
case ("export", "progress"): return L10n.Progress.saving
|
case (.export, .progress): return L10n.Progress.saving
|
||||||
case ("transfer", "progress"): return L10n.Progress.sending
|
case (.transfer, .progress): return L10n.Progress.sending
|
||||||
case ("transfer", "started"): return L10n.Progress.connected
|
case (.transfer, .started): return L10n.Progress.connected
|
||||||
case ("transfer", "completed"): return L10n.Progress.completed
|
case (.transfer, .completed): return L10n.Progress.completed
|
||||||
case ("lifecycle", "done"): return L10n.Progress.completed
|
case (.lifecycle, .done): return L10n.Progress.completed
|
||||||
case ("lifecycle", "cancelled"): return L10n.Progress.cancelled
|
case (.lifecycle, .cancelled): return L10n.Progress.cancelled
|
||||||
default:
|
default:
|
||||||
if event.phase == "handshake" { return L10n.Progress.requestingAccess }
|
if phase == .handshake { return L10n.Progress.requestingAccess }
|
||||||
if event.kind == "failed" { return L10n.Progress.failed }
|
if kind == .failed { return L10n.Progress.failed }
|
||||||
return L10n.Progress.working
|
return L10n.Progress.working
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,14 +203,14 @@ private func aggregateReceiverProgress(events: [CoreEventModel], totalSizeHint:
|
|||||||
order.append(requestKey)
|
order.append(requestKey)
|
||||||
}
|
}
|
||||||
if let size, size > 0 { state.size = size }
|
if let size, size > 0 { state.size = size }
|
||||||
switch event.kind {
|
switch event.eventKind {
|
||||||
case "progress", "started":
|
case .progress, .started:
|
||||||
if let endOffset { state.offset = max(state.offset, endOffset) }
|
if let endOffset { state.offset = max(state.offset, endOffset) }
|
||||||
state.aborted = false
|
state.aborted = false
|
||||||
case "completed":
|
case .completed:
|
||||||
state.completed = true
|
state.completed = true
|
||||||
if let s = state.size { state.offset = s }
|
if let s = state.size { state.offset = s }
|
||||||
case "aborted":
|
case .aborted:
|
||||||
state.aborted = true
|
state.aborted = true
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ struct InvitationReviewPanel: View {
|
|||||||
|
|
||||||
if state.isReceiving {
|
if state.isReceiving {
|
||||||
let progressId = state.activeReceiveTransferId
|
let progressId = state.activeReceiveTransferId
|
||||||
?? model.coreState.events.first { $0.direction == "receive" && $0.transferId != nil }?.transferId
|
?? model.coreState.events.first { $0.eventDirection == .receive && $0.transferId != nil }?.transferId
|
||||||
let progress = progressId.flatMap { progressForTransfer(events: model.coreState.events, transferId: $0) }
|
let progress = progressId.flatMap { progressForTransfer(events: model.coreState.events, transferId: $0) }
|
||||||
ProgressRow(labelKey: progress?.labelKey ?? L10n.Progress.receiving, progress: progress?.progress, detail: progress?.detail)
|
ProgressRow(labelKey: progress?.labelKey ?? L10n.Progress.receiving, progress: progress?.progress, detail: progress?.detail)
|
||||||
SecondaryButton(title: String(localized: L10n.Button.cancelReceive), action: model.cancelActiveReceive)
|
SecondaryButton(title: String(localized: L10n.Button.cancelReceive), action: model.cancelActiveReceive)
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ final class ReceiveModel: ObservableObject {
|
|||||||
func cancelActiveReceive() {
|
func cancelActiveReceive() {
|
||||||
let transferId = state.activeReceiveTransferId
|
let transferId = state.activeReceiveTransferId
|
||||||
?? coreState.transfers.first { $0.direction == .receive && $0.status == .receiving }?.transferId
|
?? coreState.transfers.first { $0.direction == .receive && $0.status == .receiving }?.transferId
|
||||||
?? coreState.events.first { $0.direction == "receive" && $0.transferId != nil }?.transferId
|
?? coreState.events.first { $0.eventDirection == .receive && $0.transferId != nil }?.transferId
|
||||||
guard let transferId else { return }
|
guard let transferId else { return }
|
||||||
Task {
|
Task {
|
||||||
let result = await repository.cancel(transferId: transferId)
|
let result = await repository.cancel(transferId: transferId)
|
||||||
|
|||||||
@@ -134,10 +134,10 @@ struct SendScreen: View {
|
|||||||
}
|
}
|
||||||
let combined = fractions.isEmpty ? nil : fractions.reduce(0, +) / Double(fractions.count)
|
let combined = fractions.isEmpty ? nil : fractions.reduce(0, +) / Double(fractions.count)
|
||||||
if active.count == 1 {
|
if active.count == 1 {
|
||||||
return TransferProgress(transferId: transfer.transferId, phase: "transfer", kind: "progress",
|
return TransferProgress(transferId: transfer.transferId, phase: .transfer, kind: .progress,
|
||||||
labelKey: L10n.Progress.sending, progress: combined)
|
labelKey: L10n.Progress.sending, progress: combined)
|
||||||
}
|
}
|
||||||
return TransferProgress(transferId: transfer.transferId, phase: "transfer", kind: "progress",
|
return TransferProgress(transferId: transfer.transferId, phase: .transfer, kind: .progress,
|
||||||
labelKey: L10n.Progress.sending, progress: combined,
|
labelKey: L10n.Progress.sending, progress: combined,
|
||||||
label: L10n.Progress.sendingToCount(count: active.count))
|
label: L10n.Progress.sendingToCount(count: active.count))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user