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
Binary file added Brisa/Assets/screenshots/brisa-break-screen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions Brisa/Brisa.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
939398DBB19571CA42D723CF /* PomodoroTimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4941FA5761A0A6F1528FB917 /* PomodoroTimerView.swift */; };
7C1E5A0B3D2F4A6981B0C4D7 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E9B6D4F1A8C3057B6E2A91C /* Theme.swift */; };
B4D80C2E6F1A93577E2D5A08 /* PomodoroWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A19F3C7E5B2D40886C1E7D35 /* PomodoroWidget.swift */; };
6B260CC4E10A47A498F5C5BE /* BreakScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB13E9A2139E4C0D8C77C6CC /* BreakScreen.swift */; };
7E4BD2D04E2F41B489898753 /* BreakActivitiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873BF2EBBF8E409DB7E971D5 /* BreakActivitiesView.swift */; };
17AE60E40130453B85CA5EB3 /* BreakActivities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E1FF787CE78443AB2D20FA8 /* BreakActivities.swift */; };
D3A61F9B2C8E47501A6B3E90 /* SystemIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F0C82E4A7B1396D0E4A7C21 /* SystemIntegration.swift */; };
8B2E4D7A1C5F60932E8A4B17 /* Confetti.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A91E3B5D2084F6A1B7E5C3 /* Confetti.swift */; };
3E7A5C1D9B4F2086A5D1C7E2 /* YouTubeVideos.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2B6D803A9E41C57B0D8E4A6 /* YouTubeVideos.swift */; };
Expand Down Expand Up @@ -56,6 +59,9 @@
4941FA5761A0A6F1528FB917 /* PomodoroTimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PomodoroTimerView.swift; sourceTree = "<group>"; };
2E9B6D4F1A8C3057B6E2A91C /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
A19F3C7E5B2D40886C1E7D35 /* PomodoroWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PomodoroWidget.swift; sourceTree = "<group>"; };
AB13E9A2139E4C0D8C77C6CC /* BreakScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreakScreen.swift; sourceTree = "<group>"; };
873BF2EBBF8E409DB7E971D5 /* BreakActivitiesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreakActivitiesView.swift; sourceTree = "<group>"; };
6E1FF787CE78443AB2D20FA8 /* BreakActivities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreakActivities.swift; sourceTree = "<group>"; };
5F0C82E4A7B1396D0E4A7C21 /* SystemIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemIntegration.swift; sourceTree = "<group>"; };
C7A91E3B5D2084F6A1B7E5C3 /* Confetti.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Confetti.swift; sourceTree = "<group>"; };
F2B6D803A9E41C57B0D8E4A6 /* YouTubeVideos.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YouTubeVideos.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -106,6 +112,9 @@
4941FA5761A0A6F1528FB917 /* PomodoroTimerView.swift */,
2E9B6D4F1A8C3057B6E2A91C /* Theme.swift */,
A19F3C7E5B2D40886C1E7D35 /* PomodoroWidget.swift */,
AB13E9A2139E4C0D8C77C6CC /* BreakScreen.swift */,
873BF2EBBF8E409DB7E971D5 /* BreakActivitiesView.swift */,
6E1FF787CE78443AB2D20FA8 /* BreakActivities.swift */,
5F0C82E4A7B1396D0E4A7C21 /* SystemIntegration.swift */,
C7A91E3B5D2084F6A1B7E5C3 /* Confetti.swift */,
F2B6D803A9E41C57B0D8E4A6 /* YouTubeVideos.swift */,
Expand Down Expand Up @@ -237,6 +246,9 @@
939398DBB19571CA42D723CF /* PomodoroTimerView.swift in Sources */,
7C1E5A0B3D2F4A6981B0C4D7 /* Theme.swift in Sources */,
B4D80C2E6F1A93577E2D5A08 /* PomodoroWidget.swift in Sources */,
6B260CC4E10A47A498F5C5BE /* BreakScreen.swift in Sources */,
7E4BD2D04E2F41B489898753 /* BreakActivitiesView.swift in Sources */,
17AE60E40130453B85CA5EB3 /* BreakActivities.swift in Sources */,
D3A61F9B2C8E47501A6B3E90 /* SystemIntegration.swift in Sources */,
8B2E4D7A1C5F60932E8A4B17 /* Confetti.swift in Sources */,
3E7A5C1D9B4F2086A5D1C7E2 /* YouTubeVideos.swift in Sources */,
Expand Down
52 changes: 26 additions & 26 deletions Brisa/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>Brisa Mix</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>local.brisa.mix</string>
</array>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>Brisa</string>
<key>CFBundleIconFile</key>
Expand All @@ -13,7 +28,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>1.2.0</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
Expand All @@ -25,32 +40,23 @@
</array>
</dict>
</array>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>Brisa Mix</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>local.brisa.mix</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>14</string>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeIdentifier</key>
<string>local.brisa.mix</string>
<key>UTTypeDescription</key>
<string>Brisa Mix</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.json</string>
</array>
<key>UTTypeDescription</key>
<string>Brisa Mix</string>
<key>UTTypeIdentifier</key>
<string>local.brisa.mix</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
Expand All @@ -60,11 +66,5 @@
</dict>
</dict>
</array>
<key>CFBundleVersion</key>
<string>14</string>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>
41 changes: 33 additions & 8 deletions Brisa/Sources/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,37 @@
# Source structure

