← back to dsavu09__pomodoro-app

Function bodies 13 total

All specs Real LLM only Function bodies
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: Ti
SoundManager 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: tr
MascotThemeTests 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: "T
SessionStoreTests 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: makeSessi
TimerEngineTests 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.isRunning
TimerStateTests 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: se
MascotAnimator 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