Files
vnidrop/apple/VniDrop/App/VniDropApp.swift
cdricms cc194f6a7b feat(apple): add direct-download macOS channel (notarized DMG + Sparkle + Homebrew)
Add a second macOS shipping channel alongside the App Store build:

- New VniDropDirect target (Release-Direct config) sharing VniDrop's sources via
  an AppBase target template; links Sparkle behind the DIRECT_DISTRIBUTION flag so
  the App Store binary never bundles a self-updater. arm64-only (core is arm64).
- Sparkle updater (SparkleUpdater.swift) + "Check for Updates" menu, compiled only
  under DIRECT_DISTRIBUTION; Info.plist SUFeedURL points at the GitHub Release
  /latest/download/appcast.xml, non-sandboxed entitlements for Developer ID.
- build-dmg.sh (archive → Developer ID export → DMG → sign → notarize → staple),
  generate-appcast.sh, and ExportOptions-DeveloperID.plist.
- apple-release.yml: on tag v*.*.*, build/notarize the DMG, publish the GitHub
  Release with appcast, and push the Homebrew cask to sudosylabs/homebrew-vnidrop.
  apple.yml gains a PR compile-check of the direct target.
- CFBundleVersion is stamped at build time as a UTC YYMMDD.HHMM timestamp for both
  channels, replacing the hand-maintained build number.
- Docs (RELEASE-MACOS.md, README), cask template + tap README, localized
  updates_check string, Makefile targets, gitignore for dist/ artifacts.
2026-07-27 14:31:36 +02:00

63 lines
2.2 KiB
Swift

import SwiftUI
/// Scene identifier for the single main window.
private let mainWindowId = "main"
/// Native app entry point for iOS, iPadOS, and macOS.
/// Opens `.vnd` invitations via `onOpenURL` and routes them to the receive flow.
@main
struct VniDropApp: App {
@StateObject private var externalInvitations = ExternalInvitationController()
#if DIRECT_DISTRIBUTION && os(macOS)
// Sparkle auto-updater, present only in the direct-download (.dmg) build.
@StateObject private var updater = SparkleUpdaterController()
#endif
var body: some Scene {
#if os(macOS)
// A single-instance `Window` (not `WindowGroup`): the app must never open a
// second window. `Window` also drops the N "New Window" command.
Window(Text(verbatim: "VniDrop"), id: mainWindowId) {
RootView(dependencies: makeAppDependencies(externalInvitations: externalInvitations))
.ignoresSafeArea()
.onOpenURL(perform: openInvitation)
}
#if DIRECT_DISTRIBUTION
.commands {
UpdatesCommands(controller: updater)
}
#endif
#else
WindowGroup(id: mainWindowId) {
RootView(dependencies: makeAppDependencies(externalInvitations: externalInvitations))
.ignoresSafeArea()
.onOpenURL(perform: openInvitation)
}
#endif
}
/// Reads a `.vnd` invitation document under a security scope, enforcing the
/// 64 KiB / strict-UTF-8 rules from `ContentView.swift`.
private func openInvitation(_ url: URL) {
guard url.pathExtension.caseInsensitiveCompare(vniDropInvitationExtension) == .orderedSame else {
externalInvitations.reportOpenFailure(message: "This is not a VniDrop invitation")
return
}
let started = url.startAccessingSecurityScopedResource()
defer { if started { url.stopAccessingSecurityScopedResource() } }
do {
let values = try url.resourceValues(forKeys: [.fileSizeKey])
if let size = values.fileSize, size > maxVniDropInvitationBytes {
throw InvitationError.tooLarge
}
let data = try Data(contentsOf: url, options: .mappedIfSafe)
let raw = try decodeInvitationBytes(data)
externalInvitations.openInvitation(raw: raw)
} catch {
externalInvitations.reportOpenFailure(
message: (error as? LocalizedError)?.errorDescription ?? "The invitation could not be opened"
)
}
}
}