From c8383e0fa5da408427d6183f6fee9361f34f90f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 15:06:15 +0000 Subject: [PATCH 1/3] feat(learning): start the study timer from a shortcut startFromShortcut starts an idle timer for the given module, continues a paused one and leaves a running one alone, and says what happened. Refs #70 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TmmqmKmX8VDSfoPbzTS1ew --- .../StudyTimer/StudyTimerViewModel.swift | 10 ++++++++++ StudyAppTests/StudyTimerTests.swift | 17 +++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/Presentation/StudyTimer/StudyTimerViewModel.swift b/Presentation/StudyTimer/StudyTimerViewModel.swift index ba1590f..3a63f49 100644 --- a/Presentation/StudyTimer/StudyTimerViewModel.swift +++ b/Presentation/StudyTimer/StudyTimerViewModel.swift @@ -67,6 +67,16 @@ final class StudyTimerViewModel { await scheduleEndNotification() } + /// Starts or continues the timer for Shortcuts and Siri and returns what to say. A running timer keeps + /// running; the module only applies to a timer that was idle. + func startFromShortcut(moduleID: UUID?) async -> String { + if timer.isRunning { return "Der Lerntimer läuft schon: \(timer.phase.title), noch \(remainingText)." } + if !isActive, let moduleID { self.moduleID = moduleID } + let resumes = timer.isPaused + await start() + return "Lerntimer läuft\(resumes ? " weiter" : ""): \(timer.phase.title), noch \(remainingText)." + } + func pause() { now = clock() timer.pause(at: now) diff --git a/StudyAppTests/StudyTimerTests.swift b/StudyAppTests/StudyTimerTests.swift index b97e635..7460d21 100644 --- a/StudyAppTests/StudyTimerTests.swift +++ b/StudyAppTests/StudyTimerTests.swift @@ -183,6 +183,23 @@ struct StudyTimerViewModelTests { #expect(scheduler.scheduled[StudyNotification.studyTimerIdentifier] == nil) } + @Test func shortcutStartsAnIdleTimerForTheModuleAndKeepsARunningOne() async { + let viewModel = makeViewModel() + let module = UUID() + + #expect(await viewModel.startFromShortcut(moduleID: module) == "Lerntimer läuft: Fokus, noch 25:00.") + #expect(viewModel.moduleID == module) + + clock.now += 60 + viewModel.tick() + #expect(await viewModel.startFromShortcut(moduleID: UUID()) == "Der Lerntimer läuft schon: Fokus, noch 24:00.") + #expect(viewModel.moduleID == module) + + viewModel.pause() + #expect(await viewModel.startFromShortcut(moduleID: nil) == "Lerntimer läuft weiter: Fokus, noch 24:00.") + viewModel.reset() + } + @Test func formatsDurationsAndClock() { #expect(StudyTimerViewModel.durationText(45) == "45 Min.") #expect(StudyTimerViewModel.durationText(120) == "2 Std.") From 46487a4a9d74e103a12608230a856c27688fa372 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 15:06:15 +0000 Subject: [PATCH 2/3] =?UTF-8?q?feat(intents):=20add=20the=20=E2=80=9ELernt?= =?UTF-8?q?imer=20starten=E2=80=9C=20action?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Shortcuts action and Siri phrase start the app's study timer through StudyAppIntentServices, optionally for a module. Refs #70 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TmmqmKmX8VDSfoPbzTS1ew --- App/Intents/StudyAppIntentServices.swift | 14 ++++++++++++- App/Intents/StudyAppIntents.swift | 26 ++++++++++++++++++++++++ App/StudyAppApp.swift | 4 ++-- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/App/Intents/StudyAppIntentServices.swift b/App/Intents/StudyAppIntentServices.swift index 5380ada..7788d4c 100644 --- a/App/Intents/StudyAppIntentServices.swift +++ b/App/Intents/StudyAppIntentServices.swift @@ -8,13 +8,20 @@ final class StudyAppIntentServices { private let modules: any ModuleRepository private let calendar: any StudyCalendarRepository private let flashcards: any FlashcardRepository + private let studyTimer: StudyTimerViewModel private let navigation: AppNavigation private let now: () -> Date - init(container: AppContainer, navigation: AppNavigation, now: @escaping () -> Date = { .now }) { + init( + container: AppContainer, + studyTimer: StudyTimerViewModel, + navigation: AppNavigation, + now: @escaping () -> Date = { .now } + ) { modules = container.moduleRepository calendar = container.studyCalendarRepository flashcards = container.flashcardRepository + self.studyTimer = studyTimer self.navigation = navigation self.now = now } @@ -32,6 +39,11 @@ final class StudyAppIntentServices { DueCardsUseCase.text(try DueCardsUseCase(modules: modules, flashcards: flashcards)(now: now())) } + /// Uses the app's one timer, so the toolbar and the menu bar show it running. + func startStudyTimer(moduleID: UUID?) async -> String { + await studyTimer.startFromShortcut(moduleID: moduleID) + } + func openFlashcards(moduleID: UUID) { navigation.show(module: moduleID, section: .flashcards) } diff --git a/App/Intents/StudyAppIntents.swift b/App/Intents/StudyAppIntents.swift index a31ecbc..ab30f4e 100644 --- a/App/Intents/StudyAppIntents.swift +++ b/App/Intents/StudyAppIntents.swift @@ -47,6 +47,23 @@ nonisolated struct OpenFlashcardsIntent: AppIntent { } } +/// „Starte den Lerntimer“: starts the study timer, optionally for a module. +nonisolated struct StartStudyTimerIntent: AppIntent { + static let title: LocalizedStringResource = "Lerntimer starten" + static let description = IntentDescription("Startet den Lerntimer in StudyApp, auf Wunsch für ein Modul.") + + @Parameter(title: "Modul") + var module: ModuleEntity? + + @Dependency private var services: StudyAppIntentServices + + @MainActor + func perform() async throws -> some IntentResult & ReturnsValue & ProvidesDialog { + let text = await services.startStudyTimer(moduleID: module?.id) + return .result(value: text, dialog: IntentDialog(stringLiteral: text)) + } +} + /// Siri phrases and the actions shown in the Shortcuts app without any setup. nonisolated struct StudyAppShortcuts: AppShortcutsProvider { static var appShortcuts: [AppShortcut] { @@ -77,5 +94,14 @@ nonisolated struct StudyAppShortcuts: AppShortcutsProvider { shortTitle: "Karteikarten lernen", systemImageName: "brain.head.profile" ) + AppShortcut( + intent: StartStudyTimerIntent(), + phrases: [ + "Starte den Lerntimer in \(.applicationName)", + "Lerntimer in \(.applicationName) starten", + ], + shortTitle: "Lerntimer starten", + systemImageName: "timer" + ) } } diff --git a/App/StudyAppApp.swift b/App/StudyAppApp.swift index 5cba309..80a098e 100644 --- a/App/StudyAppApp.swift +++ b/App/StudyAppApp.swift @@ -59,7 +59,8 @@ struct StudyAppApp: App { _todayViewModel = State(initialValue: today) _assignmentsViewModel = State(initialValue: assignments) _navigation = State(initialValue: navigation) - let intentServices = StudyAppIntentServices(container: container, navigation: navigation) + let studyTimer = container.makeStudyTimerViewModel() + let intentServices = StudyAppIntentServices(container: container, studyTimer: studyTimer, navigation: navigation) AppDependencyManager.shared.add(dependency: intentServices) _studIPViewModel = State(initialValue: studIP) let notifications = container.makeNotificationsViewModel() @@ -73,7 +74,6 @@ struct StudyAppApp: App { } responder.install() notificationResponder = responder - let studyTimer = container.makeStudyTimerViewModel() let backup = container.makeBackupViewModel() // After a restore every screen shows the restored data. backup.onRestored = { From 1f1f301a1bc5acc0804ba2b9f20aa74416e83508 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 15:06:15 +0000 Subject: [PATCH 3/3] =?UTF-8?q?docs(intents):=20list=20the=20=E2=80=9ELern?= =?UTF-8?q?timer=20starten=E2=80=9C=20action?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #70 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TmmqmKmX8VDSfoPbzTS1ew --- docs/FEATURES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 902d000..3c69197 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -97,6 +97,7 @@ StudyApp offers actions in the Shortcuts app and to Siri without any setup: | Nächste Vorlesung | „Was habe ich als Nächstes in StudyApp?“ | Spoken/returned text, e.g. „Als Nächstes: Analysis I, heute um 10:15 in HS 1.“ | | Fällige Karteikarten | „Fällige Karten in StudyApp“ | „24 Karten fällig: Analysis I 12, …“ | | Karteikarten lernen | „Starte Karteikarten Analysis I in StudyApp“ | Opens the module's flashcards | +| Lerntimer starten | „Starte den Lerntimer in StudyApp“ | Starts or continues the study timer, optionally for a module: „Lerntimer läuft: Fokus, noch 25:00.“ | Code: `App/Intents/`, `Domain/UseCases/QuickInfoUseCases.swift`