Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 73 additions & 1 deletion Apps/Keyty/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,27 @@ let appReleaseSettings: SettingsDictionary = [
"GCC_MODEL_TUNING": "G5",
]

// This configuration is intentionally separate from Release. Release produces
// the Developer ID-signed, notarized build used for direct distribution.
// AppStore is the sandboxed build that will be archived for App Store Connect.
let appStoreSettings: SettingsDictionary = [
"CODE_SIGN_ENTITLEMENTS": "Sources/Keyty/Resources/AppStore.entitlements",
"CODE_SIGN_STYLE": "Automatic",
"DEVELOPMENT_TEAM": "NEVA4MAZBL",
"GCC_MODEL_TUNING": "G5",
"PRODUCT_BUNDLE_IDENTIFIER": "app.keyty.Keyty.AppStore",
"SWIFT_ACTIVE_COMPILATION_CONDITIONS": [
"$(inherited)",
"APP_STORE",
],
]

let appStoreDebugSettings = appStoreSettings.merging([
"PRODUCT_NAME": "KeytyAppStore",
]) { _, appStoreDebugValue in
appStoreDebugValue
}

let testSettings: SettingsDictionary = [
"BUNDLE_LOADER": "$(TEST_HOST)",
"CLANG_ANALYZER_NONNULL": "YES",
Expand Down Expand Up @@ -164,13 +185,20 @@ let projectSettings = Settings.settings(
"SWIFT_COMPILATION_MODE": "wholemodule",
]
),
.release(
name: "AppStore",
settings: [
"SWIFT_COMPILATION_MODE": "wholemodule",
]
),
]
)

