Skip to content
Open
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
95 changes: 95 additions & 0 deletions Timer/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import AppKit
import UserNotifications

private final class MVNotificationResponseCompletion: @unchecked Sendable {
private let completionHandler: () -> Void

init(_ completionHandler: @escaping () -> Void) {
self.completionHandler = completionHandler
}

func call() {
self.completionHandler()
}
}

@main
@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
Expand All @@ -27,6 +39,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
self.addBadgeToDock(controller: controller)

UNUserNotificationCenter.current().delegate = self
self.registerNotificationCategories()
Task {
do {
try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound])
Expand Down Expand Up @@ -161,6 +174,22 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
completionHandler([.banner, .sound])
}

nonisolated func userNotificationCenter(
_: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
let actionIdentifier = response.actionIdentifier
let controllerIdentifier = response.notification.request.content.userInfo[
MVNotificationUserInfoKeys.controllerIdentifier
] as? String
let completion = MVNotificationResponseCompletion(completionHandler)

DispatchQueue.main.async { [weak self, completion] in
self?.handleNotificationAction(actionIdentifier, controllerIdentifier: controllerIdentifier)
completion.call()
}
}
Comment thread
mookwoo marked this conversation as resolved.
Comment thread
mookwoo marked this conversation as resolved.

func addBadgeToDock(controller: MVTimerController) {
if self.currentlyInDock != controller {
self.removeBadgeFromDock()
Expand Down Expand Up @@ -199,6 +228,72 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
self.staysOnTop = UserDefaults.standard.bool(forKey: MVUserDefaultsKeys.staysOnTop)
}

private func handleNotificationAction(_ actionIdentifier: String, controllerIdentifier: String?) {
let controller = self.controller(matching: controllerIdentifier)

switch actionIdentifier {
case MVNotificationIdentifiers.restartTimerActionIdentifier:
controller.restartLastTimer()

case MVNotificationIdentifiers.addFiveMinutesActionIdentifier:
controller.addTime(seconds: CGFloat(5 * 60))

case MVNotificationIdentifiers.stopTimerActionIdentifier:
controller.clockView.paused = false
controller.clockView.stop()

case UNNotificationDefaultActionIdentifier:
controller.window?.makeKeyAndOrderFront(nil)
NSApplication.shared.activate(ignoringOtherApps: true)

default:
break
}
}

private func controller(matching identifier: String?) -> MVTimerController {
if let identifier,
let controller = self.controllers.first(where: { $0.identifier == identifier }) {
return controller
}

if let controller = self.controllers.first {
return controller
}

let controller = MVTimerController()
controller.window?.level = self.windowLevel
self.controllers.append(controller)
self.addBadgeToDock(controller: controller)
return controller
}
Comment thread
mookwoo marked this conversation as resolved.

private func registerNotificationCategories() {
let restartAction = UNNotificationAction(
identifier: MVNotificationIdentifiers.restartTimerActionIdentifier,
title: "Restart",
options: []
)
let addFiveMinutesAction = UNNotificationAction(
identifier: MVNotificationIdentifiers.addFiveMinutesActionIdentifier,
title: "+5 min",
options: []
)
let stopAction = UNNotificationAction(
identifier: MVNotificationIdentifiers.stopTimerActionIdentifier,
title: "Stop",
options: [.destructive]
)
let category = UNNotificationCategory(
identifier: MVNotificationIdentifiers.timerCompleteCategoryIdentifier,
actions: [restartAction, addFiveMinutesAction, stopAction],
intentIdentifiers: [],
options: []
)

UNUserNotificationCenter.current().setNotificationCategories([category])
}

private func observeNotifications() {
self.notificationTasks.append(
Task { [weak self] in
Expand Down
22 changes: 22 additions & 0 deletions Timer/MVTimerController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import AVFoundation
import UserNotifications

final class MVTimerController: NSWindowController {
let identifier = UUID().uuidString

private weak var dockMenuItem: NSMenuItem?
let clockView = MVClockView()

Expand Down Expand Up @@ -73,6 +75,8 @@ final class MVTimerController: NSWindowController {
private func handleClockTimer() {
let content = UNMutableNotificationContent()
content.title = "It's time! 🕘"
content.categoryIdentifier = MVNotificationIdentifiers.timerCompleteCategoryIdentifier
content.userInfo = [MVNotificationUserInfoKeys.controllerIdentifier: self.identifier]

let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
Expand Down Expand Up @@ -100,4 +104,22 @@ final class MVTimerController: NSWindowController {
self.soundURL = nil
}
}

func restartLastTimer() {
guard let seconds = self.clockView.lastTimerSeconds, seconds > 0 else { return }
self.clockView.startTimer(seconds: seconds)
}

func addTime(seconds: CGFloat) {
let currentSeconds = self.clockView.timerTask != nil || self.clockView.paused ? self.clockView.seconds : 0
self.clockView.startTimer(seconds: currentSeconds + seconds)
}

func resetTimer() {
self.clockView.paused = false
self.clockView.stop()
self.clockView.seconds = 0
self.clockView.updateTimerTime()
self.clockView.inputSeconds = false
}
}
11 changes: 11 additions & 0 deletions Timer/MVUserDefaultsKeys.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,14 @@ enum MVUserDefaultsKeys {
static let staysOnTop = "staysOnTop"
static let soundIndex = "soundIndex"
}

enum MVNotificationIdentifiers {
static let timerCompleteCategoryIdentifier = "timerComplete"
static let restartTimerActionIdentifier = "restartTimer"
static let addFiveMinutesActionIdentifier = "addFiveMinutes"
static let stopTimerActionIdentifier = "stopTimer"
}
Comment thread
mookwoo marked this conversation as resolved.

enum MVNotificationUserInfoKeys {
static let controllerIdentifier = "controllerIdentifier"
}