feat(macos): add macOS menu bar app — popover timeline, quick timer, keyboard shortcut, settings
This commit is contained in:
parent
a2e8f985d2
commit
2fc277b663
39
ios/ChronoMindMac/ChronoMindMacApp.swift
Normal file
39
ios/ChronoMindMac/ChronoMindMacApp.swift
Normal file
@ -0,0 +1,39 @@
|
||||
// ── ChronoMind macOS Menu Bar App ─────────────────────────────
|
||||
// Native macOS menu bar app sharing 90%+ code with iOS via Shared/
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct ChronoMindMacApp: App {
|
||||
@StateObject private var menuBarState = MenuBarState.shared
|
||||
@StateObject private var timerStore = MacTimerStore.shared
|
||||
|
||||
var body: some Scene {
|
||||
// Menu bar extra — always visible
|
||||
MenuBarExtra {
|
||||
MenuBarPopover()
|
||||
.environmentObject(timerStore)
|
||||
.environmentObject(menuBarState)
|
||||
} label: {
|
||||
menuBarLabel
|
||||
}
|
||||
.menuBarExtraStyle(.window)
|
||||
|
||||
// Settings window
|
||||
Settings {
|
||||
MacSettingsView()
|
||||
.environmentObject(timerStore)
|
||||
}
|
||||
}
|
||||
|
||||
private var menuBarLabel: some View {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "clock.fill")
|
||||
if let next = timerStore.nextFiringTimer {
|
||||
let remaining = getRemainingSeconds(next, now: timerStore.now)
|
||||
Text(formatDurationCompact(remaining))
|
||||
.monospacedDigit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
180
ios/ChronoMindMac/MacTimerStore.swift
Normal file
180
ios/ChronoMindMac/MacTimerStore.swift
Normal file
@ -0,0 +1,180 @@
|
||||
// ── Mac Timer Store ───────────────────────────────────────────
|
||||
// macOS-specific timer store that shares engine logic with iOS
|
||||
// Reads from shared App Group UserDefaults for cross-device sync
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import UserNotifications
|
||||
|
||||
@MainActor
|
||||
final class MacTimerStore: ObservableObject {
|
||||
static let shared = MacTimerStore()
|
||||
|
||||
@Published var timers: [CMTimer] = []
|
||||
@Published var now: Date = Date()
|
||||
|
||||
private var tickTimer: Timer?
|
||||
private let persistenceKey = "chronomind-timers"
|
||||
private let sharedDefaults: UserDefaults?
|
||||
|
||||
var activeTimers: [CMTimer] {
|
||||
timers.filter { isTimerActive($0) }
|
||||
}
|
||||
|
||||
var nextFiringTimer: CMTimer? {
|
||||
activeTimers
|
||||
.filter { $0.state == .active || $0.state == .warning }
|
||||
.sorted { $0.targetTime < $1.targetTime }
|
||||
.first
|
||||
}
|
||||
|
||||
private init() {
|
||||
sharedDefaults = UserDefaults(suiteName: "group.com.chronomind.shared")
|
||||
loadTimers()
|
||||
startTicking()
|
||||
requestNotificationPermission()
|
||||
}
|
||||
|
||||
deinit {
|
||||
tickTimer?.invalidate()
|
||||
}
|
||||
|
||||
// MARK: - CRUD
|
||||
|
||||
func addCountdown(label: String, durationSeconds: TimeInterval) {
|
||||
let timer = createCountdown(CreateCountdownParams(
|
||||
label: label,
|
||||
durationSeconds: durationSeconds
|
||||
))
|
||||
timers.append(timer)
|
||||
scheduleNotification(for: timer)
|
||||
saveTimers()
|
||||
}
|
||||
|
||||
func addAlarm(label: String, targetTime: Date, urgency: UrgencyLevel = .standard) {
|
||||
let timer = createAlarm(CreateAlarmParams(
|
||||
label: label,
|
||||
targetTime: targetTime,
|
||||
urgency: urgency
|
||||
))
|
||||
timers.append(timer)
|
||||
scheduleNotification(for: timer)
|
||||
saveTimers()
|
||||
}
|
||||
|
||||
func removeTimer(_ id: String) {
|
||||
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [id])
|
||||
timers.removeAll { $0.id == id }
|
||||
saveTimers()
|
||||
}
|
||||
|
||||
func pause(_ id: String) {
|
||||
updateTimer(id) { pauseTimer($0) }
|
||||
}
|
||||
|
||||
func resume(_ id: String) {
|
||||
updateTimer(id) { t in
|
||||
let resumed = resumeTimer(t)
|
||||
self.scheduleNotification(for: resumed)
|
||||
return resumed
|
||||
}
|
||||
}
|
||||
|
||||
func snooze(_ id: String, minutes: Int) {
|
||||
updateTimer(id) { t in
|
||||
let snoozed = snoozeTimer(t, snoozeMinutes: minutes)
|
||||
self.scheduleNotification(for: snoozed)
|
||||
return snoozed
|
||||
}
|
||||
}
|
||||
|
||||
func dismiss(_ id: String) {
|
||||
updateTimer(id) { dismissTimer($0) }
|
||||
}
|
||||
|
||||
func complete(_ id: String) {
|
||||
updateTimer(id) { completeTimer($0) }
|
||||
}
|
||||
|
||||
// MARK: - Tick
|
||||
|
||||
private func startTicking() {
|
||||
tickTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
||||
Task { @MainActor in
|
||||
self?.tick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func tick() {
|
||||
now = Date()
|
||||
var changed = false
|
||||
|
||||
for i in timers.indices {
|
||||
if shouldTimerFire(timers[i], now: now) {
|
||||
timers[i] = fireTimer(timers[i])
|
||||
changed = true
|
||||
}
|
||||
|
||||
let newlyFired = checkWarnings(&timers[i].warnings, now: now)
|
||||
if !newlyFired.isEmpty {
|
||||
changed = true
|
||||
if timers[i].state == .active {
|
||||
timers[i].state = .warning
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if changed { saveTimers() }
|
||||
}
|
||||
|
||||
// MARK: - Persistence
|
||||
|
||||
private func loadTimers() {
|
||||
// Try shared defaults first (synced from iOS), then local
|
||||
let defaults = sharedDefaults ?? UserDefaults.standard
|
||||
guard let data = defaults.data(forKey: persistenceKey) else { return }
|
||||
if let decoded = try? JSONDecoder().decode([CMTimer].self, from: data) {
|
||||
timers = decoded
|
||||
}
|
||||
}
|
||||
|
||||
private func saveTimers() {
|
||||
guard let data = try? JSONEncoder().encode(timers) else { return }
|
||||
UserDefaults.standard.set(data, forKey: persistenceKey)
|
||||
sharedDefaults?.set(data, forKey: persistenceKey)
|
||||
}
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
private func requestNotificationPermission() {
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { _, _ in }
|
||||
}
|
||||
|
||||
private func scheduleNotification(for timer: CMTimer) {
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = timer.label
|
||||
content.body = "Timer fired!"
|
||||
content.sound = .default
|
||||
content.categoryIdentifier = "TIMER_FIRED"
|
||||
|
||||
let interval = timer.targetTime.timeIntervalSinceNow
|
||||
guard interval > 0 else { return }
|
||||
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: interval, repeats: false)
|
||||
let request = UNNotificationRequest(identifier: timer.id, content: content, trigger: trigger)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func updateTimer(_ id: String, transform: (CMTimer) -> CMTimer) {
|
||||
guard let index = timers.firstIndex(where: { $0.id == id }) else { return }
|
||||
timers[index] = transform(timers[index])
|
||||
saveTimers()
|
||||
}
|
||||
|
||||
func getTimer(_ id: String) -> CMTimer? {
|
||||
timers.first { $0.id == id }
|
||||
}
|
||||
}
|
||||
29
ios/ChronoMindMac/MenuBarState.swift
Normal file
29
ios/ChronoMindMac/MenuBarState.swift
Normal file
@ -0,0 +1,29 @@
|
||||
// ── Menu Bar State ────────────────────────────────────────────
|
||||
// Observable state for the macOS menu bar popover
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
final class MenuBarState: ObservableObject {
|
||||
static let shared = MenuBarState()
|
||||
|
||||
@Published var isExpanded = false
|
||||
@Published var showQuickTimer = false
|
||||
@Published var quickTimerLabel = ""
|
||||
@Published var quickTimerMinutes: Double = 25
|
||||
|
||||
private init() {}
|
||||
|
||||
func toggleExpanded() {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
isExpanded.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
func resetQuickTimer() {
|
||||
quickTimerLabel = ""
|
||||
quickTimerMinutes = 25
|
||||
showQuickTimer = false
|
||||
}
|
||||
}
|
||||
100
ios/ChronoMindMac/Views/MacSettingsView.swift
Normal file
100
ios/ChronoMindMac/Views/MacSettingsView.swift
Normal file
@ -0,0 +1,100 @@
|
||||
// ── macOS Settings View ───────────────────────────────────────
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MacSettingsView: View {
|
||||
@EnvironmentObject var store: MacTimerStore
|
||||
@AppStorage("cm_defaultUrgency") private var defaultUrgency = "standard"
|
||||
@AppStorage("cm_defaultCascade") private var defaultCascade = "standard"
|
||||
@AppStorage("cm_launchAtLogin") private var launchAtLogin = false
|
||||
|
||||
var body: some View {
|
||||
TabView {
|
||||
generalTab
|
||||
.tabItem { Label("General", systemImage: "gearshape") }
|
||||
|
||||
dataTab
|
||||
.tabItem { Label("Data", systemImage: "externaldrive") }
|
||||
|
||||
aboutTab
|
||||
.tabItem { Label("About", systemImage: "info.circle") }
|
||||
}
|
||||
.frame(width: 400, height: 300)
|
||||
}
|
||||
|
||||
// MARK: - General
|
||||
|
||||
private var generalTab: some View {
|
||||
Form {
|
||||
Toggle("Launch at Login", isOn: $launchAtLogin)
|
||||
|
||||
Picker("Default Urgency", selection: $defaultUrgency) {
|
||||
ForEach(UrgencyLevel.allCases) { level in
|
||||
Text(getUrgencyConfig(level).label).tag(level.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
Picker("Default Cascade", selection: $defaultCascade) {
|
||||
ForEach(CascadePreset.allCases.filter { $0 != .custom }) { preset in
|
||||
Text(preset.label).tag(preset.rawValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
// MARK: - Data
|
||||
|
||||
private var dataTab: some View {
|
||||
Form {
|
||||
HStack {
|
||||
Text("Total Timers")
|
||||
Spacer()
|
||||
Text("\(store.timers.count)")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Active")
|
||||
Spacer()
|
||||
Text("\(store.activeTimers.count)")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button("Clear Completed") {
|
||||
store.timers.removeAll { [.completed, .dismissed].contains($0.state) }
|
||||
}
|
||||
|
||||
Button("Delete All Timers", role: .destructive) {
|
||||
store.timers.removeAll()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
// MARK: - About
|
||||
|
||||
private var aboutTab: some View {
|
||||
VStack(spacing: 12) {
|
||||
Image(systemName: "clock.fill")
|
||||
.font(.system(size: 48))
|
||||
.foregroundStyle(CMColors.accent)
|
||||
|
||||
Text("ChronoMind")
|
||||
.font(.title2.bold())
|
||||
|
||||
Text("Version 1.0.0")
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Text("Time management with cascade warnings")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.tertiary)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
264
ios/ChronoMindMac/Views/MenuBarPopover.swift
Normal file
264
ios/ChronoMindMac/Views/MenuBarPopover.swift
Normal file
@ -0,0 +1,264 @@
|
||||
// ── Menu Bar Popover ──────────────────────────────────────────
|
||||
// Main popover view shown when clicking the menu bar icon
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MenuBarPopover: View {
|
||||
@EnvironmentObject var store: MacTimerStore
|
||||
@EnvironmentObject var menuBar: MenuBarState
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
// Header
|
||||
header
|
||||
|
||||
Divider().background(CMColors.border)
|
||||
|
||||
// Quick timer creation
|
||||
if menuBar.showQuickTimer {
|
||||
quickTimerForm
|
||||
Divider().background(CMColors.border)
|
||||
}
|
||||
|
||||
// Timer list
|
||||
if store.activeTimers.isEmpty && !menuBar.showQuickTimer {
|
||||
emptyState
|
||||
} else {
|
||||
timerList
|
||||
}
|
||||
|
||||
Divider().background(CMColors.border)
|
||||
|
||||
// Footer
|
||||
footer
|
||||
}
|
||||
.frame(width: 320)
|
||||
.background(CMColors.bg)
|
||||
}
|
||||
|
||||
// MARK: - Header
|
||||
|
||||
private var header: some View {
|
||||
HStack {
|
||||
Text("ChronoMind")
|
||||
.font(CMFonts.display(size: 14))
|
||||
.foregroundStyle(CMColors.text)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
menuBar.showQuickTimer.toggle()
|
||||
} label: {
|
||||
Image(systemName: menuBar.showQuickTimer ? "xmark" : "plus")
|
||||
.font(.body.weight(.semibold))
|
||||
.foregroundStyle(CMColors.accent)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.keyboardShortcut("t", modifiers: [.command, .shift])
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
|
||||
// MARK: - Quick Timer Form
|
||||
|
||||
private var quickTimerForm: some View {
|
||||
VStack(spacing: 8) {
|
||||
TextField("Timer label", text: $menuBar.quickTimerLabel)
|
||||
.textFieldStyle(.plain)
|
||||
.font(CMFonts.body(size: 14))
|
||||
.foregroundStyle(CMColors.text)
|
||||
.padding(8)
|
||||
.background(CMColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||
|
||||
HStack {
|
||||
Text("\(Int(menuBar.quickTimerMinutes))m")
|
||||
.font(CMFonts.mono(size: 13, weight: .semibold))
|
||||
.foregroundStyle(CMColors.accent)
|
||||
.frame(width: 40)
|
||||
|
||||
Slider(value: $menuBar.quickTimerMinutes, in: 1...120, step: 1)
|
||||
.tint(CMColors.accent)
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
// Preset buttons
|
||||
ForEach([5, 15, 25, 60], id: \.self) { minutes in
|
||||
Button("\(minutes)m") {
|
||||
menuBar.quickTimerMinutes = Double(minutes)
|
||||
}
|
||||
.font(CMFonts.body(size: 11, weight: .medium))
|
||||
.foregroundStyle(menuBar.quickTimerMinutes == Double(minutes) ? .white : CMColors.textSecondary)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(menuBar.quickTimerMinutes == Double(minutes) ? CMColors.accent : CMColors.surfaceHover)
|
||||
.clipShape(Capsule())
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Start") {
|
||||
let label = menuBar.quickTimerLabel.isEmpty ? "Timer" : menuBar.quickTimerLabel
|
||||
store.addCountdown(
|
||||
label: label,
|
||||
durationSeconds: menuBar.quickTimerMinutes * 60
|
||||
)
|
||||
menuBar.resetQuickTimer()
|
||||
}
|
||||
.font(CMFonts.body(size: 13, weight: .semibold))
|
||||
.foregroundStyle(.white)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 6)
|
||||
.background(CMColors.accent)
|
||||
.clipShape(Capsule())
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
}
|
||||
|
||||
// MARK: - Timer List
|
||||
|
||||
private var timerList: some View {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 1) {
|
||||
ForEach(store.activeTimers.sorted(by: { $0.targetTime < $1.targetTime })) { timer in
|
||||
MacTimerRow(timer: timer, now: store.now)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: 300)
|
||||
}
|
||||
|
||||
// MARK: - Empty State
|
||||
|
||||
private var emptyState: some View {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "clock")
|
||||
.font(.system(size: 28))
|
||||
.foregroundStyle(CMColors.textMuted)
|
||||
Text("No active timers")
|
||||
.font(CMFonts.body(size: 13))
|
||||
.foregroundStyle(CMColors.textMuted)
|
||||
Text("Press ⌘⇧T to create one")
|
||||
.font(CMFonts.body(size: 11))
|
||||
.foregroundStyle(CMColors.textMuted.opacity(0.7))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 24)
|
||||
}
|
||||
|
||||
// MARK: - Footer
|
||||
|
||||
private var footer: some View {
|
||||
HStack {
|
||||
Text("\(store.activeTimers.count) active")
|
||||
.font(CMFonts.body(size: 11))
|
||||
.foregroundStyle(CMColors.textMuted)
|
||||
|
||||
Spacer()
|
||||
|
||||
if #available(macOS 14.0, *) {
|
||||
SettingsLink {
|
||||
Text("Settings")
|
||||
.font(CMFonts.body(size: 11))
|
||||
.foregroundStyle(CMColors.textSecondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
Button("Quit") {
|
||||
NSApplication.shared.terminate(nil)
|
||||
}
|
||||
.font(CMFonts.body(size: 11))
|
||||
.foregroundStyle(CMColors.textMuted)
|
||||
.buttonStyle(.plain)
|
||||
.keyboardShortcut("q")
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mac Timer Row
|
||||
|
||||
struct MacTimerRow: View {
|
||||
let timer: CMTimer
|
||||
let now: Date
|
||||
|
||||
@EnvironmentObject var store: MacTimerStore
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
// Urgency dot
|
||||
Circle()
|
||||
.fill(CMColors.urgencyColor(timer.urgency))
|
||||
.frame(width: 6, height: 6)
|
||||
|
||||
// Label
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(timer.label)
|
||||
.font(CMFonts.body(size: 13, weight: .medium))
|
||||
.foregroundStyle(CMColors.text)
|
||||
.lineLimit(1)
|
||||
|
||||
Text(formatTime(timer.targetTime))
|
||||
.font(CMFonts.body(size: 11))
|
||||
.foregroundStyle(CMColors.textMuted)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Countdown
|
||||
let remaining = getRemainingSeconds(timer, now: now)
|
||||
Text(formatDuration(remaining))
|
||||
.font(CMFonts.mono(size: 13, weight: .semibold))
|
||||
.foregroundStyle(stateColor)
|
||||
|
||||
// Actions
|
||||
if timer.state == .firing {
|
||||
Button {
|
||||
store.snooze(timer.id, minutes: 5)
|
||||
} label: {
|
||||
Image(systemName: "moon.zzz")
|
||||
.font(.caption)
|
||||
.foregroundStyle(CMColors.textMuted)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Button {
|
||||
store.dismiss(timer.id)
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.caption)
|
||||
.foregroundStyle(CMColors.error)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
} else {
|
||||
Button {
|
||||
store.removeTimer(timer.id)
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
.font(.caption2.weight(.bold))
|
||||
.foregroundStyle(CMColors.textMuted.opacity(0.5))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 8)
|
||||
.background(timer.state == .firing ? CMColors.urgencyBg(timer.urgency) : Color.clear)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
|
||||
private var stateColor: Color {
|
||||
switch timer.state {
|
||||
case .firing: return CMColors.urgencyColor(timer.urgency)
|
||||
case .warning: return CMColors.important
|
||||
case .paused: return CMColors.textMuted
|
||||
default: return CMColors.accent
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -146,6 +146,36 @@ targets:
|
||||
com.apple.security.application-groups:
|
||||
- group.com.chronomind.shared
|
||||
|
||||
ChronoMindMac:
|
||||
type: application
|
||||
platform: macOS
|
||||
deploymentTarget: "14.0"
|
||||
sources:
|
||||
- path: ChronoMindMac
|
||||
excludes:
|
||||
- "**/.DS_Store"
|
||||
- path: ChronoMind/Shared/TimerEngine
|
||||
- path: ChronoMind/Shared/AppGroup
|
||||
- path: ChronoMind/Shared/Reschedule
|
||||
- path: ChronoMind/Shared/Gamification
|
||||
- path: ChronoMind/Shared/Cloud
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: com.chronomind.mac
|
||||
INFOPLIST_GENERATION_MODE: GeneratedFile
|
||||
GENERATE_INFOPLIST_FILE: true
|
||||
MARKETING_VERSION: "1.0.0"
|
||||
CURRENT_PROJECT_VERSION: "1"
|
||||
INFOPLIST_KEY_CFBundleDisplayName: ChronoMind
|
||||
INFOPLIST_KEY_LSUIElement: true
|
||||
entitlements:
|
||||
path: ChronoMindMac/ChronoMindMac.entitlements
|
||||
properties:
|
||||
com.apple.security.application-groups:
|
||||
- group.com.chronomind.shared
|
||||
com.apple.security.app-sandbox: true
|
||||
com.apple.security.network.client: true
|
||||
|
||||
schemes:
|
||||
ChronoMind:
|
||||
build:
|
||||
@ -179,3 +209,16 @@ schemes:
|
||||
config: Debug
|
||||
archive:
|
||||
config: Release
|
||||
|
||||
ChronoMindMac:
|
||||
build:
|
||||
targets:
|
||||
ChronoMindMac: all
|
||||
run:
|
||||
config: Debug
|
||||
profile:
|
||||
config: Release
|
||||
analyze:
|
||||
config: Debug
|
||||
archive:
|
||||
config: Release
|
||||
|
||||
Loading…
Reference in New Issue
Block a user