- `AppEntry.swift` owns the SwiftUI app lifecycle and scene setup.
- `AppModel.swift` owns persisted user state, mix actions, playback state, and timer state.
- `SoundLibrary.swift` defines the available sounds and saved-mix value type.
- `AudioBank.swift` generates and plays ambient audio layers.
- `InputSounds.swift` manages optional keyboard and mouse feedback, including permissions.
- `ContentView.swift`, `WelcomeView.swift`, `InputSoundsView.swift`, `MixEditor.swift`, and `MenuBarPlayerView.swift` each own one SwiftUI surface.
## App and state

`AppModel` is the only place where user actions change playback, mixes, favorites, timer state, or persisted preferences. Services expose focused work only: `AudioBank` plays ambient layers and `InputSounds` handles the optional keyboard and mouse feedback.
- `Main.swift` starts the app; `AppEntry.swift` owns the SwiftUI scenes (main window, menu bar), the desktop mini player, and the Settings sheet.
- `AppModel.swift` owns persisted user state: playback, mixes, favourites, the sleep timer and the Pomodoro timer. `Countdown` holds the two values that change every second, so a running timer only redraws the views that show it.
- `Theme.swift` defines the themes and the shared `BrisaThemeStore`.
- `SystemIntegration.swift` handles media keys, Now Playing, sleep and wake, and launch at login.
- `BrisaIntents.swift` provides Shortcuts, Siri and Spotlight actions and the Focus filter.

The build script compiles every `Sources/*.swift` file. New application-level state belongs in `AppModel`; lifecycle code belongs in `AppEntry`; and focused services or view components can be added as separate files in `Sources/`.
## Sound

- `SoundLibrary.swift` defines the built-in sounds and the saved-mix type.
- `AudioBank.swift` prepares sound loops off the main thread and plays the mix, with crossfades and the living mix.
- `SoundSynthesis.swift` generates the noise colours and binaural tones.
- `ImportedSounds.swift` and `ImportSoundsView.swift` import local files and audio links; `YouTubeVideos.swift` plays YouTube links in the embedded player.
- `MixSharing.swift` turns mixes into `.brisamix` files and `brisa://mix` links, and validates what comes in.
- `InputSounds.swift` and `InputSoundsView.swift` handle the optional keyboard and mouse sounds, including permissions.

## Focus

- `PomodoroTimerView.swift` is the Pomodoro page; `PomodoroWidget.swift` is the desktop Pomodoro widget; `Confetti.swift` is the end-of-phase celebration.
- `BreakActivities.swift` holds the break activities, what was done in each break, and the break screen settings (`BreakStore`). `BreakScreen.swift` shows the full-screen break screen between Pomodoro phases, and `BreakActivitiesView.swift` edits it from the Pomodoro page.
- `Routines.swift` and `RoutinesView.swift` schedule and edit routines.
- `Modes.swift` and `ModesView.swift` open apps, arrange their windows and quiet distractions.

