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
14 changes: 13 additions & 1 deletion App/Intents/StudyAppIntentServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)
}
Expand Down
26 changes: 26 additions & 0 deletions App/Intents/StudyAppIntents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
static let title: LocalizedStringResource = "Nächste Vorlesung"
static let description = IntentDescription("Sagt, welche Vorlesung gerade läuft oder als Nächstes ansteht.")

@Dependency private var services: StudyAppIntentServices

Check warning on line 9 in App/Intents/StudyAppIntents.swift

View workflow job for this annotation

GitHub Actions / Build & Tests (macOS)

'nonisolated' cannot be applied to mutable stored properties; this is an error in the Swift 6 language mode

Check warning on line 9 in App/Intents/StudyAppIntents.swift

View workflow job for this annotation

GitHub Actions / Build & Tests (macOS)

'nonisolated' cannot be applied to mutable stored properties; this is an error in the Swift 6 language mode

@MainActor
func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog {
Expand All @@ -20,7 +20,7 @@
static let title: LocalizedStringResource = "Fällige Karteikarten"
static let description = IntentDescription("Zählt die fälligen Karteikarten je Modul.")

@Dependency private var services: StudyAppIntentServices

Check warning on line 23 in App/Intents/StudyAppIntents.swift

View workflow job for this annotation

GitHub Actions / Build & Tests (macOS)

'nonisolated' cannot be applied to mutable stored properties; this is an error in the Swift 6 language mode

Check warning on line 23 in App/Intents/StudyAppIntents.swift

View workflow job for this annotation

GitHub Actions / Build & Tests (macOS)

'nonisolated' cannot be applied to mutable stored properties; this is an error in the Swift 6 language mode

@MainActor
func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog {
Expand All @@ -36,9 +36,9 @@
static let openAppWhenRun = true

@Parameter(title: "Modul")
var module: ModuleEntity

Check warning on line 39 in App/Intents/StudyAppIntents.swift

View workflow job for this annotation

GitHub Actions / Build & Tests (macOS)

'nonisolated' cannot be applied to mutable stored properties; this is an error in the Swift 6 language mode

Check warning on line 39 in App/Intents/StudyAppIntents.swift

View workflow job for this annotation

GitHub Actions / Build & Tests (macOS)

'nonisolated' cannot be applied to mutable stored properties; this is an error in the Swift 6 language mode

@Dependency private var services: StudyAppIntentServices

Check warning on line 41 in App/Intents/StudyAppIntents.swift

View workflow job for this annotation

GitHub Actions / Build & Tests (macOS)

'nonisolated' cannot be applied to mutable stored properties; this is an error in the Swift 6 language mode

Check warning on line 41 in App/Intents/StudyAppIntents.swift

View workflow job for this annotation

GitHub Actions / Build & Tests (macOS)

'nonisolated' cannot be applied to mutable stored properties; this is an error in the Swift 6 language mode

@MainActor
func perform() async throws -> some IntentResult {
Expand All @@ -47,6 +47,23 @@
}
}

/// „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?

Check warning on line 56 in App/Intents/StudyAppIntents.swift

View workflow job for this annotation

GitHub Actions / Build & Tests (macOS)

'nonisolated' cannot be applied to mutable stored properties; this is an error in the Swift 6 language mode

Check warning on line 56 in App/Intents/StudyAppIntents.swift

View workflow job for this annotation

GitHub Actions / Build & Tests (macOS)

'nonisolated' cannot be applied to mutable stored properties; this is an error in the Swift 6 language mode

@Dependency private var services: StudyAppIntentServices

Check warning on line 58 in App/Intents/StudyAppIntents.swift

View workflow job for this annotation

GitHub Actions / Build & Tests (macOS)

'nonisolated' cannot be applied to mutable stored properties; this is an error in the Swift 6 language mode

Check warning on line 58 in App/Intents/StudyAppIntents.swift

View workflow job for this annotation

GitHub Actions / Build & Tests (macOS)

'nonisolated' cannot be applied to mutable stored properties; this is an error in the Swift 6 language mode

@MainActor
func perform() async throws -> some IntentResult & ReturnsValue<String> & 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] {
Expand Down Expand Up @@ -77,5 +94,14 @@
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"
)
}
}
2 changes: 1 addition & 1 deletion App/StudyAppApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ struct StudyAppApp: App {
_todayViewModel = State(initialValue: today)
_assignmentsViewModel = State(initialValue: assignments)
_navigation = State(initialValue: navigation)
let intentServices = StudyAppIntentServices(container: container, navigation: navigation)
let intentServices = StudyAppIntentServices(container: container, studyTimer: studyTimer, navigation: navigation)
AppDependencyManager.shared.add(dependency: intentServices)
_studIPViewModel = State(initialValue: studIP)
let notifications = container.makeNotificationsViewModel()
Expand Down
10 changes: 10 additions & 0 deletions Presentation/StudyTimer/StudyTimerViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions StudyAppTests/StudyTimerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
1 change: 1 addition & 0 deletions docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
Loading