mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
feat(apple): transfer controls, progress fixes, and project config
- Sender progress: aggregate only in-flight receivers so the bar clears on
completion and is order-independent ("Sending to N")
- Add Stop sharing and per-receiver Refuse (pending requests) on the sender
- Fix macOS: raise approval modal above the Share sheet; drive foreground
state off NSApplication so background notifications fire
- Persist app identity (display name, category) and signing team via
Info.plist / project.yml / gitignored Local.xcconfig
This commit is contained in:
3
apple/.gitignore
vendored
3
apple/.gitignore
vendored
@@ -6,6 +6,9 @@ VnidropCore/Sources/VnidropCore/Vnidrop.swift
|
||||
# Generated by XcodeGen from project.yml
|
||||
VniDrop.xcodeproj/
|
||||
|
||||
# Per-developer signing (team id); Signing.xcconfig optionally includes it
|
||||
Local.xcconfig
|
||||
|
||||
# SwiftPM / Xcode
|
||||
.build/
|
||||
.swiftpm/
|
||||
|
||||
9
apple/Signing.xcconfig
Normal file
9
apple/Signing.xcconfig
Normal file
@@ -0,0 +1,9 @@
|
||||
// Committed signing config. Contains no secrets.
|
||||
//
|
||||
// Per-developer signing (e.g. DEVELOPMENT_TEAM) goes in Local.xcconfig, which is
|
||||
// gitignored. The optional include below means the build still works for anyone
|
||||
// who doesn't have a Local.xcconfig — Xcode automatic signing fills in their team.
|
||||
//
|
||||
// To persist your team across `xcodegen generate`, create apple/Local.xcconfig:
|
||||
// DEVELOPMENT_TEAM = XXXXXXXXXX
|
||||
#include? "Local.xcconfig"
|
||||
@@ -82,6 +82,25 @@ struct RootView: View {
|
||||
break
|
||||
}
|
||||
}
|
||||
// A pending approval is a blocking modal; close the sender's detail panel
|
||||
// (e.g. the Share/QR sheet) so the approval sheet isn't presented under it
|
||||
// on macOS.
|
||||
.onChange(of: approvals.state.current?.id) { id in
|
||||
if id != nil { sendModel.closeDetailPanel() }
|
||||
}
|
||||
#if os(macOS)
|
||||
// macOS keeps `scenePhase == .active` even when the app loses focus, so
|
||||
// drive foreground/background off NSApplication's active state instead —
|
||||
// otherwise notifications (only posted when unfocused) never fire.
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didResignActiveNotification)) { _ in
|
||||
graph.visibility.setForeground(false)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
||||
graph.visibility.setForeground(true)
|
||||
settingsModel.refreshNotificationPermission()
|
||||
Task { _ = await graph.coreRepository.refresh() }
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// iOS uses a bottom tab bar; macOS uses a native source-list sidebar so each
|
||||
|
||||
@@ -23,6 +23,9 @@ struct TransferProgress: Equatable {
|
||||
let labelKey: String
|
||||
let progress: Double?
|
||||
var detail: String? = nil
|
||||
/// Pre-resolved label that overrides `labelKey` when set (e.g. "Sending to 2",
|
||||
/// which needs a runtime count).
|
||||
var label: String? = nil
|
||||
}
|
||||
|
||||
func statusLabelKey(_ status: TransferStatus) -> String {
|
||||
@@ -107,8 +110,11 @@ func progressForReceiver(
|
||||
)
|
||||
}
|
||||
|
||||
/// Best send-side progress for a transfer (any receiver).
|
||||
func activeSendProgress(
|
||||
/// Send-side progress summary for a transfer's list row. Aggregates only the
|
||||
/// receivers that are *currently downloading* (in-flight), so the bar disappears
|
||||
/// once every receiver has completed/aborted, and its value is deterministic
|
||||
/// (order-independent) rather than "whichever receiver's event arrived last".
|
||||
func sendProgressSummary(
|
||||
events: [CoreEventModel],
|
||||
transferId: UInt64,
|
||||
totalSizeHint: UInt64? = nil
|
||||
@@ -118,23 +124,53 @@ func activeSendProgress(
|
||||
.compactMap { findString($0.dataJson, key: "endpoint_id") }
|
||||
.reduce(into: [String]()) { acc, id in if !acc.contains(id) { acc.append(id) } }
|
||||
|
||||
// No per-endpoint attribution: fall back to the single connection-scoped
|
||||
// stream, hidden once it completes/aborts.
|
||||
if endpointIds.isEmpty {
|
||||
let relevant = events.filter {
|
||||
$0.transferId == transferId && $0.direction == "send" && $0.phase == "transfer"
|
||||
&& ["started", "progress"].contains($0.kind)
|
||||
&& ["started", "progress", "completed", "aborted"].contains($0.kind)
|
||||
}
|
||||
guard let first = relevant.first else { return nil }
|
||||
guard let latest = relevant.first else { return nil }
|
||||
if latest.kind == "completed" || latest.kind == "aborted" { return nil }
|
||||
let live = relevant.filter { ["started", "progress"].contains($0.kind) }
|
||||
return TransferProgress(
|
||||
transferId: transferId, phase: "transfer", kind: first.kind,
|
||||
transferId: transferId, phase: "transfer", kind: latest.kind,
|
||||
labelKey: "progress_sending",
|
||||
progress: aggregateReceiverProgress(events: relevant, totalSizeHint: totalSizeHint),
|
||||
detail: progressDetail(first)
|
||||
progress: aggregateReceiverProgress(events: live, totalSizeHint: totalSizeHint),
|
||||
detail: progressDetail(latest)
|
||||
)
|
||||
}
|
||||
let all = endpointIds.compactMap {
|
||||
progressForReceiver(events: events, transferId: transferId, remoteEndpointId: $0, totalSizeHint: totalSizeHint)
|
||||
|
||||
// Keep only receivers still in flight (latest state started/progress).
|
||||
var activeCount = 0
|
||||
var fractions: [Double] = []
|
||||
var lastActive: TransferProgress?
|
||||
for id in endpointIds {
|
||||
guard let p = progressForReceiver(
|
||||
events: events, transferId: transferId, remoteEndpointId: id, totalSizeHint: totalSizeHint
|
||||
) else { continue }
|
||||
guard p.kind == "progress" || p.kind == "started" else { continue }
|
||||
activeCount += 1
|
||||
lastActive = p
|
||||
if let fraction = p.progress { fractions.append(fraction) }
|
||||
}
|
||||
return all.first { $0.kind == "progress" || $0.kind == "started" } ?? all.first
|
||||
if activeCount == 0 { return nil }
|
||||
|
||||
// Same total per receiver, so the byte-summed progress is the mean of the
|
||||
// per-receiver fractions.
|
||||
let combined = fractions.isEmpty ? nil : fractions.reduce(0, +) / Double(fractions.count)
|
||||
if activeCount == 1 {
|
||||
return TransferProgress(
|
||||
transferId: transferId, phase: "transfer", kind: "progress",
|
||||
labelKey: "progress_sending", progress: combined, detail: lastActive?.detail
|
||||
)
|
||||
}
|
||||
return TransferProgress(
|
||||
transferId: transferId, phase: "transfer", kind: "progress",
|
||||
labelKey: "progress_sending", progress: combined,
|
||||
label: String(format: String(localized: "progress_sending_to_count"), activeCount)
|
||||
)
|
||||
}
|
||||
|
||||
func formatBytes(_ size: UInt64) -> String {
|
||||
|
||||
@@ -205,6 +205,37 @@ final class SendModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancels/refuses a single receiver by responding to its request negatively.
|
||||
/// Uses the core's `respondReceiverRequest` (no backend change); applies to
|
||||
/// receivers that are still pending or accepted.
|
||||
func cancelReceiver(requestId: String) {
|
||||
Task {
|
||||
let result = await repository.respondReceiverRequest(requestId: requestId, accepted: false, reason: nil)
|
||||
switch result {
|
||||
case .success:
|
||||
if let transferId = state.selectedTransferId { refreshReceivers(transferId) }
|
||||
_ = await repository.refresh()
|
||||
case .failure(let error):
|
||||
messages.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops an active outgoing share (interrupts any in-flight receivers). The
|
||||
/// transfer stays in history as "Stopped". Uses the core's `cancelTransfer`.
|
||||
func stopSharing(transferId: UInt64) {
|
||||
Task {
|
||||
let result = await repository.cancel(transferId: transferId)
|
||||
switch result {
|
||||
case .success:
|
||||
_ = await repository.refresh()
|
||||
messages.tryShow(UiMessage(text: .resource("transfer_event_stopped"), tone: .info))
|
||||
case .failure(let error):
|
||||
messages.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Invitation results / share
|
||||
|
||||
func onInvitationResult(_ action: InvitationAction, _ result: Result<Void, Error>) {
|
||||
|
||||
@@ -116,7 +116,7 @@ struct SendScreen: View {
|
||||
private func progress(for transfer: Transfer) -> TransferProgress? {
|
||||
switch transfer.status {
|
||||
case .importing: return progressForTransfer(events: model.coreState.events, transferId: transfer.transferId)
|
||||
case .sharing: return activeSendProgress(events: model.coreState.events, transferId: transfer.transferId, totalSizeHint: transfer.totalSize)
|
||||
case .sharing: return sendProgressSummary(events: model.coreState.events, transferId: transfer.transferId, totalSizeHint: transfer.totalSize)
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
@@ -142,7 +142,7 @@ private struct TransferListItem: View {
|
||||
Text("\(formatBytes(transfer.totalSize)) · \(accessPolicyLabel(transfer.accessPolicy))")
|
||||
.font(.caption).foregroundStyle(.secondary).lineLimit(1)
|
||||
if let progress, transfer.status == .importing || transfer.status == .sharing {
|
||||
ProgressRow(labelKey: progress.labelKey, progress: progress.progress, detail: progress.detail)
|
||||
ProgressRow(labelKey: progress.labelKey, progress: progress.progress, detail: progress.detail, labelText: progress.label)
|
||||
.padding(.top, 2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@ struct TransferDetailsView: View {
|
||||
@ObservedObject var model: SendModel
|
||||
let transfer: Transfer
|
||||
let events: [CoreEventModel]
|
||||
@State private var showStopConfirmation = false
|
||||
|
||||
private var isActiveShare: Bool {
|
||||
transfer.status == .sharing || transfer.status == .importing
|
||||
}
|
||||
|
||||
private var pendingReceivers: Int {
|
||||
model.state.receiverHistory.filter { $0.status == .requested || $0.status == .accepted }.count
|
||||
@@ -45,6 +50,16 @@ struct TransferDetailsView: View {
|
||||
onTap: model.openShare
|
||||
)
|
||||
}
|
||||
|
||||
if isActiveShare {
|
||||
Section {
|
||||
Button(role: .destructive) {
|
||||
showStopConfirmation = true
|
||||
} label: {
|
||||
Label(String(localized: "send_stop_sharing"), systemImage: "stop.circle")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.navigationTitle(Text(LocalizedStringKey("send_transfer_details_title")))
|
||||
@@ -58,6 +73,18 @@ struct TransferDetailsView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.confirmationDialog(
|
||||
Text(LocalizedStringKey("send_stop_sharing")),
|
||||
isPresented: $showStopConfirmation,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button(String(localized: "send_stop_sharing"), role: .destructive) {
|
||||
model.stopSharing(transferId: transfer.transferId)
|
||||
}
|
||||
Button(String(localized: "button_cancel"), role: .cancel) {}
|
||||
} message: {
|
||||
Text(LocalizedStringKey("send_stop_sharing_description"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +140,8 @@ struct DetailPanelContent: View {
|
||||
receivers: model.state.receiverHistory,
|
||||
loading: model.state.isLoadingReceivers,
|
||||
events: model.coreState.events,
|
||||
transferTotalSize: transfer.totalSize
|
||||
transferTotalSize: transfer.totalSize,
|
||||
onCancel: model.cancelReceiver
|
||||
)
|
||||
case .share:
|
||||
TransferSharePanel(model: model, transfer: transfer)
|
||||
@@ -163,6 +191,7 @@ struct ReceiverHistoryPanel: View {
|
||||
let loading: Bool
|
||||
let events: [CoreEventModel]
|
||||
let transferTotalSize: UInt64
|
||||
let onCancel: (String) -> Void
|
||||
|
||||
var body: some View {
|
||||
PanelContainer(title: String(localized: "transfer_receivers_title")) {
|
||||
@@ -173,7 +202,7 @@ struct ReceiverHistoryPanel: View {
|
||||
} else {
|
||||
ForEach(Array(receivers.enumerated()), id: \.element.id) { index, receiver in
|
||||
if index > 0 { Divider().overlay(colors.borderDefault) }
|
||||
ReceiverRow(receiver: receiver, sendProgress: sendProgress(for: receiver))
|
||||
ReceiverRow(receiver: receiver, sendProgress: sendProgress(for: receiver), onCancel: onCancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,18 +222,27 @@ private struct ReceiverRow: View {
|
||||
@Environment(\.vniColors) private var colors
|
||||
let receiver: ReceiverRequestModel
|
||||
let sendProgress: TransferProgress?
|
||||
let onCancel: (String) -> Void
|
||||
|
||||
/// Only pending requests can be cancelled per-receiver: the core rejects a
|
||||
/// negative response to an already-accepted request ("...not approved, or it
|
||||
/// was refused"). Interrupting an in-flight receiver needs Stop sharing.
|
||||
private var isCancelable: Bool {
|
||||
receiver.status == .requested
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let name = receiver.receiverName ?? receiver.receiverDeviceName ?? String(localized: "transfer_nearby_device")
|
||||
let showLive = sendProgress != nil && receiver.status != .completed
|
||||
&& receiver.status != .refused && receiver.status != .expired
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(name).font(VniType.bodyLarge).lineLimit(1)
|
||||
if let deviceName = receiver.receiverDeviceName, deviceName != name {
|
||||
Text(deviceName).font(VniType.bodySmall).foregroundStyle(colors.foregroundLighter)
|
||||
}
|
||||
if showLive, let sendProgress {
|
||||
ProgressRow(labelKey: sendProgress.labelKey, progress: sendProgress.progress, detail: sendProgress.detail)
|
||||
ProgressRow(labelKey: sendProgress.labelKey, progress: sendProgress.progress, detail: sendProgress.detail, labelText: sendProgress.label)
|
||||
} else {
|
||||
Text(LocalizedStringKey(receiver.status.statusTextKey))
|
||||
.font(VniType.bodySmall).fontWeight(.medium)
|
||||
@@ -215,6 +253,18 @@ private struct ReceiverRow: View {
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
if isCancelable {
|
||||
Button(role: .destructive) {
|
||||
onCancel(receiver.id)
|
||||
} label: {
|
||||
Text(LocalizedStringKey("button_refuse"))
|
||||
.font(VniType.bodySmall)
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.tint(.red)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.vertical, 13)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,26 +2,12 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>VniDrop</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
@@ -37,6 +23,56 @@
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.utilities</string>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>NFCReaderUsageDescription</key>
|
||||
<string>VniDrop uses NFC to read transfer invitation tags.</string>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string></string>
|
||||
</array>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>VniDrop uses the camera to scan transfer QR codes.</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>VniDrop needs local network access to send to other local devices if needed.</string>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
<string>processing</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
@@ -59,27 +95,5 @@
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
<string>processing</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>VniDrop uses the camera to scan transfer QR codes.</string>
|
||||
<key>NFCReaderUsageDescription</key>
|
||||
<string>VniDrop uses NFC to read transfer invitation tags.</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>VniDrop needs local network access to send to other local devices if needed.</string>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string></string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1185,6 +1185,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"progress_sending_to_count" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Sending to %lld"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"progress_share_ready" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -1627,6 +1637,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"send_stop_sharing" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Stop sharing"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"send_stop_sharing_description" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "This stops the transfer and interrupts anyone currently downloading it. It stays in your history as Stopped."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"send_title" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
|
||||
@@ -33,11 +33,13 @@ struct ProgressRow: View {
|
||||
let labelKey: String
|
||||
let progress: Double?
|
||||
var detail: String? = nil
|
||||
/// Pre-resolved label; when set it overrides `labelKey`.
|
||||
var labelText: String? = nil
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text(LocalizedStringKey(labelKey)).font(.subheadline).lineLimit(1)
|
||||
label.font(.subheadline).lineLimit(1)
|
||||
Spacer()
|
||||
if let progress {
|
||||
Text("\(Int(progress * 100))%").font(.caption).foregroundStyle(.secondary)
|
||||
@@ -54,6 +56,15 @@ struct ProgressRow: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var label: some View {
|
||||
if let labelText {
|
||||
Text(labelText)
|
||||
} else {
|
||||
Text(LocalizedStringKey(labelKey))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Field
|
||||
|
||||
@@ -17,6 +17,9 @@ targets:
|
||||
VniDrop:
|
||||
type: application
|
||||
supportedDestinations: [iOS, macOS]
|
||||
configFiles:
|
||||
Debug: Signing.xcconfig
|
||||
Release: Signing.xcconfig
|
||||
sources:
|
||||
- path: VniDrop
|
||||
excludes:
|
||||
@@ -30,6 +33,10 @@ targets:
|
||||
CURRENT_PROJECT_VERSION: "1"
|
||||
GENERATE_INFOPLIST_FILE: NO
|
||||
INFOPLIST_FILE: VniDrop/Resources/Info.plist
|
||||
# Mirror the Info.plist identity so Xcode's Identity editor shows it too
|
||||
# (the editor reads these build settings, not the manual plist).
|
||||
INFOPLIST_KEY_CFBundleDisplayName: VniDrop
|
||||
INFOPLIST_KEY_LSApplicationCategoryType: public.app-category.utilities
|
||||
SWIFT_VERSION: "5.9"
|
||||
ENABLE_USER_SCRIPT_SANDBOXING: NO
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
||||
|
||||
Reference in New Issue
Block a user