Function bodies 13 total
NotificationManager class · swift · L4-L66 (63 LOC)PomodoroApp/Engine/NotificationManager.swift
final class NotificationManager {
/// Request notification permission from the user.
func requestPermission() async throws {
let center = UNUserNotificationCenter.current()
try await center.requestAuthorization(options: [.alert, .sound, .badge])
}
/// Send an immediate local notification.
func sendNotification(title: String, body: String, sound: UNNotificationSound = .default) async throws {
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = sound
let request = UNNotificationRequest(
identifier: UUID().uuidString,
content: content,
trigger: nil // nil = deliver immediately
)
let center = UNUserNotificationCenter.current()
try await center.add(request)
}
/// Schedule a notification for a future time (e.g., "1 minute warning").
func scheduleNotification(
title: String,
SessionStore class · swift · L5-L101 (97 LOC)PomodoroApp/Engine/SessionStore.swift
final class SessionStore: ObservableObject {
@Published private(set) var sessions: [PomodoroSession] = []
private let storageKey = "pomodoro_sessions"
private let defaults = UserDefaults.standard
init() {
loadSessions()
}
// MARK: - CRUD
/// Save a completed session.
func save(session: PomodoroSession) {
sessions.insert(session, at: 0) // Most recent first
persist()
}
/// Clear all session history.
func clearAll() {
sessions.removeAll()
persist()
}
// MARK: - Queries
/// Sessions from today.
var todaySessions: [PomodoroSession] {
let calendar = Calendar.current
return sessions.filter { calendar.isDateInToday($0.startedAt) }
}
/// Completed work sessions from today.
var todayWorkSessions: [PomodoroSession] {
todaySessions.filter { $0.type == .work && $0.wasCompleted }
}
/// Total focus time today in seconds.
var todayFocusTime: TiSoundManager class · swift · L5-L73 (69 LOC)PomodoroApp/Engine/SoundManager.swift
final class SoundManager {
private var ambientPlayer: AVAudioPlayer?
// MARK: - Notification Sounds
/// Play the timer completion sound using the user's selected sound.
func playCompletionSound(named name: String = "default") {
let soundName = (name == "default") ? "Glass" : name
if let sound = NSSound(named: NSSound.Name(soundName)) {
sound.play()
}
}
/// Play a gentle tick sound (for optional ticking mode).
func playTickSound() {
if let sound = NSSound(named: "Tink") {
sound.play()
}
}
/// Play a named system sound.
func playSystemSound(named name: String) {
if let sound = NSSound(named: NSSound.Name(name)) {
sound.play()
}
}
// MARK: - Ambient Sounds
/// Available ambient sound files (bundled in Resources/).
static let ambientSounds = [
"rain",
"white_noise",
"lofi",
"forest",
"ocean"
]
TimerEngine class · swift · L6-L256 (251 LOC)PomodoroApp/Engine/TimerEngine.swift
final class TimerEngine: ObservableObject {
// MARK: - Published State
@Published private(set) var state: TimerState = .idle
@Published private(set) var timeRemaining: TimeInterval = 0
@Published private(set) var totalDuration: TimeInterval = 0
@Published private(set) var isRunning: Bool = false
@Published private(set) var completedWorkSessions: Int = 0
@Published var currentTaskLabel: String = ""
// MARK: - Dependencies
let settings: AppSettings
let notificationManager: NotificationManager
let soundManager: SoundManager
let sessionStore: SessionStore
// MARK: - Internal
private var timer: AnyCancellable?
private var sessionStartedAt: Date?
private let tickInterval: TimeInterval = 0.1 // Update 10x/sec for smooth progress ring
// MARK: - Computed Properties
/// Progress from 0.0 (just started) to 1.0 (time's up).
var progress: Double {
guard totalDuration > 0 else { return 0 }
return 1.AppSettings class · swift · L5-L51 (47 LOC)PomodoroApp/Models/AppSettings.swift
final class AppSettings: ObservableObject {
// MARK: - Timer Durations (in minutes)
@AppStorage("workDuration") var workDuration: Int = 25
@AppStorage("shortBreakDuration") var shortBreakDuration: Int = 5
@AppStorage("longBreakDuration") var longBreakDuration: Int = 15
@AppStorage("sessionsBeforeLongBreak") var sessionsBeforeLongBreak: Int = 4
// MARK: - Sound
@AppStorage("soundEnabled") var soundEnabled: Bool = true
@AppStorage("notificationSoundName") var notificationSoundName: String = "default"
@AppStorage("tickingSoundEnabled") var tickingSoundEnabled: Bool = false
// MARK: - Overlay
@AppStorage("overlayEnabled") var overlayEnabled: Bool = false
@AppStorage("overlayOpacity") var overlayOpacity: Double = 0.9
@AppStorage("overlaySize") var overlaySize: Double = 180 // width in points
// MARK: - Mascot
@AppStorage("selectedMascotThemeID") var selectedMascotThemeIDString: String = MascotTheme.default.id.uuidString
MascotEmotionTests class · swift · L4-L80 (77 LOC)PomodoroAppTests/MascotEmotionTests.swift
final class MascotEmotionTests: XCTestCase {
// MARK: - Emotion from Progress
func testRelaxedAtStart() {
let emotion = MascotEmotion.from(progress: 0.0, isBreak: false)
XCTAssertEqual(emotion, .relaxed)
}
func testFocusedAtQuarter() {
let emotion = MascotEmotion.from(progress: 0.30, isBreak: false)
XCTAssertEqual(emotion, .focused)
}
func testAlertAtHalf() {
let emotion = MascotEmotion.from(progress: 0.55, isBreak: false)
XCTAssertEqual(emotion, .alert)
}
func testStressedNearEnd() {
let emotion = MascotEmotion.from(progress: 0.80, isBreak: false)
XCTAssertEqual(emotion, .stressed)
}
func testStressedAtFull() {
let emotion = MascotEmotion.from(progress: 1.0, isBreak: false)
XCTAssertEqual(emotion, .stressed)
}
// MARK: - Break Always Celebrates
func testCelebratingDuringBreak() {
XCTAssertEqual(MascotEmotion.from(progress: 0.0, isBreak: trMascotThemeTests class · swift · L4-L45 (42 LOC)PomodoroAppTests/MascotThemeTests.swift
final class MascotThemeTests: XCTestCase {
func testThreeBuiltInThemes() {
XCTAssertEqual(MascotTheme.builtIn.count, 3)
}
func testDefaultIsPommy() {
XCTAssertEqual(MascotTheme.default.name, "Pommy")
XCTAssertEqual(MascotTheme.default.spriteAtlasName, "mascot_pommy")
}
func testAllThemesHaveUniqueIDs() {
let ids = MascotTheme.builtIn.map(\.id)
XCTAssertEqual(Set(ids).count, ids.count)
}
func testAllThemesHaveUniqueNames() {
let names = MascotTheme.builtIn.map(\.name)
XCTAssertEqual(Set(names).count, names.count)
}
func testAllThemesHaveUniqueSpriteAtlasNames() {
let atlasNames = MascotTheme.builtIn.map(\.spriteAtlasName)
XCTAssertEqual(Set(atlasNames).count, atlasNames.count)
}
func testDefaultStressThreshold() {
for theme in MascotTheme.builtIn {
XCTAssertEqual(theme.stressThreshold, 0.75)
}
}
func testCodable() throws {
About: code-quality intelligence by Repobility · https://repobility.com
PomodoroSessionTests class · swift · L4-L60 (57 LOC)PomodoroAppTests/PomodoroSessionTests.swift
final class PomodoroSessionTests: XCTestCase {
func testDurationIsCalculatedFromDates() {
let start = Date()
let end = start.addingTimeInterval(25 * 60) // 25 minutes
let session = PomodoroSession(
startedAt: start,
endedAt: end,
type: .work,
wasCompleted: true
)
XCTAssertEqual(session.duration, 25 * 60, accuracy: 0.01)
}
func testDefaultEndedAtIsNow() {
let start = Date()
let session = PomodoroSession(startedAt: start, type: .work)
// Duration should be ~0 since endedAt defaults to Date()
XCTAssertLessThan(session.duration, 1.0)
}
func testUniqueIDs() {
let a = PomodoroSession(startedAt: Date(), type: .work)
let b = PomodoroSession(startedAt: Date(), type: .work)
XCTAssertNotEqual(a.id, b.id)
}
func testTaskLabelOptional() {
let withLabel = PomodoroSession(startedAt: Date(), type: .work, taskLabel: "TSessionStoreTests class · swift · L4-L152 (149 LOC)PomodoroAppTests/SessionStoreTests.swift
final class SessionStoreTests: XCTestCase {
private func makeStore() -> SessionStore {
let store = SessionStore()
store.clearAll() // Start clean
return store
}
private func makeSession(
type: TimerState = .work,
startedAt: Date = Date(),
wasCompleted: Bool = true,
taskLabel: String? = nil
) -> PomodoroSession {
PomodoroSession(
startedAt: startedAt,
endedAt: startedAt.addingTimeInterval(25 * 60),
type: type,
taskLabel: taskLabel,
wasCompleted: wasCompleted
)
}
// MARK: - Save & Clear
func testSaveAndRetrieve() {
let store = makeStore()
let session = makeSession()
store.save(session: session)
XCTAssertEqual(store.sessions.count, 1)
XCTAssertEqual(store.sessions.first?.id, session.id)
}
func testClearAll() {
let store = makeStore()
store.save(session: makeSessiTimerEngineTests class · swift · L5-L220 (216 LOC)PomodoroAppTests/TimerEngineTests.swift
final class TimerEngineTests: XCTestCase {
private func makeEngine(
workDuration: Int = 1, // 1 min for fast tests
shortBreak: Int = 1,
longBreak: Int = 1,
sessionsBeforeLong: Int = 4,
autoStartBreaks: Bool = false,
autoStartWork: Bool = false
) -> TimerEngine {
let settings = AppSettings()
settings.workDuration = workDuration
settings.shortBreakDuration = shortBreak
settings.longBreakDuration = longBreak
settings.sessionsBeforeLongBreak = sessionsBeforeLong
settings.autoStartBreaks = autoStartBreaks
settings.autoStartWork = autoStartWork
settings.soundEnabled = false // Don't play sounds in tests
return TimerEngine(settings: settings, sessionStore: SessionStore())
}
// MARK: - Initial State
func testInitialStateIsIdle() {
let engine = makeEngine()
XCTAssertEqual(engine.state, .idle)
XCTAssertFalse(engine.isRunningTimerStateTests class · swift · L4-L46 (43 LOC)PomodoroAppTests/TimerStateTests.swift
final class TimerStateTests: XCTestCase {
// MARK: - Display Names
func testDisplayNames() {
XCTAssertEqual(TimerState.idle.displayName, "Ready")
XCTAssertEqual(TimerState.work.displayName, "Focus")
XCTAssertEqual(TimerState.shortBreak.displayName, "Short Break")
XCTAssertEqual(TimerState.longBreak.displayName, "Long Break")
}
// MARK: - isBreak
func testIsBreak() {
XCTAssertFalse(TimerState.idle.isBreak)
XCTAssertFalse(TimerState.work.isBreak)
XCTAssertTrue(TimerState.shortBreak.isBreak)
XCTAssertTrue(TimerState.longBreak.isBreak)
}
// MARK: - Default Durations
func testDefaultDurations() {
let settings = AppSettings()
settings.workDuration = 25
settings.shortBreakDuration = 5
settings.longBreakDuration = 15
XCTAssertEqual(TimerState.idle.defaultDuration(settings: settings), 0)
XCTAssertEqual(TimerState.work.defaultDuration(settings: seMascotAnimator class · swift · L9-L90 (82 LOC)PomodoroApp/Views/Mascot/MascotAnimator.swift
final class MascotAnimator: ObservableObject {
@Published private(set) var currentEmotion: MascotEmotion = .relaxed
@Published private(set) var isTransitioning: Bool = false
private var cancellables = Set<AnyCancellable>()
private var idleTimer: AnyCancellable?
/// Start observing a TimerEngine and driving emotion changes.
func bind(to timer: TimerEngine) {
// Observe emotion changes from the timer
timer.$state
.combineLatest(
timer.objectWillChange
.map { timer.progress }
.prepend(timer.progress)
)
.receive(on: DispatchQueue.main)
.sink { [weak self] state, progress in
guard let self else { return }
let newEmotion = MascotEmotion.from(
progress: progress,
isBreak: state.isBreak,
stressThreshold: timer.settings.mascotStressThreshold
)MascotScene class · swift · L4-L297 (294 LOC)PomodoroApp/Views/Mascot/MascotScene.swift
class MascotScene: SKScene {
// MARK: - Properties
private var mascotNode: SKSpriteNode?
private var currentEmotion: MascotEmotion = .relaxed
private var currentTheme: MascotTheme = .default
private var textureCache: [MascotEmotion: [SKTexture]] = [:]
// MARK: - Scene Lifecycle
override func didMove(to view: SKView) {
backgroundColor = .clear
view.allowsTransparency = true
setupMascot()
preloadTextures()
playAnimation(for: .relaxed)
}
// MARK: - Setup
private func setupMascot() {
let node = SKSpriteNode(color: .clear, size: CGSize(width: 80, height: 80))
node.position = CGPoint(x: size.width / 2, y: size.height / 2)
node.texture?.filteringMode = .nearest // Pixel art: no smoothing!
addChild(node)
mascotNode = node
}
/// Preload all texture frames for the current theme.
private func preloadTextures() {
textureCache.removeAll()
let