Skip to content
Merged
2 changes: 1 addition & 1 deletion StandLock/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
let hasActiveOverlay = sender.windows.contains { $0 is BreakOverlayWindow && $0.isVisible }
return hasActiveOverlay ? .terminateCancel : .terminateNow
}
}
}
33 changes: 33 additions & 0 deletions StandLock/AppState/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ final class AppCoordinator: ObservableObject {
loadData()
syncPreferencesWithPermissions()
startProgressTimer()
observeSystemSleep()
permissionSyncCancellable = permissionChecker.objectWillChange
.receive(on: RunLoop.main)
.sink { [weak self] _ in
Expand Down Expand Up @@ -223,6 +224,10 @@ final class AppCoordinator: ObservableObject {
breakScheduledAt = Date()
recalculateProgress()
updateMenuBarTimer()
if menuBarTimerText != nil {
progressTimer?.cancel()
startProgressTimer()
}

case .breakStarted(let e):
deferralReason = nil
Expand Down Expand Up @@ -305,6 +310,34 @@ final class AppCoordinator: ObservableObject {
coordinator?.changeDisciplineLevel(level)
}

// MARK: - System Sleep

private func observeSystemSleep() {
let center = NSWorkspace.shared.notificationCenter
center.addObserver(forName: NSWorkspace.willSleepNotification, object: nil, queue: .main) { [weak self] _ in
MainActor.assumeIsolated {
self?.coordinator?.handleSystemSleep()
}
}
center.addObserver(forName: NSWorkspace.didWakeNotification, object: nil, queue: .main) { [weak self] _ in
MainActor.assumeIsolated {
self?.coordinator?.handleSystemWake()
}
}

let distributed = DistributedNotificationCenter.default()
distributed.addObserver(forName: .init("com.apple.screenIsLocked"), object: nil, queue: .main) { [weak self] _ in
MainActor.assumeIsolated {
self?.coordinator?.handleScreenLock()
}
}
distributed.addObserver(forName: .init("com.apple.screenIsUnlocked"), object: nil, queue: .main) { [weak self] _ in
MainActor.assumeIsolated {
self?.coordinator?.handleScreenUnlock()
}
}
}

// MARK: - Break Progress

private func startProgressTimer() {
Expand Down
2 changes: 1 addition & 1 deletion StandLock/StandLockApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ struct StandLockApp: App {
.menuBarExtraStyle(.window)

Settings {
SettingsView()
SettingsView(selectedTab: $appCoordinator.selectedSettingsTab, updater: appDelegate.updaterController.updater)
.environmentObject(appCoordinator)
.environmentObject(appCoordinator.permissionChecker)
}
Expand Down
2 changes: 1 addition & 1 deletion StandLock/Views/BreakScreen/TimerNumerals.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ struct TimerNumerals: View {
Text(timeString)
.font(BreakTypography.timerNumerals(size: fontSize))
.monospacedDigit()
.tracking(fontSize * -0.04)
.tracking(fontSize * -0.02)
.foregroundStyle(palette.ink)
.contentTransition(.identity)
.transaction { $0.animation = nil }
Expand Down
26 changes: 18 additions & 8 deletions StandLock/Views/Settings/AboutView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import SwiftUI
@preconcurrency import Sparkle

struct AboutView: View {
@NSApplicationDelegateAdaptor private var appDelegate: AppDelegate
let updater: SPUUpdater

private var version: String {
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0"
Expand Down Expand Up @@ -35,21 +35,31 @@ struct AboutView: View {
.foregroundStyle(.secondary)

VStack(alignment: .leading, spacing: 8) {
UpdaterSettingsView(updater: appDelegate.updaterController.updater)
CheckForUpdatesView(updater: appDelegate.updaterController.updater)
UpdaterSettingsView(updater: updater)
CheckForUpdatesView(updater: updater)
}
.padding(.horizontal, 40)
.padding(12)
.background(.quaternary.opacity(0.5), in: RoundedRectangle(cornerRadius: 8))

VStack(spacing: 4) {
Text("Made by Yağız")
.font(.footnote)
Link("GitHub", destination: URL(string: "https://github.com/yagizdo/StandLock")!)
.font(.footnote)
HStack(spacing: 16) {
aboutLink("GitHub", icon: "curlybraces", url: "https://github.com/yagizdo/StandLock")
aboutLink("Website", icon: "globe", url: "https://standlock.app")
aboutLink("Twitter", icon: "at", url: "https://x.com/yagizdo")
}

Text("© 2026 Yılmaz Yağız Dokumacı. MIT License.")
.font(.footnote)
.foregroundStyle(.tertiary)
}
.padding(.top, 12)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}

private func aboutLink(_ title: String, icon: String, url: String) -> some View {
Link(destination: URL(string: url)!) {
Label(title, systemImage: icon)
.font(.footnote)
}
}
}
75 changes: 57 additions & 18 deletions StandLock/Views/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
@@ -1,30 +1,69 @@
import SwiftUI
@preconcurrency import Sparkle

struct SettingsView: View {
@EnvironmentObject private var coordinator: AppCoordinator
@Binding var selectedTab: AppCoordinator.SettingsTab
let updater: SPUUpdater

var body: some View {
TabView(selection: $coordinator.selectedSettingsTab) {
GeneralSettingsView()
.tabItem { Label("General", systemImage: "gearshape") }
.tag(AppCoordinator.SettingsTab.general)
VStack(spacing: 0) {
SettingsTabBar(selectedTab: $selectedTab)
Divider()
tabContent
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.frame(width: 520, height: 480)
}

ScheduleEditorView()
.tabItem { Label("Schedules", systemImage: "calendar.badge.clock") }
.tag(AppCoordinator.SettingsTab.schedules)
@ViewBuilder
private var tabContent: some View {
switch selectedTab {
case .general: GeneralSettingsView()
case .schedules: ScheduleEditorView()
case .detection: DetectionSettingsView()
case .permissions: PermissionsView()
case .about: AboutView(updater: updater)
}
}
}

DetectionSettingsView()
.tabItem { Label("Detection", systemImage: "eye") }
.tag(AppCoordinator.SettingsTab.detection)
private struct SettingsTabBar: View {
@Binding var selectedTab: AppCoordinator.SettingsTab

PermissionsView()
.tabItem { Label("Permissions", systemImage: "lock.shield") }
.tag(AppCoordinator.SettingsTab.permissions)
private static let tabs: [(AppCoordinator.SettingsTab, String, String)] = [
(.general, "General", "gearshape"),
(.schedules, "Schedules", "calendar.badge.clock"),
(.detection, "Detection", "eye"),
(.permissions, "Permissions", "lock.shield"),
(.about, "About", "info.circle"),
]

AboutView()
.tabItem { Label("About", systemImage: "info.circle") }
.tag(AppCoordinator.SettingsTab.about)
var body: some View {
HStack(spacing: 2) {
ForEach(Self.tabs, id: \.0) { tab, title, icon in
Button {
selectedTab = tab
} label: {
VStack(spacing: 2) {
Image(systemName: icon)
.font(.system(size: 16))
.frame(width: 24, height: 20)
Text(title)
.font(.system(size: 10))
}
.padding(.vertical, 6)
.padding(.horizontal, 10)
.contentShape(Rectangle())
.foregroundStyle(selectedTab == tab ? Color.accentColor : .secondary)
.background(
RoundedRectangle(cornerRadius: 6)
.fill(selectedTab == tab ? Color.accentColor.opacity(0.12) : .clear)
)
}
.buttonStyle(.plain)
}
}
.frame(width: 520, height: 480)
.padding(.horizontal, 16)
.padding(.vertical, 8)
}
}
41 changes: 41 additions & 0 deletions StandLockKit/Sources/Coordination/BreakCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,47 @@ public final class BreakCoordinator: BreakCoordinating {
scheduleNextBreak()
}

public func handleSystemSleep() {
cancelAndDismissIfShowing()
}

public func handleSystemWake() {
if isPaused {
resume()
} else {
scheduleNextBreak()
}
}

public func handleScreenLock() {
cancelAndDismissIfShowing()
}

public func handleScreenUnlock() {
if isPaused {
resume()
} else {
scheduleNextBreak()
}
}

private func cancelAndDismissIfShowing() {
breakTimer?.cancel()
breakTimer = nil
if locker.isShowing {
locker.dismissOverlay()
if var event = currentBreak {
event.outcome = .skipped
statistics.breaksSkipped += 1
statistics.currentStreak = 0
eventContinuation.yield(.breakSkipped(event))
eventContinuation.yield(.statisticsUpdated(statistics))
}
currentBreak = nil
currentSchedule = nil
}
}

public func skipNextBreak() {
breakTimer?.cancel()
breakTimer = nil
Expand Down
5 changes: 4 additions & 1 deletion StandLockKit/Sources/StandLockCore/Models/Schedule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,10 @@ public struct TimeWindow: Codable, Sendable, Equatable {
let time = hour * 60 + minute
let start = startHour * 60 + startMinute
let end = endHour * 60 + endMinute
return time >= start && time < end
if start <= end {
return time >= start && time < end
}
return time >= start || time < end
}
}

Expand Down
Loading
Loading