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",
|
||||
totalSizeHint: 100
|
||||
)
|
||||
XCTAssertEqual(progress?.kind, "completed")
|
||||
XCTAssertEqual(progress?.kind, .completed)
|
||||
XCTAssertEqual(progress?.labelKey, L10n.Progress.completed)
|
||||
XCTAssertEqual(progress?.progress, 1)
|
||||
}
|
||||
|
||||
@@ -15,10 +15,56 @@ struct CoreEventModel: Equatable, Identifiable, Sendable {
|
||||
let timestamp: Int64
|
||||
let scope: String
|
||||
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 phase: String
|
||||
let kind: 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 {
|
||||
|
||||
@@ -298,14 +298,15 @@ private extension CoreEvent {
|
||||
}
|
||||
}
|
||||
|
||||
private let refreshPhases: Set<String> = ["lifecycle", "error", "ticket", "import", "download", "export", "handshake"]
|
||||
private let refreshKinds: Set<String> = [
|
||||
"started", "done", "created", "failed", "cancelled", "share-stopped", "found-collection", "connected",
|
||||
private let refreshPhases: Set<EventPhase> = [.lifecycle, .error, .ticket, .importing, .download, .export, .handshake]
|
||||
private let refreshKinds: Set<EventKind> = [
|
||||
.started, .done, .created, .failed, .cancelled, .shareStopped, .foundCollection, .connected,
|
||||
]
|
||||
|
||||
private extension CoreEventModel {
|
||||
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.
|
||||
struct TransferProgress: Equatable {
|
||||
let transferId: UInt64?
|
||||
let phase: String
|
||||
let kind: String
|
||||
let phase: EventPhase
|
||||
let kind: EventKind
|
||||
let labelKey: String.LocalizationValue
|
||||
let progress: Double?
|
||||
var detail: String? = nil
|
||||
@@ -40,32 +40,19 @@ func statusLabelKey(_ status: TransferStatus) -> String.LocalizationValue {
|
||||
}
|
||||
}
|
||||
|
||||
private let progressPhases: Set<String> = [
|
||||
"import", "ticket", "access", "transfer", "download", "export",
|
||||
"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.
|
||||
/// Latest progress snapshot for a transfer. Events are newest-first. Only events
|
||||
/// whose `phase` and `kind` map to known cases participate.
|
||||
func progressForTransfer(events: [CoreEventModel], transferId: UInt64) -> TransferProgress? {
|
||||
let relevant = events.filter { event in
|
||||
event.transferId == transferId
|
||||
&& progressPhases.contains(event.phase)
|
||||
&& progressKinds.contains(event.kind)
|
||||
event.transferId == transferId && event.eventPhase != nil && event.eventKind != nil
|
||||
}
|
||||
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)
|
||||
return TransferProgress(
|
||||
transferId: transferId,
|
||||
phase: latest.phase,
|
||||
kind: latest.kind,
|
||||
labelKey: humanProgressLabel(latest),
|
||||
phase: phase,
|
||||
kind: kind,
|
||||
labelKey: humanProgressLabel(phase: phase, kind: kind),
|
||||
progress: parseProgress(latest.dataJson, sizeHint: sizeHint),
|
||||
detail: progressDetail(latest)
|
||||
)
|
||||
@@ -80,32 +67,31 @@ func progressForReceiver(
|
||||
) -> TransferProgress? {
|
||||
if remoteEndpointId.isEmpty { return nil }
|
||||
let connectionIds = connectionIdsForEndpoint(events: events, remoteEndpointId: remoteEndpointId)
|
||||
let receiverKinds: Set<EventKind> = [.started, .progress, .completed, .aborted]
|
||||
let transferEvents = events.filter { event in
|
||||
event.transferId == transferId
|
||||
&& event.direction == "send"
|
||||
&& event.phase == "transfer"
|
||||
&& ["started", "progress", "completed", "aborted"].contains(event.kind)
|
||||
&& event.eventDirection == .send
|
||||
&& event.eventPhase == .transfer
|
||||
&& (event.eventKind.map(receiverKinds.contains) ?? false)
|
||||
&& eventBelongsToReceiver(event, remoteEndpointId: remoteEndpointId, connectionIds: connectionIds)
|
||||
}
|
||||
if transferEvents.isEmpty { return nil }
|
||||
|
||||
let latest = transferEvents[0]
|
||||
if latest.kind == "aborted" {
|
||||
guard let latest = transferEvents.first, let latestKind = latest.eventKind else { return nil }
|
||||
if latestKind == .aborted {
|
||||
return TransferProgress(
|
||||
transferId: transferId, phase: "transfer", kind: "aborted",
|
||||
transferId: transferId, phase: .transfer, kind: .aborted,
|
||||
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(
|
||||
transferId: transferId, phase: "transfer", kind: "completed",
|
||||
transferId: transferId, phase: .transfer, kind: .completed,
|
||||
labelKey: L10n.Progress.completed, progress: 1, detail: nil
|
||||
)
|
||||
}
|
||||
let progress = aggregateReceiverProgress(events: transferEvents, totalSizeHint: totalSizeHint)
|
||||
|
||||
return TransferProgress(
|
||||
transferId: transferId, phase: "transfer", kind: latest.kind,
|
||||
transferId: transferId, phase: .transfer, kind: latestKind,
|
||||
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)
|
||||
|
||||
private func humanProgressLabel(_ event: CoreEventModel) -> String.LocalizationValue {
|
||||
switch (event.phase, event.kind) {
|
||||
case ("import", "copy-progress"), ("import", "outboard-progress"), ("import", "started"):
|
||||
private func humanProgressLabel(phase: EventPhase, kind: EventKind) -> String.LocalizationValue {
|
||||
switch (phase, kind) {
|
||||
case (.importing, .copyProgress), (.importing, .outboardProgress), (.importing, .started):
|
||||
return L10n.Progress.preparing
|
||||
case ("import", "done"): return L10n.Progress.ready
|
||||
case ("ticket", "created"): return L10n.Progress.shareReady
|
||||
case ("network", "connecting"): return L10n.Progress.connecting
|
||||
case ("network", "connected"): return L10n.Progress.connected
|
||||
case ("download", "found-collection"): return L10n.Progress.gettingReady
|
||||
case ("download", "progress"): return L10n.Progress.downloading
|
||||
case ("export", "progress"): return L10n.Progress.saving
|
||||
case ("transfer", "progress"): return L10n.Progress.sending
|
||||
case ("transfer", "started"): return L10n.Progress.connected
|
||||
case ("transfer", "completed"): return L10n.Progress.completed
|
||||
case ("lifecycle", "done"): return L10n.Progress.completed
|
||||
case ("lifecycle", "cancelled"): return L10n.Progress.cancelled
|
||||
case (.importing, .done): return L10n.Progress.ready
|
||||
case (.ticket, .created): return L10n.Progress.shareReady
|
||||
case (.network, .connecting): return L10n.Progress.connecting
|
||||
case (.network, .connected): return L10n.Progress.connected
|
||||
case (.download, .foundCollection): return L10n.Progress.gettingReady
|
||||
case (.download, .progress): return L10n.Progress.downloading
|
||||
case (.export, .progress): return L10n.Progress.saving
|
||||
case (.transfer, .progress): return L10n.Progress.sending
|
||||
case (.transfer, .started): return L10n.Progress.connected
|
||||
case (.transfer, .completed): return L10n.Progress.completed
|
||||
case (.lifecycle, .done): return L10n.Progress.completed
|
||||
case (.lifecycle, .cancelled): return L10n.Progress.cancelled
|
||||
default:
|
||||
if event.phase == "handshake" { return L10n.Progress.requestingAccess }
|
||||
if event.kind == "failed" { return L10n.Progress.failed }
|
||||
if phase == .handshake { return L10n.Progress.requestingAccess }
|
||||
if kind == .failed { return L10n.Progress.failed }
|
||||
return L10n.Progress.working
|
||||
}
|
||||
}
|
||||
@@ -217,14 +203,14 @@ private func aggregateReceiverProgress(events: [CoreEventModel], totalSizeHint:
|
||||
order.append(requestKey)
|
||||
}
|
||||
if let size, size > 0 { state.size = size }
|
||||
switch event.kind {
|
||||
case "progress", "started":
|
||||
switch event.eventKind {
|
||||
case .progress, .started:
|
||||
if let endOffset { state.offset = max(state.offset, endOffset) }
|
||||
state.aborted = false
|
||||
case "completed":
|
||||
case .completed:
|
||||
state.completed = true
|
||||
if let s = state.size { state.offset = s }
|
||||
case "aborted":
|
||||
case .aborted:
|
||||
state.aborted = true
|
||||
default:
|
||||
break
|
||||
|
||||
@@ -129,7 +129,7 @@ struct InvitationReviewPanel: View {
|
||||
|
||||
if state.isReceiving {
|
||||
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) }
|
||||
ProgressRow(labelKey: progress?.labelKey ?? L10n.Progress.receiving, progress: progress?.progress, detail: progress?.detail)
|
||||
SecondaryButton(title: String(localized: L10n.Button.cancelReceive), action: model.cancelActiveReceive)
|
||||
|
||||
@@ -202,7 +202,7 @@ final class ReceiveModel: ObservableObject {
|
||||
func cancelActiveReceive() {
|
||||
let transferId = state.activeReceiveTransferId
|
||||
?? 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 }
|
||||
Task {
|
||||
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)
|
||||
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)
|
||||
}
|
||||
return TransferProgress(transferId: transfer.transferId, phase: "transfer", kind: "progress",
|
||||
return TransferProgress(transferId: transfer.transferId, phase: .transfer, kind: .progress,
|
||||
labelKey: L10n.Progress.sending, progress: combined,
|
||||
label: L10n.Progress.sendingToCount(count: active.count))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user