## Other surfaces

- `ContentView.swift` (main window), `MenuBarPlayerView.swift`, `MixEditor.swift` and `WelcomeView.swift` each own one SwiftUI surface.

## Conventions

User actions that change playback, mixes, timers or preferences go through `AppModel` (or its extensions in `Routines.swift`, `Modes.swift` and `MixSharing.swift`). Features with their own settings keep them in a small shared store next to the feature, such as `BreakStore` and `BrisaThemeStore`. Services expose focused work only: `AudioBank` plays ambient layers and `InputSounds` handles keyboard and mouse feedback.

Pure logic (scheduling, sharing, synthesis, mode planning, break suggestions) lives in types without app state so `Tests/` can cover it.

The build script compiles every `Sources/*.swift` file. A new file also needs adding to `Brisa.xcodeproj` so Xcode builds match.
28 changes: 23 additions & 5 deletions Brisa/Sources/AppEntry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,25 +71,42 @@ struct BrisaApp: App {
Button("Settings…") { NotificationCenter.default.post(name: Notification.Name("BrisaShowSettings"), object: nil) }
.keyboardShortcut(",")
}
// ⌘Q quits, like any Mac app. Closing the window (⌘W) keeps Brisa running in the menu bar.
CommandGroup(replacing: .appTermination) {
Button("Keep Running in Menu Bar") { BrisaWindowActions.moveToMenuBar() }
Button("Quit Brisa") { NSApp.terminate(nil) }
.keyboardShortcut("q")
Button("Quit Brisa Completely") { NSApp.terminate(nil) }
.keyboardShortcut("q", modifiers: [.command, .option])
}
}
MenuBarExtra {
MenuBarPlayerView(model: model)
} label: {
// While a Pomodoro is running (or paused mid-phase) the menu bar shows the countdown instead of the Brisa icon.
MenuBarLabel(model: model)
}
.menuBarExtraStyle(.window)
}
}

/// While a Pomodoro is running (or paused mid-phase) the menu bar shows the countdown instead of the Brisa icon.
private struct MenuBarLabel: View {
@ObservedObject var model: AppModel
@ObservedObject private var countdown = Countdown.shared
@Environment(\.openWindow) private var openWindow

var body: some View {
Group {
if model.isPomodoroRunning || model.pomodoroRemainingSeconds < model.pomodoroTotalSeconds {
Label(model.pomodoroTimeText, systemImage: model.isPomodoroRunning ? model.pomodoroPhase.symbol : "pause.fill")
.labelStyle(.titleAndIcon).monospacedDigit()
} else {
Image(systemName: "wind")
}
}
.menuBarExtraStyle(.window)
// The menu bar icon lives while the main window is closed, so it can reopen it (the window's own listener is gone then).
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("BrisaShowWindow"))) { _ in
guard !NSApp.windows.contains(where: { $0.title == "Brisa" && $0.isVisible }) else { return }
openWindow(id: "main")
}
}
}

Expand Down Expand Up @@ -229,6 +246,7 @@ private struct PlayerWaves: View {
private struct LiveBrisaPlayer: View {
@ObservedObject var model: AppModel
@ObservedObject private var desktop = BrisaDesktopPlayer.shared
@ObservedObject private var countdown = Countdown.shared
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var heldPhase = 0.0
@State private var lastRenderedPhase = 0.0
Expand Down Expand Up @@ -262,7 +280,7 @@ private struct LiveBrisaPlayer: View {
}.buttonStyle(.plain).accessibilityLabel("Close mini player")
}.foregroundStyle(mint)
Menu {
ForEach(["Noise", "Water", "Nature", "Spaces", "Imported"], id: \.self) { category in
ForEach(["Noise", "Water", "Nature", "Spaces", "Tones", "Imported"], id: \.self) { category in
Menu(category) {
ForEach(model.availableLibrary.filter { $0.category == category }) { sound in
Button { model.replaceWith(sound) } label: { Label(sound.name, systemImage: sound.icon) }
Expand Down
Loading