mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
feat(apple): add context menus to Send and Receive rows
Send rows get a context menu that acts inline without navigating: Share opens the share panel (QR + delivery) over the list via a dedicated sheet host, Stop sharing (active shares) and Delete transfer run in place, the latter through a new id-based SendModel.deleteTransfer and a list-level confirmation alert. Receive rows get a Delete action mirroring swipe-to-delete (handy on macOS).
This commit is contained in:
@@ -79,6 +79,15 @@ struct ReceiveScreen: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.contextMenu {
|
||||||
|
if transfer.status.isTerminalReceiveHistory {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
model.requestDeleteHistoryItem(transfer.transferId)
|
||||||
|
} label: {
|
||||||
|
Label(String(localized: L10n.Button.deleteTransfer), systemSymbol: .trash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} header: {
|
} header: {
|
||||||
Text(String(localized: L10n.Receive.historyTitle))
|
Text(String(localized: L10n.Receive.historyTitle))
|
||||||
|
|||||||
@@ -228,6 +228,31 @@ final class SendModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deletes a transfer by id, independent of the detail selection — used by the
|
||||||
|
/// list context menu so it can act inline without navigating into the detail.
|
||||||
|
func deleteTransfer(id: UInt64) {
|
||||||
|
if state.isDeleting { return }
|
||||||
|
state.isDeleting = true
|
||||||
|
Task {
|
||||||
|
let result = await repository.delete(transferId: id)
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
filePreviewRepository.remove(transferId: id)
|
||||||
|
if state.selectedTransferId == id {
|
||||||
|
state.selectedTransferId = nil
|
||||||
|
state.detailPanel = nil
|
||||||
|
state.receiverHistory = []
|
||||||
|
}
|
||||||
|
state.isDeleting = false
|
||||||
|
_ = await repository.refresh()
|
||||||
|
messages.tryShow(UiMessage(text: .resource(L10n.Transfer.deleted), tone: .success))
|
||||||
|
case .failure(let error):
|
||||||
|
state.isDeleting = false
|
||||||
|
messages.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Cancels/refuses a single receiver by responding to its request negatively.
|
/// Cancels/refuses a single receiver by responding to its request negatively.
|
||||||
/// Uses the core's `respondReceiverRequest` (no backend change); applies to
|
/// Uses the core's `respondReceiverRequest` (no backend change); applies to
|
||||||
/// receivers that are still pending or accepted.
|
/// receivers that are still pending or accepted.
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ struct SendScreen: View {
|
|||||||
@ObservedObject var model: SendModel
|
@ObservedObject var model: SendModel
|
||||||
let windowClass: WindowClass
|
let windowClass: WindowClass
|
||||||
|
|
||||||
|
/// Transfer whose share panel is presented inline from the list context menu.
|
||||||
|
@State private var shareTarget: Transfer?
|
||||||
|
/// Transfer pending an inline (list-level) delete confirmation.
|
||||||
|
@State private var deleteTarget: Transfer?
|
||||||
|
|
||||||
private var outgoing: [Transfer] {
|
private var outgoing: [Transfer] {
|
||||||
model.coreState.transfers.filter { $0.direction == .send }
|
model.coreState.transfers.filter { $0.direction == .send }
|
||||||
}
|
}
|
||||||
@@ -41,6 +46,18 @@ struct SendScreen: View {
|
|||||||
detailView(for: transfer)
|
detailView(for: transfer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Attached inside the NavigationStack (a different sheet host than the
|
||||||
|
// composer drawer on the outer body, so the two don't clash). Opens the
|
||||||
|
// share panel over the list without navigating into the transfer detail.
|
||||||
|
.adaptiveDrawer(
|
||||||
|
isPresented: Binding(get: { shareTarget != nil }, set: { if !$0 { shareTarget = nil } }),
|
||||||
|
windowClass: windowClass,
|
||||||
|
onDismiss: { shareTarget = nil }
|
||||||
|
) {
|
||||||
|
if let shareTarget {
|
||||||
|
TransferSharePanel(model: model, transfer: shareTarget)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.adaptiveDrawer(
|
.adaptiveDrawer(
|
||||||
isPresented: Binding(get: { model.state.isComposerOpen }, set: { _ in }),
|
isPresented: Binding(get: { model.state.isComposerOpen }, set: { _ in }),
|
||||||
@@ -49,7 +66,23 @@ struct SendScreen: View {
|
|||||||
) {
|
) {
|
||||||
TransferComposer(model: model, windowClass: windowClass)
|
TransferComposer(model: model, windowClass: windowClass)
|
||||||
}
|
}
|
||||||
|
.alert(
|
||||||
|
Text(String(localized: L10n.Transfer.deleteTitle)),
|
||||||
|
isPresented: Binding(get: { deleteTarget != nil }, set: { if !$0 { deleteTarget = nil } })
|
||||||
|
) {
|
||||||
|
Button(String(localized: L10n.Button.cancel), role: .cancel) { deleteTarget = nil }
|
||||||
|
Button(String(localized: L10n.Button.deleteTransfer), role: .destructive) {
|
||||||
|
if let target = deleteTarget { model.deleteTransfer(id: target.transferId) }
|
||||||
|
deleteTarget = nil
|
||||||
}
|
}
|
||||||
|
} message: {
|
||||||
|
if let target = deleteTarget {
|
||||||
|
Text(L10n.Transfer.deleteDescription(
|
||||||
|
transferName: target.transferName ?? String(localized: L10n.Send.newTransferTitle)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// The pushed transfer details view, with its detail-panel sheet and delete
|
/// The pushed transfer details view, with its detail-panel sheet and delete
|
||||||
/// alert attached here so they present from the detail's own context (presenting
|
/// alert attached here so they present from the detail's own context (presenting
|
||||||
@@ -91,6 +124,28 @@ struct SendScreen: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
.contextMenu {
|
||||||
|
if transfer.ticket != nil {
|
||||||
|
Button {
|
||||||
|
shareTarget = transfer
|
||||||
|
} label: {
|
||||||
|
Label(String(localized: L10n.Transfer.shareTitle), systemSymbol: .squareAndArrowUp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if transfer.status == .sharing {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
model.stopSharing(transferId: transfer.transferId)
|
||||||
|
} label: {
|
||||||
|
Label(String(localized: L10n.Send.stopSharing), systemSymbol: .stopCircle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
|
Button(role: .destructive) {
|
||||||
|
deleteTarget = transfer
|
||||||
|
} label: {
|
||||||
|
Label(String(localized: L10n.Button.deleteTransfer), systemSymbol: .trash)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} header: {
|
} header: {
|
||||||
Text(String(localized: L10n.Send.transfersTitle))
|
Text(String(localized: L10n.Send.transfersTitle))
|
||||||
|
|||||||
Reference in New Issue
Block a user