let project = Project(
name: "Keyty",
organizationName: "Keyty",
options: .options(
automaticSchemesOptions: .disabled,
defaultKnownRegions: ["de", "en", "es", "fr", "it", "ja", "ko", "nl", "pt-BR", "uk", "zh-Hans"],
developmentRegion: "en"
),
Expand Down Expand Up @@ -204,7 +232,10 @@ let project = Project(
buildableFolders: [
.folder(
"Sources/Keyty",
exceptions: [.exception(excluded: ["Resources/Info.plist"])]
exceptions: [.exception(excluded: [
"Resources/Info.plist",
"Resources/AppStore-Info.plist",
])]
),
],
entitlements: .file(path: "Sources/Keyty/Resources/Keyty.entitlements"),
Expand All @@ -225,6 +256,38 @@ let project = Project(
]
)
),
.target(
name: "KeytyAppStore",
destinations: .macOS,
product: .app,
bundleId: "app.keyty.Keyty.AppStore",
deploymentTargets: .macOS("11.0"),
infoPlist: .file(path: "Sources/Keyty/Resources/AppStore-Info.plist"),
buildableFolders: [
.folder(
"Sources/Keyty",
exceptions: [.exception(excluded: [
"Resources/Info.plist",
"Resources/AppStore-Info.plist",
])]
),
],
entitlements: .file(path: "Sources/Keyty/Resources/AppStore.entitlements"),
dependencies: [
.sdk(name: "Cocoa", type: .framework),
.sdk(name: "Quartz", type: .framework),
.sdk(name: "QuartzCore", type: .framework),
.sdk(name: "Carbon", type: .framework),
.package(product: "ShortcutRecorder"),
],
settings: .settings(
base: appSettings,
configurations: [
.debug(name: "Debug", settings: appStoreDebugSettings),
.release(name: "AppStore", settings: appStoreSettings),
]
)
),
.target(
name: "KeytyTests",
destinations: .macOS,
Expand Down Expand Up @@ -265,6 +328,15 @@ let project = Project(
profileAction: .profileAction(configuration: .release),
analyzeAction: .analyzeAction(configuration: .debug)
),
.scheme(
name: "Keyty AppStore",
shared: true,
buildAction: .buildAction(targets: ["KeytyAppStore"]),
runAction: .runAction(configuration: "AppStore"),
archiveAction: .archiveAction(configuration: "AppStore"),
profileAction: .profileAction(configuration: "AppStore"),
analyzeAction: .analyzeAction(configuration: .debug)
),
],
resourceSynthesizers: [
.custom(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
//

import AppKit
import Sparkle

@MainActor
final class AppDependencies {
Expand All @@ -23,7 +22,7 @@ final class AppDependencies {

init(
statusShortcutItem: NSMenuItem,
updater: SPUUpdater,
updateService: any UpdateService,
keyValueStore: KeyValueStore = UserDefaultsStore()
) {
self.settings = AppSettingsContainer(store: keyValueStore)
Expand All @@ -34,7 +33,7 @@ final class AppDependencies {
self.ui = AppUIContainer(
settings: self.settings,
services: self.services,
updater: updater
updateService: updateService
)
}
}
6 changes: 2 additions & 4 deletions Apps/Keyty/Sources/Keyty/App/Composition/AppUIContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
// SPDX-License-Identifier: BSD-3-Clause
//

import Sparkle

@MainActor
final class AppUIContainer {
let aboutWindowController: AboutWindowController
Expand All @@ -18,7 +16,7 @@ final class AppUIContainer {
init(
settings: AppSettingsContainer,
services: AppServiceContainer,
updater: SPUUpdater
updateService: any UpdateService
) {
self.aboutWindowController = AboutWindowController()
let keyboardVisualizerPlacementWindowController = KeyboardVisualizerPlacementWindowController(
Expand All @@ -34,7 +32,7 @@ final class AppUIContainer {
pointerRingVisualizer: services.pointerVisualizersManager.ring,
pointerRipplesVisualizer: services.pointerVisualizersManager.ripples,
permissionsService: services.permissionsService,
updater: updater,
updateService: updateService,
placementCoordinator: keyboardVisualizerPlacementWindowController
)
let settingsWindowController = SettingsWindowController(context: settingsContext)
Expand Down
20 changes: 9 additions & 11 deletions Apps/Keyty/Sources/Keyty/App/Lifecycle/AppController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,20 @@
//

import AppKit

#if !APP_STORE
import AppMover
import Sparkle
#endif

@MainActor
final class AppController: NSObject {
private let menuController: MenuController
private let statusItemController: StatusItemController
private let dependencies: AppDependencies
private let updaterController: SPUStandardUpdaterController
private let updateService: any UpdateService

override init() {
self.updaterController = SPUStandardUpdaterController(
startingUpdater: true,
updaterDelegate: nil,
userDriverDelegate: nil
)
self.updateService = UpdateServiceFactory.make()
self.menuController = MenuController()
let statusShortcutItem = self.menuController.makeStatusShortcutMenuItem()
self.statusItemController = StatusItemController(
Expand All @@ -31,7 +29,7 @@ final class AppController: NSObject {
)
self.dependencies = AppDependencies(
statusShortcutItem: statusShortcutItem,
updater: self.updaterController.updater
updateService: self.updateService
)
super.init()
self.menuController.setAppController(self)
Expand All @@ -51,7 +49,7 @@ extension AppController: NSApplicationDelegate {

NSApp.activate(ignoringOtherApps: true)

#if !DEBUG
#if !DEBUG && !APP_STORE
if AppMover.moveIfNecessary() {
return
}
Expand Down Expand Up @@ -86,8 +84,8 @@ extension AppController: NSApplicationDelegate {
// MARK: - Settings Presentation
private extension AppController {
func checkForUpdatesAtLaunchIfNeeded() {
guard self.updaterController.updater.automaticallyChecksForUpdates else { return }
self.updaterController.updater.checkForUpdatesInBackground()
guard self.updateService.automaticallyChecksForUpdates else { return }
self.updateService.checkForUpdatesInBackground()
}

func showSettingsAtLaunchIfNeeded() {
Expand Down
4 changes: 4 additions & 0 deletions Apps/Keyty/Sources/Keyty/App/Support/L10n.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@
// SPDX-License-Identifier: BSD-3-Clause
//

#if APP_STORE
typealias L10n = KeytyAppStoreStrings.Localizable
#else
typealias L10n = KeytyStrings.Localizable
#endif
114 changes: 62 additions & 52 deletions Apps/Keyty/Sources/Keyty/Features/About/AboutWindowViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,28 +72,7 @@ final class AboutWindowViewModel: ObservableObject {
]
)
]
self.credits = [
Section(
title: "Sparkle",
subtitle: L10n.About.Credits.sparkleSubtitle,
body: LicenseLoader.text(
named: "Sparkle",
trimmingLeadingLines: [
"Sparkle",
"The MIT License (MIT)"
]
)
),
Section(
title: "AppMover",
subtitle: L10n.About.Credits.appMoverSubtitle,
body: LicenseLoader.text(
named: "AppMover",
trimmingLeadingLines: [
"MIT License"
]
)
),
var credits = [
Section(
title: "ShortcutRecorder",
subtitle: L10n.About.Credits.shortcutRecorderSubtitle,
Expand All @@ -105,6 +84,37 @@ final class AboutWindowViewModel: ObservableObject {
)
)
]

#if !APP_STORE
credits.insert(
contentsOf: [
Section(
title: "Sparkle",
subtitle: L10n.About.Credits.sparkleSubtitle,
body: LicenseLoader.text(
named: "Sparkle",
trimmingLeadingLines: [
"Sparkle",
"The MIT License (MIT)"
]
)
),
Section(
title: "AppMover",
subtitle: L10n.About.Credits.appMoverSubtitle,
body: LicenseLoader.text(
named: "AppMover",
trimmingLeadingLines: [
"MIT License"
]
)
),
],
at: 0
)
#endif

self.credits = credits
}

var selectedSections: [Section] {
Expand Down Expand Up @@ -198,44 +208,44 @@ extension AboutWindowViewModel {

extension AboutWindowViewModel {
enum LicenseLoader {
static func text(
named resourceName: String,
trimmingLeadingLines linesToTrim: [String] = [],
bundle: Bundle = .main
) -> String {
let candidates = [
bundle.url(forResource: resourceName, withExtension: "txt", subdirectory: "AboutLicenses"),
bundle.url(forResource: resourceName, withExtension: "txt")
]

for candidate in candidates {
guard let url = candidate else { continue }
if let text = try? String(contentsOf: url, encoding: .utf8) {
return Self.sanitized(text, trimmingLeadingLines: linesToTrim)
static func text(
named resourceName: String,
trimmingLeadingLines linesToTrim: [String] = [],
bundle: Bundle = .main
) -> String {
let candidates = [
bundle.url(forResource: resourceName, withExtension: "txt", subdirectory: "AboutLicenses"),
bundle.url(forResource: resourceName, withExtension: "txt")
]

for candidate in candidates {
guard let url = candidate else { continue }
if let text = try? String(contentsOf: url, encoding: .utf8) {
return Self.sanitized(text, trimmingLeadingLines: linesToTrim)
}
}
}

return L10n.About.Credits.licenseMissing
}

private static func sanitized(_ text: String, trimmingLeadingLines linesToTrim: [String]) -> String {
var lines = text.components(separatedBy: .newlines)

while let first = lines.first, first.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
lines.removeFirst()
return L10n.About.Credits.licenseMissing
}

for lineToTrim in linesToTrim {
guard let first = lines.first else { break }
if first.trimmingCharacters(in: .whitespacesAndNewlines) == lineToTrim {
private static func sanitized(_ text: String, trimmingLeadingLines linesToTrim: [String]) -> String {
var lines = text.components(separatedBy: .newlines)

while let first = lines.first, first.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
lines.removeFirst()
while let next = lines.first, next.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
}

for lineToTrim in linesToTrim {
guard let first = lines.first else { break }
if first.trimmingCharacters(in: .whitespacesAndNewlines) == lineToTrim {
lines.removeFirst()
while let next = lines.first, next.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
lines.removeFirst()
}
}
}
}

return lines.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines)
}
return lines.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines)
}
}
}
Loading
Loading