From 12e7a17f7dce7f202ac313feb29173a11785d284 Mon Sep 17 00:00:00 2001 From: Oleksandr Riasnyi Date: Fri, 7 Aug 2026 22:57:21 +0300 Subject: [PATCH 1/4] Fix UI overlap bugs in MonitorPane and WeatherPane --- Sources/VoidBar/UI/MonitorPane.swift | 50 +++++++++++++--------------- Sources/VoidBar/UI/WeatherPane.swift | 11 +++--- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/Sources/VoidBar/UI/MonitorPane.swift b/Sources/VoidBar/UI/MonitorPane.swift index 116deaf..40c3dc4 100644 --- a/Sources/VoidBar/UI/MonitorPane.swift +++ b/Sources/VoidBar/UI/MonitorPane.swift @@ -36,37 +36,35 @@ struct MonitorPane: View { .frame(width: 40, height: 40) } .padding(.horizontal, 16) - } - .padding(.vertical, 16) - - HStack { - VStack(alignment: .leading, spacing: 4) { - Text("Network") - .font(.system(size: 10, weight: .bold)) - .foregroundStyle(Theme.secondary) - - HStack(spacing: 12) { - HStack(spacing: 4) { - Image(systemName: "arrow.down.circle.fill") - .foregroundStyle(Theme.tertiary) - Text(formatSpeed(monitor.networkDownloadSpeed)) - .font(.system(size: 14, weight: .medium).monospacedDigit()) - .foregroundStyle(.white) - } + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("Network") + .font(.system(size: 10, weight: .bold)) + .foregroundStyle(Theme.secondary) - HStack(spacing: 4) { - Image(systemName: "arrow.up.circle.fill") - .foregroundStyle(Theme.tertiary) - Text(formatSpeed(monitor.networkUploadSpeed)) - .font(.system(size: 14, weight: .medium).monospacedDigit()) - .foregroundStyle(.white) + HStack(spacing: 12) { + HStack(spacing: 4) { + Image(systemName: "arrow.down.circle.fill") + .foregroundStyle(Theme.tertiary) + Text(formatSpeed(monitor.networkDownloadSpeed)) + .font(.system(size: 14, weight: .medium).monospacedDigit()) + .foregroundStyle(.white) + } + + HStack(spacing: 4) { + Image(systemName: "arrow.up.circle.fill") + .foregroundStyle(Theme.tertiary) + Text(formatSpeed(monitor.networkUploadSpeed)) + .font(.system(size: 14, weight: .medium).monospacedDigit()) + .foregroundStyle(.white) + } } } + Spacer() } - Spacer() + .padding(.horizontal, 16) } - .padding(.horizontal, 16) - .padding(.bottom, 16) + .padding(.vertical, 16) } private func formatSpeed(_ bytesPerSecond: Double) -> String { diff --git a/Sources/VoidBar/UI/WeatherPane.swift b/Sources/VoidBar/UI/WeatherPane.swift index 14042f6..a6a05fa 100644 --- a/Sources/VoidBar/UI/WeatherPane.swift +++ b/Sources/VoidBar/UI/WeatherPane.swift @@ -4,7 +4,7 @@ struct WeatherPane: View { @ObservedObject var weatherStore: WeatherStore var body: some View { - VStack(spacing: 8) { + VStack(spacing: 4) { if let error = weatherStore.error { Text(error) .font(.system(size: 11, weight: .medium)) @@ -12,11 +12,11 @@ struct WeatherPane: View { .multilineTextAlignment(.center) } else if let weather = weatherStore.weather { Image(systemName: icon(for: weather.condition)) - .font(.system(size: 36, weight: .light)) + .font(.system(size: 32, weight: .light)) .foregroundStyle(.white) Text(String(format: "%.1f°", weather.temperature)) - .font(.system(size: 24, weight: .semibold)) + .font(.system(size: 22, weight: .semibold)) .foregroundStyle(.white) if let location = weather.locationName { @@ -26,7 +26,6 @@ struct WeatherPane: View { } if !weather.hourly.isEmpty { - Spacer(minLength: 8) ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 16) { ForEach(weather.hourly) { hour in @@ -35,7 +34,7 @@ struct WeatherPane: View { } .padding(.horizontal, 16) } - .frame(height: 60) + .frame(height: 50) } } else { ProgressView() @@ -43,7 +42,7 @@ struct WeatherPane: View { } } .frame(maxWidth: .infinity, maxHeight: .infinity) - .padding(.vertical, 16) + .padding(.vertical, 8) } private func icon(for code: Int) -> String { From 601766d85d13b634e84182ec2ea73c17ec251565 Mon Sep 17 00:00:00 2001 From: Oleksandr Riasnyi Date: Fri, 7 Aug 2026 23:05:58 +0300 Subject: [PATCH 2/4] chore: prepare repository for open source release --- .github/ISSUE_TEMPLATE/bug_report.md | 31 +++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++ .github/PULL_REQUEST_TEMPLATE.md | 26 +++++++ CODE_OF_CONDUCT.md | 75 +++++++++++++++++++++ CONTRIBUTING.md | 42 ++++++++++++ README.md | 4 ++ README.uk.md | 4 ++ Sources/VoidBar/Services/NoteStore.swift | 2 - Sources/VoidBar/Services/SnippetStore.swift | 6 +- 9 files changed, 204 insertions(+), 6 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..8f327dd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - macOS version: [e.g. 15.0] + - App version: [e.g. 2.5.0] + +**Additional context** +Add any other context about the problem here. (e.g. Crash logs, Console errors) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..11fc491 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..f407549 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,26 @@ +## Description + +Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. + +Fixes # (issue) + +## Type of change + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update + +## How Has This Been Tested? + +Please describe the tests that you ran to verify your changes. +- [ ] Tested locally on macOS 15+ +- [ ] Verify there are no visual glitches or overlaps in the UI + +## Checklist: + +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation (if applicable) +- [ ] My changes generate no new warnings in Xcode diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..2bb3e31 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,75 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f0bb5bd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,42 @@ +# Contributing to VoidBar + +Thank you for your interest in contributing to VoidBar! We welcome contributions of all kinds: bug reports, feature requests, and pull requests. + +## Development Setup + +To build and run VoidBar locally, you will need: +- A Mac running **macOS 15** or later. +- The **Swift 6** toolchain (via Xcode 16+). + +1. Clone the repository: + ```bash + git clone https://github.com/xand0dev/voidbar.git + cd voidbar + ``` + +2. Build the project: + VoidBar uses a custom build script that handles compilation, assembling the bundle, compiling the media helper, localization, and ad-hoc signing. + ```bash + ./Scripts/bundle.sh + ``` + +3. Run the app: + ```bash + open build/VoidBar.app + ``` + +## Pull Request Process + +1. **Fork the repository** and create your branch from `main`. +2. **Ensure it builds**: Run `./Scripts/bundle.sh` locally and verify the app opens and works without crashing. +3. **Check Code Style**: Try to follow the existing code style in the project. VoidBar uses Swift formatting conventions. +4. **Test your changes**: Please test your feature or bug fix manually since the project heavily relies on AppKit and SwiftUI UI components. +5. **Open a PR**: Fill out the provided Pull Request template. + +## Issues + +- Before opening an issue, please check if a similar issue already exists. +- Use the provided Issue Templates for bug reports and feature requests. +- Provide as much context as possible, including OS version and screenshots. + +We are excited to see what you build! diff --git a/README.md b/README.md index d34aa1e..224aebb 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,10 @@ VoidBar stays hidden until you hover over the notch area. When triggered, it gra - 📝 **Quick Notes & Snippets:** Keep a library of frequently used texts (like your email or phone number) and a scratchpad for quick, temporary notes. - 🗓️ **Meeting Tracker:** Connects to Calendar to show your next meeting and provides a one-click join button for Zoom, Teams, Meet, and others. - 🌍 **Offline Translator:** Built-in offline translation leveraging macOS's native `Translation.framework`. +- ✅ **TickTick Integration:** Connects with an iCal link to manage and prioritize your daily tasks directly from the notch. +- 🍅 **Pomodoro Timer:** Stay focused with built-in presets (5m, 10m, 25m, 50m) and a daily pomodoro completion tracker. +- 📊 **System Monitor:** Keep an eye on your Mac's performance with CPU, Memory, and live Network speed stats. +- ⛅️ **Weather:** View current local weather and a detailed 24-hour horizontal forecast. ## Installation & Setup diff --git a/README.uk.md b/README.uk.md index 8206e7b..44048c6 100644 --- a/README.uk.md +++ b/README.uk.md @@ -24,6 +24,10 @@ VoidBar залишається невидимим, поки ви не навед - 📝 **Нотатки та Шаблони:** Зберігайте часто використовувані тексти (наприклад, пошту чи номер телефону) або робіть швидкі тимчасові нотатки. - 🗓️ **Календар зустрічей:** Підключається до вашого календаря, щоб показати наступну зустріч, та дає кнопку для швидкого приєднання до Zoom, Teams, Meet тощо. - 🌍 **Офлайн-перекладач:** Вбудований перекладач, що працює без інтернету на базі нативного `Translation.framework`. +- ✅ **Інтеграція з TickTick:** Підключіть iCal-посилання для керування щоденними завданнями та пріоритетами прямо з панелі. +- 🍅 **Таймер Pomodoro:** Залишайтеся зосередженими завдяки вбудованим пресетам (5хв, 10хв, 25хв, 50хв) та відстеженню виконаних сесій за день. +- 📊 **Моніторинг Системи:** Слідкуйте за продуктивністю вашого Mac (ЦП, Пам'ять) та швидкістю мережі в реальному часі. +- ⛅️ **Погода:** Переглядайте поточну локальну погоду та детальний горизонтальний прогноз на 24 години. ## Встановлення diff --git a/Sources/VoidBar/Services/NoteStore.swift b/Sources/VoidBar/Services/NoteStore.swift index 8200cd8..ab964f2 100644 --- a/Sources/VoidBar/Services/NoteStore.swift +++ b/Sources/VoidBar/Services/NoteStore.swift @@ -25,7 +25,6 @@ final class NoteStore: ObservableObject { /// choice survives the pane being unmounted with the panel. @Published var selected: Note.ID? - // F-07 FIX: Use VoidBar directory. private static let file: URL = { let fm = FileManager.default let folder = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] @@ -34,7 +33,6 @@ final class NoteStore: ObservableObject { return folder.appendingPathComponent("notes.json") }() - // F-07 FIX: Enforce 0600 file permissions on persist. static func enforcePermissions() { let attributes: [FileAttributeKey: Any] = [.posixPermissions: 0o600] try? FileManager.default.setAttributes(attributes, ofItemAtPath: file.path) diff --git a/Sources/VoidBar/Services/SnippetStore.swift b/Sources/VoidBar/Services/SnippetStore.swift index 24adaa8..7018799 100644 --- a/Sources/VoidBar/Services/SnippetStore.swift +++ b/Sources/VoidBar/Services/SnippetStore.swift @@ -48,18 +48,16 @@ final class SnippetStore: ObservableObject { } /// `~/Library/Application Support/VoidBar/snippets.json`. A plain array of - /// `{"label": "...", "text": "..."}`, where `label` may be left out. - // F-07 FIX: Use VoidBar directory. + /// the texts. The order is preserved. `label` may be left out. static let file: URL = { let fm = FileManager.default - let folder = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] + let folder = try! FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) .appendingPathComponent("VoidBar", isDirectory: true) try? fm.createDirectory(at: folder, withIntermediateDirectories: true) let fileURL = folder.appendingPathComponent("snippets.json") return fileURL }() - // F-07 FIX: Enforce 0600 file permissions on persist. static func enforcePermissions() { let attributes: [FileAttributeKey: Any] = [.posixPermissions: 0o600] try? FileManager.default.setAttributes(attributes, ofItemAtPath: file.path) From 3ee37acbc6504337c3ec2a2455b97721e77f98aa Mon Sep 17 00:00:00 2001 From: Oleksandr Riasnyi Date: Sat, 8 Aug 2026 10:09:14 +0300 Subject: [PATCH 3/4] feat: customizable tabs and settings UI --- Sources/VoidBar/App/AppDelegate.swift | 14 ++++ .../App/SettingsWindowController.swift | 35 ++++++++++ Sources/VoidBar/Model/NotchViewModel.swift | 10 +-- Sources/VoidBar/Model/TabManager.swift | 66 +++++++++++++++++++ Sources/VoidBar/Notch/NotchController.swift | 4 ++ Sources/VoidBar/UI/NotchContentView.swift | 4 +- Sources/VoidBar/UI/SettingsView.swift | 42 ++++++++++++ 7 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 Sources/VoidBar/App/SettingsWindowController.swift create mode 100644 Sources/VoidBar/Model/TabManager.swift create mode 100644 Sources/VoidBar/UI/SettingsView.swift diff --git a/Sources/VoidBar/App/AppDelegate.swift b/Sources/VoidBar/App/AppDelegate.swift index 2ab9bf4..10f2e52 100644 --- a/Sources/VoidBar/App/AppDelegate.swift +++ b/Sources/VoidBar/App/AppDelegate.swift @@ -43,6 +43,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { toggle.target = self menu.addItem(toggle) + let prefs = NSMenuItem( + title: localized("Preferences..."), + action: #selector(openPreferences), + keyEquivalent: "," + ) + prefs.target = self + menu.addItem(prefs) + let login = NSMenuItem( title: localized("Launch at Login"), action: #selector(toggleLaunchAtLogin), @@ -102,6 +110,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { @objc private func togglePanel() { controller?.toggle() } + + @objc private func openPreferences() { + if let tabManager = controller?.tabManager { + SettingsWindowController.shared.show(tabManager: tabManager) + } + } /// The size is measured when the menu opens, not kept fresh in between: a /// folder nobody is looking at deserves no bookkeeping. diff --git a/Sources/VoidBar/App/SettingsWindowController.swift b/Sources/VoidBar/App/SettingsWindowController.swift new file mode 100644 index 0000000..045a8b3 --- /dev/null +++ b/Sources/VoidBar/App/SettingsWindowController.swift @@ -0,0 +1,35 @@ +import AppKit +import SwiftUI + +final class SettingsWindowController: NSWindowController { + static let shared = SettingsWindowController() + + private init() { + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 400, height: 500), + styleMask: [.titled, .closable, .miniaturizable], + backing: .buffered, + defer: false + ) + window.title = "VoidBar Preferences" + window.center() + window.setFrameAutosaveName("VoidBarPreferencesWindow") + window.isReleasedWhenClosed = false + super.init(window: window) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func show(tabManager: TabManager) { + if window?.contentViewController == nil { + let settingsView = SettingsView(tabManager: tabManager) + let hostingController = NSHostingController(rootView: settingsView) + window?.contentViewController = hostingController + } + + window?.makeKeyAndOrderFront(nil) + NSApp.activate(ignoringOtherApps: true) + } +} diff --git a/Sources/VoidBar/Model/NotchViewModel.swift b/Sources/VoidBar/Model/NotchViewModel.swift index 332514d..eb8418c 100644 --- a/Sources/VoidBar/Model/NotchViewModel.swift +++ b/Sources/VoidBar/Model/NotchViewModel.swift @@ -3,7 +3,7 @@ import Combine @MainActor final class NotchViewModel: ObservableObject { - enum Tab: String, CaseIterable, Identifiable { + enum Tab: String, CaseIterable, Identifiable, Codable { case media, shelf, clipboard, snippets, calendar, timer, translate, notes, teleprompter, monitor, weather, tasks var id: String { rawValue } @@ -45,14 +45,10 @@ final class NotchViewModel: ObservableObject { /// that arriving and typing is a single move. var needsKeyboard: Bool { self == .translate || self == .snippets || self == .notes || self == .teleprompter } - /// Which rail the icon sits on. The left one carries the original six - /// and is full — a seventh icon would outgrow the height the panel - /// body has — so growth continues in a second column on the right, - /// which the scratch notes open. - static let leftRail: [Tab] = [.media, .shelf, .clipboard, .snippets, .calendar, .timer, .translate] - static let rightRail: [Tab] = [.notes, .tasks, .teleprompter, .monitor, .weather] } + @Published var tabManager = TabManager() + @Published var isOpen = false { didSet { if isOpen != oldValue { diff --git a/Sources/VoidBar/Model/TabManager.swift b/Sources/VoidBar/Model/TabManager.swift new file mode 100644 index 0000000..725fed1 --- /dev/null +++ b/Sources/VoidBar/Model/TabManager.swift @@ -0,0 +1,66 @@ +import SwiftUI +import Combine + +@MainActor +final class TabManager: ObservableObject { + struct TabConfig: Codable, Identifiable, Equatable { + var id: NotchViewModel.Tab + var isEnabled: Bool + } + + @Published var configs: [TabConfig] = [] { + didSet { + save() + } + } + + private let key = "voidbar.tabs.config" + + init() { + load() + } + + private func load() { + if let data = UserDefaults.standard.data(forKey: key), + let saved = try? JSONDecoder().decode([TabConfig].self, from: data) { + var merged = saved + let savedIds = Set(saved.map { $0.id }) + for tab in NotchViewModel.Tab.allCases { + if !savedIds.contains(tab) { + merged.append(TabConfig(id: tab, isEnabled: true)) + } + } + // Remove any tabs that no longer exist (if they were removed from the enum, though decoding would fail anyway) + self.configs = merged + } else { + // Default configuration + self.configs = NotchViewModel.Tab.allCases.map { + TabConfig(id: $0, isEnabled: $0 != .monitor) + } + } + } + + private func save() { + if let data = try? JSONEncoder().encode(configs) { + UserDefaults.standard.set(data, forKey: key) + } + } + + var activeTabs: [NotchViewModel.Tab] { + configs.filter { $0.isEnabled }.map { $0.id } + } + + var leftRail: [NotchViewModel.Tab] { + let active = activeTabs + if active.isEmpty { return [] } + let mid = Int(ceil(Double(active.count) / 2.0)) + return Array(active.prefix(mid)) + } + + var rightRail: [NotchViewModel.Tab] { + let active = activeTabs + if active.count <= 1 { return [] } + let mid = Int(ceil(Double(active.count) / 2.0)) + return Array(active.suffix(from: mid)) + } +} diff --git a/Sources/VoidBar/Notch/NotchController.swift b/Sources/VoidBar/Notch/NotchController.swift index 663db68..62b41eb 100644 --- a/Sources/VoidBar/Notch/NotchController.swift +++ b/Sources/VoidBar/Notch/NotchController.swift @@ -13,6 +13,10 @@ final class NotchController { /// Monotonic stamp for the deferred half of closing: any newer open or /// close outdates the one still in flight. private var openGeneration = 0 + + var tabManager: TabManager? { + viewModel?.tabManager + } func install() { build() diff --git a/Sources/VoidBar/UI/NotchContentView.swift b/Sources/VoidBar/UI/NotchContentView.swift index fcbf70e..45f9a2d 100644 --- a/Sources/VoidBar/UI/NotchContentView.swift +++ b/Sources/VoidBar/UI/NotchContentView.swift @@ -136,9 +136,9 @@ struct NotchContentView: View { private var content: some View { HStack(spacing: 14) { - Rail(vm: vm, tabs: NotchViewModel.Tab.leftRail) + Rail(vm: vm, tabs: vm.tabManager.leftRail) panes - Rail(vm: vm, tabs: NotchViewModel.Tab.rightRail) + Rail(vm: vm, tabs: vm.tabManager.rightRail) } .padding(.horizontal, 14) .padding(.bottom, 14) diff --git a/Sources/VoidBar/UI/SettingsView.swift b/Sources/VoidBar/UI/SettingsView.swift new file mode 100644 index 0000000..5b2080a --- /dev/null +++ b/Sources/VoidBar/UI/SettingsView.swift @@ -0,0 +1,42 @@ +import SwiftUI + +struct SettingsView: View { + @ObservedObject var tabManager: TabManager + + var body: some View { + VStack(spacing: 0) { + Text("VoidBar Preferences") + .font(.headline) + .padding() + + Text("Drag and drop to reorder tabs. Use the toggle to show or hide a tab in the notch.") + .font(.caption) + .foregroundStyle(.secondary) + .padding(.horizontal) + .padding(.bottom, 8) + + List { + ForEach($tabManager.configs) { $config in + HStack { + Image(systemName: config.id.symbol) + .frame(width: 24, alignment: .center) + .foregroundStyle(.secondary) + + Text(config.id.title) + + Spacer() + + Toggle("", isOn: $config.isEnabled) + .toggleStyle(.switch) + } + .padding(.vertical, 4) + } + .onMove { indices, newOffset in + tabManager.configs.move(fromOffsets: indices, toOffset: newOffset) + } + } + .listStyle(.inset) + } + .frame(width: 400, height: 500) + } +} From d1a7e4554f72c2ae85e31e388182d16b213410ef Mon Sep 17 00:00:00 2001 From: Oleksandr Riasnyi Date: Sat, 8 Aug 2026 12:34:58 +0300 Subject: [PATCH 4/4] chore: complete open-source readiness --- .github/CODEOWNERS | 1 + .github/ISSUE_TEMPLATE/bug_report.md | 31 ------ .github/ISSUE_TEMPLATE/bug_report.yml | 66 +++++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 ++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ---- .github/ISSUE_TEMPLATE/feature_request.yml | 41 +++++++ .github/PULL_REQUEST_TEMPLATE.md | 34 +++--- .github/workflows/build.yml | 79 ++++++++----- .gitignore | 2 +- CHANGELOG.md | 61 ++++++++++ CODE_OF_CONDUCT.md | 5 +- CONTRIBUTING.md | 85 +++++++++----- PRIVACY.md | 48 ++++++++ README.md | 107 +++++++++++------- README.uk.md | 111 ++++++++++++------- Resources/en.lproj/Localizable.strings | 4 +- Resources/uk.lproj/Localizable.strings | 4 +- SECURITY.md | 56 ++++------ SUPPORT.md | 14 +++ Sources/VoidBar/Model/NotchViewModel.swift | 9 +- Sources/VoidBar/Services/CalendarStore.swift | 5 +- Sources/VoidBar/Services/TimerStore.swift | 32 +++++- Sources/VoidBar/Services/WeatherStore.swift | 4 +- Sources/VoidBar/UI/CalendarPane.swift | 2 +- docs/releases/0.3.0.md | 2 +- docs/releases/0.4.0.md | 2 +- docs/releases/0.5.0.md | 2 +- docs/releases/0.5.1.md | 2 +- 28 files changed, 575 insertions(+), 262 deletions(-) create mode 100644 .github/CODEOWNERS delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 CHANGELOG.md create mode 100644 PRIVACY.md create mode 100644 SUPPORT.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..c6688cc --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @xand0dev diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 8f327dd..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: bug -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - - macOS version: [e.g. 15.0] - - App version: [e.g. 2.5.0] - -**Additional context** -Add any other context about the problem here. (e.g. Crash logs, Console errors) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..7688ffb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,66 @@ +name: Bug report +description: Report a reproducible problem in VoidBar +title: "[Bug]: " +labels: [bug] +body: + - type: markdown + attributes: + value: | + Thanks for helping improve VoidBar. Search existing issues first. For a security vulnerability, stop here and follow `SECURITY.md` instead. + - type: input + id: version + attributes: + label: VoidBar version or commit + description: Use the app version from the menu bar or a commit SHA for a source build. + placeholder: v0.5.1 or 3ee37ac + validations: + required: true + - type: input + id: macos + attributes: + label: macOS and Mac model + placeholder: macOS 15.6, MacBook Pro 14-inch (M3 Pro) + validations: + required: true + - type: dropdown + id: build + attributes: + label: Installation method + options: + - Built from source + - GitHub release + - Other + validations: + required: true + - type: textarea + id: problem + attributes: + label: What happened? + description: Describe the problem and what you expected instead. + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Steps to reproduce + placeholder: | + 1. Open … + 2. Select … + 3. Observe … + validations: + required: true + - type: textarea + id: evidence + attributes: + label: Logs, screenshots, or recordings + description: Remove private clipboard, calendar, file path, and TickTick URL data before uploading. + render: shell + - type: checkboxes + id: checks + attributes: + label: Checklist + options: + - label: I searched for an existing issue. + required: true + - label: This report does not disclose a security vulnerability or private data. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..d20e251 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Security vulnerability + url: https://github.com/xand0dev/VoidBar/security/advisories/new + about: Report security issues privately; do not open a public issue. + - name: Usage question or idea + url: https://github.com/xand0dev/VoidBar/discussions + about: Ask for help or discuss an idea before turning it into an issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 11fc491..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: enhancement -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..bbac754 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,41 @@ +name: Feature request +description: Propose an improvement to VoidBar +title: "[Feature]: " +labels: [enhancement] +body: + - type: markdown + attributes: + value: Thanks for the idea. Please describe the problem before prescribing an implementation. + - type: textarea + id: problem + attributes: + label: Problem + description: What workflow is difficult today, and who experiences it? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: Describe the smallest useful outcome. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Mention workarounds or different approaches you considered. + - type: textarea + id: impact + attributes: + label: Privacy, permissions, and performance + description: Would this add a network service, permission, background work, or stored data? + - type: checkboxes + id: checks + attributes: + label: Checklist + options: + - label: I searched for an existing issue or discussion. + required: true + - label: I am willing to help implement or test this feature. + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f407549..29964d2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,26 +1,24 @@ -## Description +## Summary -Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. + -Fixes # (issue) +Fixes # -## Type of change +## Validation -- [ ] Bug fix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) -- [ ] This change requires a documentation update + -## How Has This Been Tested? +- [ ] `swift test` +- [ ] `./Scripts/bundle.sh release` +- [ ] Tested the affected flow on macOS 15+ -Please describe the tests that you ran to verify your changes. -- [ ] Tested locally on macOS 15+ -- [ ] Verify there are no visual glitches or overlaps in the UI +## Impact -## Checklist: +- [ ] No new network request, permission, or persisted data +- [ ] Privacy/security documentation updated if behavior changed +- [ ] English and Ukrainian localization updated if user-facing text changed +- [ ] No unrelated changes are included -- [ ] My code follows the style guidelines of this project -- [ ] I have performed a self-review of my own code -- [ ] I have commented my code, particularly in hard-to-understand areas -- [ ] I have made corresponding changes to the documentation (if applicable) -- [ ] My changes generate no new warnings in Xcode +## Notes + + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 742077f..c120d88 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,58 +1,83 @@ name: build -# Тестов в проекте нет, поэтому проверка ровно одна и честно названа: собирается -# ли то, что лежит в main. Половина поломок в таком приложении — это сломанный -# скрипт сборки бандла, а не код, поэтому гоняется весь путь до .app, а не -# только swift build. on: push: branches: [main] - paths-ignore: ['**.md', 'docs/**'] + paths-ignore: + - "**.md" + - "docs/**" pull_request: branches: [main] workflow_dispatch: +permissions: + contents: read + +concurrency: + group: build-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: - build: - # macOS 15 — минимум приложения: Translation.framework младше не бывает. + test-and-build: + name: Test and build app runs-on: macos-15 + timeout-minutes: 20 + steps: - - uses: actions/checkout@v5 + - name: Check out repository + uses: actions/checkout@v5 - - name: Версия Swift + - name: Show Swift version run: swift --version - - name: Сборка приложения + - name: Run unit tests + run: swift test + + - name: Build release app bundle run: ./Scripts/bundle.sh release - - name: Проверить, что в бандле лежит то, что нужно + - name: Validate app bundle + shell: bash run: | - APP=build/VoidBar.app + set -euo pipefail + APP="build/VoidBar.app" VERSION="$(sed -n 's/^VERSION=//p' Scripts/version)" + test -x "$APP/Contents/MacOS/VoidBar" - # Локализации кладет тот же скрипт, и молча потерять их проще всего: - # приложение с пропавшей таблицей запускается и выглядит целым. + test -f "$APP/Contents/Resources/AppIcon.icns" test -f "$APP/Contents/Resources/en.lproj/Localizable.strings" test -f "$APP/Contents/Resources/uk.lproj/Localizable.strings" + INSIDE="$(/usr/libexec/PlistBuddy -c 'Print CFBundleShortVersionString' "$APP/Contents/Info.plist")" - test "$INSIDE" = "$VERSION" || { echo "версия в бандле $INSIDE, в Scripts/version $VERSION"; exit 1; } + test "$INSIDE" = "$VERSION" || { + echo "Bundle version $INSIDE does not match Scripts/version $VERSION" + exit 1 + } - - name: Ключи перевода совпадают с кодом - # Строка, для которой нет ключа, не ломает сборку — она просто - # показывается идентификатором тому, у кого этот язык. + - name: Validate localization keys run: | python3 - <<'PY' - import re, io, pathlib, sys + import io + import pathlib + import re + import sys + code = set() - for p in pathlib.Path("Sources").rglob("*.swift"): - code |= set(re.findall(r'localized\("([^"]+)"', io.open(p, encoding="utf-8").read())) - bad = False + for path in pathlib.Path("Sources").rglob("*.swift"): + source = io.open(path, encoding="utf-8").read() + code |= set(re.findall(r'localized\("([^"]+)"', source)) + + invalid = False for table in pathlib.Path("Resources").glob("*.lproj/Localizable.strings"): - text = io.open(table, encoding="utf-8").read() - keys = {k.replace("\\n", "\n") for k in re.findall(r'^"((?:[^"\\]|\\.)*)"\s*=', text, re.M)} + source = io.open(table, encoding="utf-8").read() + keys = { + key.replace("\\n", "\n") + for key in re.findall(r'^"((?:[^"\\]|\\.)*)"\s*=', source, re.M) + } missing = code - keys if missing: - bad = True - print(f"{table}: нет ключей: {sorted(missing)}") - sys.exit(1 if bad else 0) + invalid = True + print(f"{table}: missing keys: {sorted(missing)}") + + sys.exit(1 if invalid else 0) PY diff --git a/.gitignore b/.gitignore index b8c7dbc..64c0409 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ build/ *.xcworkspace .swiftpm/ -# Чтобы секрет не уехал в публичный репозиторий по случайности +# Common local secrets and signing material .env .env.* *.pem diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b879b46 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,61 @@ +# Changelog + +All notable changes to VoidBar are documented in this file. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Customizable tab visibility and ordering in Preferences. +- English and Ukrainian open-source documentation and community templates. + +### Changed + +- CI now runs unit tests as well as the full application bundle build. +- Privacy documentation now describes every intentional remote request, persisted data category, and system permission. + +## [0.5.1] - 2026-08-05 + +### Fixed + +- A newly created or selected note now receives keyboard focus immediately. + +## [0.5.0] - 2026-08-05 + +### Added + +- Persistent scratch notes with automatic focus and cleanup of empty notes. + +### Fixed + +- File-shelf removal controls now remain visible as cards move under the pointer. + +## [0.4.0] - 2026-08-05 + +### Added + +- Menu bar controls for viewing and moving saved screenshots to Trash. + +### Changed + +- Background work is reduced while the panel is collapsed. + +### Fixed + +- The panel now closes reliably after leaving text-entry tabs, switching spaces, or sleeping the display. + +## [0.3.0] - 2026-08-05 + +### Added + +- First installable disk image. +- Snippets, Calendar, and Translation tabs. +- Hover-based tab switching with a dwell threshold. +- Copied-image handoff to the shelf through the macOS pasteboard. +- English and Russian localization (Ukrainian replaced Russian in a later development version). + +[Unreleased]: https://github.com/xand0dev/VoidBar/compare/v0.5.1...HEAD +[0.5.1]: https://github.com/xand0dev/VoidBar/compare/v0.5.0...v0.5.1 +[0.5.0]: https://github.com/xand0dev/VoidBar/compare/v0.4.0...v0.5.0 +[0.4.0]: https://github.com/xand0dev/VoidBar/compare/v0.3.0...v0.4.0 +[0.3.0]: https://github.com/xand0dev/VoidBar/releases/tag/v0.3.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 2bb3e31..a446b9f 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -59,7 +59,10 @@ representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement. +reported to the project owner through the contact options on the +[@xand0dev GitHub profile](https://github.com/xand0dev). If a private contact +method is unavailable, use a GitHub Security Advisory and state that the report +concerns community safety rather than a software vulnerability. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f0bb5bd..d629a49 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,42 +1,67 @@ # Contributing to VoidBar -Thank you for your interest in contributing to VoidBar! We welcome contributions of all kinds: bug reports, feature requests, and pull requests. +Thanks for helping improve VoidBar. Bug reports, feature proposals, documentation, localization, tests, and code changes are all welcome. -## Development Setup +## Before you start -To build and run VoidBar locally, you will need: -- A Mac running **macOS 15** or later. -- The **Swift 6** toolchain (via Xcode 16+). +- Search existing issues before opening a new one. +- Use GitHub Discussions for broad ideas or usage questions when Discussions are available. +- Open an issue before investing in a large feature or architectural change so the direction can be agreed on first. +- Never include clipboard contents, calendar data, private TickTick URLs, credentials, or other personal data in an issue or test fixture. +- Follow the [Code of Conduct](CODE_OF_CONDUCT.md). -1. Clone the repository: - ```bash - git clone https://github.com/xand0dev/voidbar.git - cd voidbar - ``` +Security vulnerabilities must be reported privately as described in [SECURITY.md](SECURITY.md). -2. Build the project: - VoidBar uses a custom build script that handles compilation, assembling the bundle, compiling the media helper, localization, and ad-hoc signing. - ```bash - ./Scripts/bundle.sh - ``` +## Development setup -3. Run the app: - ```bash - open build/VoidBar.app - ``` +You need macOS 15 or later and Xcode 16+ (or another Swift 6 toolchain). -## Pull Request Process +```bash +git clone https://github.com/xand0dev/VoidBar.git +cd VoidBar +swift test +./Scripts/bundle.sh +open build/VoidBar.app +``` -1. **Fork the repository** and create your branch from `main`. -2. **Ensure it builds**: Run `./Scripts/bundle.sh` locally and verify the app opens and works without crashing. -3. **Check Code Style**: Try to follow the existing code style in the project. VoidBar uses Swift formatting conventions. -4. **Test your changes**: Please test your feature or bug fix manually since the project heavily relies on AppKit and SwiftUI UI components. -5. **Open a PR**: Fill out the provided Pull Request template. +`Scripts/bundle.sh` compiles the Swift package and Objective-C media helper, assembles the app bundle, copies resources, and applies an ad-hoc signature. Build output is written to `.build/` and `build/`; neither directory should be committed. -## Issues +## Project map -- Before opening an issue, please check if a similar issue already exists. -- Use the provided Issue Templates for bug reports and feature requests. -- Provide as much context as possible, including OS version and screenshots. +- `Sources/VoidBar/App` — app lifecycle, menu bar, and preferences window. +- `Sources/VoidBar/Notch` — panel lifecycle, geometry, shape, and pointer handling. +- `Sources/VoidBar/Model` — shared application and tab state. +- `Sources/VoidBar/Services` — feature state, persistence, system APIs, and remote requests. +- `Sources/VoidBar/UI` — SwiftUI panes and shared visual components. +- `Sources/VoidBarMediaHelper` — Objective-C helper for system media state. +- `Resources` — app icon and localized strings. +- `Tests/VoidBarTests` — unit tests. -We are excited to see what you build! +## Making a change + +1. Fork the repository and branch from `main`. +2. Keep each pull request focused on one logical change. +3. Follow the existing Swift and SwiftUI style; explain non-obvious system behavior in comments. +4. Add or update tests for logic that can be exercised outside the UI. +5. Add localization keys to both `Resources/en.lproj/Localizable.strings` and `Resources/uk.lproj/Localizable.strings`. +6. Update README, privacy documentation, or release notes when behavior changes. +7. Run the checks below before opening a pull request. + +```bash +swift test +./Scripts/bundle.sh release +``` + +For UI changes, also launch the built app and verify the panel on the relevant display configuration. Include a screenshot or short recording in the pull request when the visual change is meaningful. + +## Pull requests + +Fill in the pull request template and describe: + +- the problem and why the change is needed; +- the implementation and important tradeoffs; +- automated and manual validation; +- privacy, permission, persistence, localization, and performance effects; +- related issues using `Fixes #123` when applicable. + +Maintainers may ask for a smaller scope, additional tests, or documentation before merging. By contributing, you agree that your work is licensed under the repository's [MIT License](LICENSE). diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000..37ad2e6 --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,48 @@ +# Privacy + +VoidBar is local-first and has no analytics, advertising, user accounts, or project-operated backend. This document describes every intentional network connection, stored data category, and system permission in the current codebase. + +## Network connections + +| Feature | Destination | Data sent | When | +| --- | --- | --- | --- | +| Weather location | `https://ipapi.co/json/` | The requester's public IP is visible to the service as part of a normal HTTPS request. | After the Weather tab is first opened and every 30 minutes afterward while VoidBar runs. | +| Weather forecast | `https://api.open-meteo.com/` | Approximate latitude and longitude returned by the location service. | After a successful weather-location request. | +| TickTick | The iCal URL entered in settings | A normal HTTPS request to that URL; the URL itself may contain a private subscription token. | After configuration and every 15 minutes while VoidBar runs. | +| Spotify artwork | `i.scdn.co`, `mosaic.scdn.co`, or `lineup-images.scdn.co` | A normal HTTPS image request. Cookies and URL caching are disabled. | When compatible Spotify artwork is available. | +| Translation assets | Apple-managed services | Managed by macOS, not by VoidBar. | macOS may download a language pack when needed. | + +VoidBar does not send notes, snippets, clipboard contents, file-shelf paths, calendar events, translations, system statistics, or telemetry to a VoidBar server. There is no VoidBar server. + +## Local data + +| Data | Storage | Lifetime | +| --- | --- | --- | +| Clipboard history | Process memory only | Cleared when VoidBar quits; concealed password-manager entries are ignored. | +| Copied images | `~/Pictures/VoidBar/` | Kept until the user disables saving or moves the folder contents to Trash from the menu bar. | +| File shelf | File paths in macOS preferences | Kept across launches; source files are never copied or uploaded by the shelf. | +| Notes | `~/Library/Application Support/VoidBar/notes.json` | Kept until deleted in the app. The file is set to owner-only permissions where supported. | +| Snippets | `~/Library/Application Support/VoidBar/snippets.json` | Kept until deleted in the app or file. The file is set to owner-only permissions where supported. | +| Teleprompter text | macOS preferences | Kept across launches. | +| Pomodoro totals, tab layout, and settings | macOS preferences | Kept across launches. | +| TickTick subscription URL | macOS preferences | Kept until removed from settings. Treat this URL as a secret because it may contain an access token. | + +Local files and macOS preferences are not encrypted by VoidBar. Protection depends on the user's macOS account, disk encryption, backups, and device security. + +## Permissions + +- **Calendar:** requested only after the user chooses to grant calendar access from the Calendar pane. +- **Automation / Apple Events:** macOS may request access when AppleScript fallback controls Apple Music or Spotify. +- **Accessibility:** only needed by the system-wide media-key fallback when direct player control is unavailable. +- **Notifications:** requested when the user starts a Pomodoro timer so VoidBar can announce completion. +- **Launch at Login:** enabled or disabled explicitly from the menu bar. + +VoidBar does not request Screen Recording permission. + +## Third-party policies + +Remote requests are governed by the privacy policies of the service receiving them: ipapi, Open-Meteo, TickTick, Spotify, or Apple. Avoid opening the Weather tab or configuring TickTick if you do not want those connections. Once Weather has been opened, it continues refreshing every 30 minutes until VoidBar quits. + +## Questions and changes + +Privacy issues can be reported through [GitHub Issues](https://github.com/xand0dev/VoidBar/issues). Security-sensitive reports should use the private process in [SECURITY.md](SECURITY.md). Material behavior changes should update this file in the same pull request. diff --git a/README.md b/README.md index 224aebb..c7d3c15 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,93 @@ # VoidBar -*English · [Українська](README.uk.md)* +

+ A native productivity hub for the MacBook notch.
+ Media controls, file shelf, clipboard history, notes, calendar, translation, Pomodoro, weather, and more — one hover away. +

-VoidBar is a minimalist, zero-distraction utility for macOS that lives in your MacBook's notch (or menu bar on older Macs). It gives you instant access to your most used tools without cluttering your screen or consuming system resources. +

+ Build + MIT License + macOS 15+ + Swift 6 +

-[![Build Status](https://github.com/xand0dev/voidbar/actions/workflows/build.yml/badge.svg)](https://github.com/xand0dev/voidbar/actions/workflows/build.yml) +

Українська

-![VoidBar UI Preview](docs/panel.png) +![VoidBar media controls inside the MacBook notch](docs/panel.png) -## Overview +VoidBar stays out of sight until you hover over the notch, then opens a compact panel of native macOS tools. Move away and it collapses again. It is built in Swift with SwiftUI and AppKit, has no third-party runtime dependencies, and includes English and Ukrainian localization. -VoidBar stays hidden until you hover over the notch area. When triggered, it gracefully drops down to reveal a powerful suite of productivity widgets. Once you're done, move your mouse away and it vanishes. - -- **Zero-Footprint:** Consumes 0% CPU when collapsed. -- **Privacy-First:** Doesn't require invasive screen recording or accessibility permissions. -- **Native Experience:** Built with AppKit and SwiftUI for smooth, native performance. +> [!IMPORTANT] +> VoidBar is in active development. Public, notarized binary releases are not available yet; build it from source using the instructions below. ## Features -- 🎵 **Media Controller:** Control your currently playing media, whether it's Apple Music, Spotify, or a browser tab. -- 📂 **Drop Shelf:** A temporary holding zone for your files. Drag files to the notch, switch apps, and drag them out when needed. -- 📋 **Clipboard Manager:** Automatically keeps track of your last 40 copied items, including universal clipboard items from your iPhone. -- 📝 **Quick Notes & Snippets:** Keep a library of frequently used texts (like your email or phone number) and a scratchpad for quick, temporary notes. -- 🗓️ **Meeting Tracker:** Connects to Calendar to show your next meeting and provides a one-click join button for Zoom, Teams, Meet, and others. -- 🌍 **Offline Translator:** Built-in offline translation leveraging macOS's native `Translation.framework`. -- ✅ **TickTick Integration:** Connects with an iCal link to manage and prioritize your daily tasks directly from the notch. -- 🍅 **Pomodoro Timer:** Stay focused with built-in presets (5m, 10m, 25m, 50m) and a daily pomodoro completion tracker. -- 📊 **System Monitor:** Keep an eye on your Mac's performance with CPU, Memory, and live Network speed stats. -- ⛅️ **Weather:** View current local weather and a detailed 24-hour horizontal forecast. +| Tool | What it does | +| --- | --- | +| Media | Controls Apple Music, Spotify, and compatible system media sessions, with track progress and artwork. | +| File shelf | Holds file references while you switch between apps and supports drag-in/drag-out workflows. | +| Clipboard | Keeps the latest 40 text, link, file, and image entries in memory while VoidBar runs. | +| Screenshots | Saves copied images to `~/Pictures/VoidBar` and places them on the shelf; this can be disabled. | +| Snippets | Stores reusable text, links, email addresses, and phone numbers in an editable JSON file. | +| Calendar | Shows upcoming meetings and opens Zoom, Google Meet, Teams, and other safe meeting links. | +| Notes | Provides a fast, persistent scratchpad for temporary thoughts. | +| Teleprompter | Keeps scrolling reference text close at hand. | +| Translation | Uses Apple's native Translation framework, with no custom translation service. | +| Pomodoro | Offers 5, 10, 25, and 50 minute presets and tracks completed sessions for the day. | +| System monitor | Displays CPU, memory, upload, and download activity. | +| Weather | Shows current conditions and the next 24 hours using IP-based location and Open-Meteo. | +| TickTick | Reads tasks from a user-provided TickTick iCal subscription URL. | -## Installation & Setup +Tabs can be reordered, hidden, and restored from Preferences. VoidBar can also launch at login and works from a menu bar control on Macs without a display notch. -1. **[Download the latest release](https://github.com/xand0dev/voidbar/releases/latest)**. -2. Drag `VoidBar.app` to your Applications folder. -3. Because VoidBar is not notarized by Apple (it's ad-hoc signed), you need to manually allow it: - - Go to **System Settings → Privacy & Security**, scroll down, and click **Open Anyway**. - - *Alternatively*, run this command in your terminal: `xattr -dr com.apple.quarantine /Applications/VoidBar.app` +## Requirements -## Building from Source +- macOS 15 Sequoia or later +- Xcode 16+ or another Swift 6 toolchain +- A MacBook notch is optional -You need a Mac running macOS 15+ and the Swift 6 toolchain. +## Build from source ```bash -git clone https://github.com/xand0dev/voidbar.git -cd voidbar +git clone https://github.com/xand0dev/VoidBar.git +cd VoidBar ./Scripts/bundle.sh open build/VoidBar.app ``` -## Security & Privacy +The bundle script builds the Swift package, compiles the media helper, copies localizations and the app icon, assembles `VoidBar.app`, and applies an ad-hoc signature. To produce a drag-to-Applications disk image: + +```bash +./Scripts/dmg.sh +``` + +Because local builds are not notarized, macOS may block the first launch. Open **System Settings → Privacy & Security** and choose **Open Anyway**. Only use `xattr -dr com.apple.quarantine /Applications/VoidBar.app` for an app you built yourself or obtained from a source you trust. + +## Test + +```bash +swift test +./Scripts/bundle.sh release +``` + +Pull requests run both commands on macOS 15 in GitHub Actions. + +## Privacy + +VoidBar has no analytics, advertising, accounts, or project-operated backend. Most data stays on your Mac, but features that need remote data do use the network: -VoidBar is designed to be completely unobtrusive. -- It doesn't request Accessibility or Automation permissions by default. -- Calendar permissions are only requested if you explicitly click the Calendar tab. -- Automation is only requested if you use the Media Controller to control Apple Music/Spotify. -- All your notes, snippets, and clipboard data are stored locally in plain text under `~/Library/Application Support/VoidBar/`. We don't track you or send data anywhere. +- After the Weather tab is opened, it contacts `ipapi.co` for approximate IP-based location and `api.open-meteo.com` for forecasts. +- TickTick fetches the private iCal URL you configure. +- Spotify artwork may be loaded from allow-listed Spotify CDN hosts. +- Apple's Translation framework may download language assets managed by macOS. + +Clipboard history stays in memory. Notes, snippets, preferences, and the optional TickTick URL are stored locally; copied images can be saved under `~/Pictures/VoidBar`. See [PRIVACY.md](PRIVACY.md) for the complete data and permission map, and [SECURITY.md](SECURITY.md) for vulnerability reporting. + +## Contributing + +Bug reports, feature ideas, documentation improvements, and code contributions are welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md), use the issue templates, and follow the [Code of Conduct](CODE_OF_CONDUCT.md). ## License -MIT License + +VoidBar is available under the [MIT License](LICENSE). diff --git a/README.uk.md b/README.uk.md index 44048c6..8ebd405 100644 --- a/README.uk.md +++ b/README.uk.md @@ -1,60 +1,93 @@ # VoidBar -*[English](README.md) · Українська* +

+ Нативний центр продуктивності у вирізі MacBook.
+ Медіа, файли, буфер обміну, нотатки, календар, переклад, Pomodoro, погода та інше — на відстані одного наведення. +

-VoidBar — це мінімалістична утиліта для macOS, яка ховається у вирізі (notch) вашого MacBook. Вона надає миттєвий доступ до найнеобхідніших інструментів, не захаращуючи екран і не споживаючи ресурси системи. +

+ Збірка + Ліцензія MIT + macOS 15+ + Swift 6 +

-[![Статус збірки](https://github.com/xand0dev/voidbar/actions/workflows/build.yml/badge.svg)](https://github.com/xand0dev/voidbar/actions/workflows/build.yml) +

English

-![Інтерфейс VoidBar](docs/panel.png) +![Медіаконтролер VoidBar у вирізі MacBook](docs/panel.png) -## Огляд +VoidBar не заважає, доки ви не наведете курсор на виріз екрана. Тоді він відкриває компактну панель нативних інструментів macOS і згортається, щойно ви відведете курсор. Застосунок створено на Swift із SwiftUI та AppKit, він не має сторонніх runtime-залежностей і підтримує українську та англійську мови. -VoidBar залишається невидимим, поки ви не наведете курсор на зону вирізу. Після цього він плавно розгортається, відкриваючи потужний набір віджетів для продуктивності. Щойно ви відведете мишу — панель зникає. - -- **Нульове навантаження:** Споживає 0% процесорного часу у згорнутому стані. -- **Приватність:** Не вимагає дозволів на запис екрану чи універсальний доступ (Accessibility). -- **Нативність:** Створено за допомогою AppKit та SwiftUI для максимальної швидкодії. +> [!IMPORTANT] +> VoidBar активно розвивається. Публічних нотаризованих збірок поки немає; зберіть застосунок із вихідного коду за інструкцією нижче. ## Можливості -- 🎵 **Медіа-контролер:** Керуйте відтворенням музики з Apple Music, Spotify або вкладок браузера. -- 📂 **Полиця для файлів:** Тимчасова зона для ваших файлів. Перетягніть файл у челку, перемкніть програму і витягніть його куди потрібно. -- 📋 **Менеджер буфера обміну:** Автоматично зберігає останні 40 скопійованих елементів, включаючи знімки екрана, скопійовані на iPhone через Handoff. -- 📝 **Нотатки та Шаблони:** Зберігайте часто використовувані тексти (наприклад, пошту чи номер телефону) або робіть швидкі тимчасові нотатки. -- 🗓️ **Календар зустрічей:** Підключається до вашого календаря, щоб показати наступну зустріч, та дає кнопку для швидкого приєднання до Zoom, Teams, Meet тощо. -- 🌍 **Офлайн-перекладач:** Вбудований перекладач, що працює без інтернету на базі нативного `Translation.framework`. -- ✅ **Інтеграція з TickTick:** Підключіть iCal-посилання для керування щоденними завданнями та пріоритетами прямо з панелі. -- 🍅 **Таймер Pomodoro:** Залишайтеся зосередженими завдяки вбудованим пресетам (5хв, 10хв, 25хв, 50хв) та відстеженню виконаних сесій за день. -- 📊 **Моніторинг Системи:** Слідкуйте за продуктивністю вашого Mac (ЦП, Пам'ять) та швидкістю мережі в реальному часі. -- ⛅️ **Погода:** Переглядайте поточну локальну погоду та детальний горизонтальний прогноз на 24 години. - -## Встановлення - -1. **[Завантажити останню версію](https://github.com/xand0dev/voidbar/releases/latest)**. -2. Перетягніть `VoidBar.app` у папку «Програми» (Applications). -3. Оскільки програма підписана ad-hoc і не нотаризована Apple, перший запуск потрібно дозволити вручну: - - Відкрийте **Системні параметри → Приватність і безпека**, прокрутіть вниз і натисніть **Відкрити все одно**. - - *Або* виконайте цю команду в терміналі: `xattr -dr com.apple.quarantine /Applications/VoidBar.app` +| Інструмент | Що він робить | +| --- | --- | +| Медіа | Керує Apple Music, Spotify та сумісними системними медіасесіями, показує прогрес і обкладинку. | +| Полиця файлів | Тримає посилання на файли, поки ви перемикаєтеся між програмами; підтримує drag-and-drop. | +| Буфер обміну | Пам'ятає останні 40 текстових, файлових, графічних елементів і посилань, поки VoidBar працює. | +| Знімки | Зберігає скопійовані зображення у `~/Pictures/VoidBar` і додає їх на полицю; функцію можна вимкнути. | +| Заготовки | Зберігає повторювані тексти, адреси, посилання, пошту й телефони у редагованому JSON-файлі. | +| Календар | Показує найближчі зустрічі та відкриває безпечні посилання Zoom, Google Meet, Teams тощо. | +| Нотатки | Дає швидкий постійний чернетник для тимчасових думок. | +| Телесуфлер | Тримає текст із прокруткою поруч під час запису чи виступу. | +| Переклад | Використовує нативний Translation framework від Apple без власного сервісу перекладу. | +| Pomodoro | Має пресети на 5, 10, 25 і 50 хвилин та рахує завершені сесії за день. | +| Моніторинг | Показує CPU, пам'ять, швидкість завантаження та відвантаження даних. | +| Погода | Показує поточні умови й прогноз на 24 години через IP-геолокацію та Open-Meteo. | +| TickTick | Читає завдання з наданого користувачем приватного iCal-посилання TickTick. | + +Вкладки можна приховувати, повертати й змінювати їхній порядок у Налаштуваннях. VoidBar уміє запускатися разом із системою та доступний через меню-бар на Mac без вирізу екрана. + +## Вимоги + +- macOS 15 Sequoia або новіша +- Xcode 16+ або інший Swift 6 toolchain +- Виріз екрана MacBook необов'язковий ## Збірка з вихідного коду -Вам знадобиться Mac з macOS 15+ та Swift 6 toolchain. - ```bash -git clone https://github.com/xand0dev/voidbar.git -cd voidbar +git clone https://github.com/xand0dev/VoidBar.git +cd VoidBar ./Scripts/bundle.sh open build/VoidBar.app ``` -## Безпека та Приватність +Скрипт збирає Swift package і media helper, копіює локалізації та іконку, формує `VoidBar.app` і застосовує ad-hoc підпис. Щоб створити DMG із перетягуванням у Applications: + +```bash +./Scripts/dmg.sh +``` -VoidBar створено з думкою про вашу приватність. -- За замовчуванням програма не просить жодних дозволів на автоматизацію чи доступ. -- Доступ до календаря запитується лише тоді, коли ви вперше відкриваєте вкладку «Календар». -- Дозвіл на Автоматизацію запитується тільки для керування Apple Music або Spotify. -- Усі ваші нотатки, шаблони та історія буфера обміну зберігаються локально у папці `~/Library/Application Support/VoidBar/`. Жодні дані нікуди не відправляються. +Локальні збірки не нотаризовані, тому macOS може заблокувати перший запуск. Відкрийте **Системні параметри → Приватність і безпека** та натисніть **Відкрити все одно**. Команду `xattr -dr com.apple.quarantine /Applications/VoidBar.app` використовуйте лише для застосунку, який зібрали самі або отримали з надійного джерела. + +## Тести + +```bash +swift test +./Scripts/bundle.sh release +``` + +Для кожного pull request GitHub Actions виконує обидві команди на macOS 15. + +## Приватність + +У VoidBar немає аналітики, реклами, акаунтів чи власного сервера проєкту. Більшість даних залишається на Mac, але мережеві функції звертаються до зовнішніх сервісів: + +- Після першого відкриття вкладки Погода вона звертається до `ipapi.co` для приблизного визначення місця за IP та до `api.open-meteo.com` по прогноз. +- TickTick завантажує приватний iCal URL, який ви вкажете. +- Обкладинки Spotify можуть завантажуватися з дозволених CDN-хостів Spotify. +- Translation framework від Apple може завантажувати мовні пакети засобами macOS. + +Історія буфера обміну живе лише в пам'яті. Нотатки, заготовки, налаштування та необов'язковий TickTick URL зберігаються локально; скопійовані зображення можуть записуватися у `~/Pictures/VoidBar`. Повна мапа даних і дозволів є у [PRIVACY.md](PRIVACY.md), а правила повідомлення про вразливості — у [SECURITY.md](SECURITY.md). + +## Участь у розробці + +Ми раді баг-репортам, ідеям, покращенням документації та коду. Почніть із [CONTRIBUTING.md](CONTRIBUTING.md), використовуйте шаблони issue та дотримуйтеся [Кодексу поведінки](CODE_OF_CONDUCT.md). ## Ліцензія -MIT License + +VoidBar поширюється за умовами [MIT License](LICENSE). diff --git a/Resources/en.lproj/Localizable.strings b/Resources/en.lproj/Localizable.strings index da99f50..1b355bf 100644 --- a/Resources/en.lproj/Localizable.strings +++ b/Resources/en.lproj/Localizable.strings @@ -13,9 +13,11 @@ "Timer" = "Timer"; "Monitor" = "Monitor"; "Weather" = "Weather"; +"Tasks" = "Tasks"; /* Menu bar */ "Open Panel" = "Open Panel"; +"Preferences..." = "Preferences..."; "Launch at Login" = "Launch at Login"; "Save Clipboard Screenshots" = "Save Clipboard Screenshots"; "Show Screenshots Folder" = "Show Screenshots Folder"; @@ -60,7 +62,7 @@ "Join · %@" = "Join · %@"; "No other meetings this week" = "No other meetings this week"; "See your next meetings" = "See your next meetings"; -"VoidBar needs access to Calendar. It is the only permission\nthe app asks for, and only for this tab." = "VoidBar needs access to Calendar. It is the only permission\nthe app asks for, and only for this tab."; +"VoidBar needs Calendar access for this tab. Other features\nmay request their own permissions when used." = "VoidBar needs Calendar access for this tab. Other features\nmay request their own permissions when used."; "Allow" = "Allow"; "Calendar access is off" = "Calendar access is off"; "Settings → Privacy → Calendars" = "Settings → Privacy → Calendars"; diff --git a/Resources/uk.lproj/Localizable.strings b/Resources/uk.lproj/Localizable.strings index ec362b1..5153d10 100644 --- a/Resources/uk.lproj/Localizable.strings +++ b/Resources/uk.lproj/Localizable.strings @@ -12,9 +12,11 @@ "Timer" = "Таймер"; "Monitor" = "Монітор"; "Weather" = "Погода"; +"Tasks" = "Завдання"; /* Меню-бар */ "Open Panel" = "Відкрити панель"; +"Preferences..." = "Налаштування..."; "Launch at Login" = "Запускати при вході"; "Save Clipboard Screenshots" = "Зберігати знімки екрана"; "Show Screenshots Folder" = "Показати папку знімків"; @@ -59,7 +61,7 @@ "Join · %@" = "Приєднатися · %@"; "No other meetings this week" = "Інших зустрічей цього тижня немає"; "See your next meetings" = "Показати найближчі зустрічі"; -"VoidBar needs access to Calendar. It is the only permission\nthe app asks for, and only for this tab." = "Потрібен доступ до Календаря. Це єдиний дозвіл,\nякий запитує VoidBar — і лише для цієї вкладки."; +"VoidBar needs Calendar access for this tab. Other features\nmay request their own permissions when used." = "VoidBar потрібен доступ до Календаря для цієї вкладки. Інші функції\nможуть попросити окремі дозволи, коли ви ними скористаєтеся."; "Allow" = "Дозволити"; "Calendar access is off" = "Доступ до Календаря закрито"; "Settings → Privacy → Calendars" = "Системні параметри → Приватність → Календарі"; diff --git a/SECURITY.md b/SECURITY.md index b18ff14..6919da3 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,49 +1,31 @@ -# Безопасность +# Security Policy -## Как сообщить об уязвимости +## Supported versions -Через [Security Advisories](https://github.com/xand0dev/voidbar/security/advisories/new) -на GitHub — это приватный канал, issue заводить не нужно. Ответ придёт, когда придёт: -проект личный, дежурства по нему нет. +Until the first public binary release, security fixes are applied to the `main` branch. After releases begin, only the latest published version will be supported unless a release note says otherwise. -## Поверхность атаки +## Report a vulnerability -Её практически нет, и это осознанно. +Please report suspected vulnerabilities privately through [GitHub Security Advisories](https://github.com/xand0dev/VoidBar/security/advisories/new). Do not open a public issue for an undisclosed vulnerability. -VoidBar не открывает портов, не слушает сеть и не ходит в неё. Ни одного сетевого -соединения приложение не устанавливает вовсе — ни для обновлений, ни для -телеметрии, ни для чего-либо ещё. Всё, что оно делает, происходит на одной машине -между процессами, которые и так принадлежат пользователю. +Include, when possible: -Снимок с айфона попадает на полку через универсальный буфер обмена, то есть -средствами самой macOS: передачу выполняет Continuity, она зашифрована и -подтверждена Apple ID, а VoidBar лишь читает то, что оказалось в буфере. Своего -канала связи с телефоном у него нет. +- the affected version or commit; +- reproduction steps and the expected impact; +- relevant logs, screenshots, or a minimal proof of concept; +- whether the issue has already been disclosed elsewhere. -## Разрешения +This is a volunteer-maintained project with no guaranteed response SLA. The maintainer will acknowledge a valid report when available, investigate it, and coordinate disclosure before publishing a fix. -Одно, и только по нажатию кнопки: доступ к Календарю на вкладке «Календарь». -Ни автоматизации, ни универсального доступа, ни записи экрана в обычном режиме -работы не требуется. Не открываешь календарь — приложение живёт вообще без -разрешений. +## Security boundaries -Автоматизация и универсальный доступ запрашиваются только запасным путём для -управления плеером, если основной перестанет работать. +VoidBar does not expose a listening network service or operate a project backend. It does make outbound requests for weather, configured TickTick feeds, and allow-listed Spotify artwork. See [PRIVACY.md](PRIVACY.md) for the complete network, storage, and permission map. -## Что VoidBar читает и где это лежит +The TickTick iCal subscription URL may contain a secret token and is stored in macOS preferences. Reports involving exposed personal URLs, credentials, or other private data should be sanitized before sharing. -**История буфера обмена** живёт только в памяти процесса и не пишется на диск. -Записи с типом `org.nspasteboard.ConcealedType`, которым менеджеры паролей -помечают свои копирования, в историю не попадают вовсе. +## Out of scope -**Снимки экрана из буфера** сохраняются файлами в `~/Pictures/VoidBar` и не -удаляются автоматически никогда. Папка целиком принадлежит пользователю; чистит -её только он. Отключается пунктом меню-бара. - -**Полка** хранит в настройках приложения пути к файлам, а не сами файлы. - -**Заготовки** читаются из `~/Library/Application Support/VoidBar/snippets.json`. -Файл только читается, никогда не записывается. - -**Перевод** выполняется `Translation.framework` полностью офлайн: текст не -покидает машину. +- Gatekeeper warnings caused solely by locally built, ad-hoc-signed binaries; +- vulnerabilities in macOS or a third-party service without a VoidBar-specific impact; +- social engineering, spam, denial-of-service testing, or destructive testing; +- reports produced only by automated scanners without a reproducible impact. diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000..314ff66 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,14 @@ +# Support + +VoidBar is a volunteer-maintained open-source project and does not provide guaranteed support or response times. + +## Get help + +1. Read the English or [Ukrainian README](README.uk.md), especially the requirements, build, privacy, and Gatekeeper notes. +2. Search [GitHub Discussions](https://github.com/xand0dev/VoidBar/discussions) and [existing issues](https://github.com/xand0dev/VoidBar/issues). +3. Start a Discussion for setup help, usage questions, or early feature ideas. +4. Open a bug report only when you can provide reproducible steps. + +Please include your macOS version, Mac model, VoidBar version or commit, installation method, and sanitized logs. Never post a private TickTick iCal URL, clipboard contents, calendar details, credentials, or personal file paths. + +Security vulnerabilities belong in the private reporting process described in [SECURITY.md](SECURITY.md). diff --git a/Sources/VoidBar/Model/NotchViewModel.swift b/Sources/VoidBar/Model/NotchViewModel.swift index eb8418c..03d91a1 100644 --- a/Sources/VoidBar/Model/NotchViewModel.swift +++ b/Sources/VoidBar/Model/NotchViewModel.swift @@ -61,9 +61,13 @@ final class NotchViewModel: ObservableObject { didSet { // Opening the tab only re-checks the status. The permission prompt // is the user's own press on the button inside the pane: this is - // the one permission VoidBar asks for at all, and it deserves an - // explanation before the system dialog, not after. + // Calendar deserves an explanation before the system dialog, not + // after, so switching tabs only refreshes the current status. if tab == .calendar { calendar.refreshAccess() } + // Weather reveals an approximate location through the public IP. + // Do not make that request merely because the app launched; the + // first visit to the pane is the user's explicit opt-in. + if tab == .weather { weather.start() } // The snippets file is edited from outside the app, so it is read // on the way in rather than held from launch. if tab == .snippets { snippets.reload() } @@ -198,7 +202,6 @@ final class NotchViewModel: ObservableObject { // never prompts on its own. calendar.start() monitor.start() - weather.start() // Screenshots reach the shelf through here whether they were taken on // this Mac or on a phone: a copy made on the phone arrives in the same diff --git a/Sources/VoidBar/Services/CalendarStore.swift b/Sources/VoidBar/Services/CalendarStore.swift index 3fb1abe..bf4d997 100644 --- a/Sources/VoidBar/Services/CalendarStore.swift +++ b/Sources/VoidBar/Services/CalendarStore.swift @@ -3,9 +3,8 @@ import EventKit /// Today's meetings, and the link that joins the next one. /// -/// Access is requested only when the user first opens the calendar tab: it is -/// the one permission VoidBar needs at all, and nobody should be asked for it -/// just because the app launched. +/// Access is requested only after the user presses the button in the Calendar +/// pane. Nobody should be asked for it just because the app launched. @MainActor final class CalendarStore: ObservableObject { enum Access { diff --git a/Sources/VoidBar/Services/TimerStore.swift b/Sources/VoidBar/Services/TimerStore.swift index 21df4e5..0de6b2d 100644 --- a/Sources/VoidBar/Services/TimerStore.swift +++ b/Sources/VoidBar/Services/TimerStore.swift @@ -1,6 +1,6 @@ import Foundation import Combine -import AppKit +import UserNotifications @MainActor final class TimerStore: ObservableObject { @@ -35,6 +35,7 @@ final class TimerStore: ObservableObject { func start() { checkNewDay() + requestNotificationAccess() if state == .idle { timeRemaining = selectedDuration } @@ -85,11 +86,30 @@ final class TimerStore: ObservableObject { defaults.set(lastCompletionDate, forKey: "pomodoroLastDate") reset() - let notification = NSUserNotification() - notification.title = "Pomodoro Finished" - notification.informativeText = "Time to take a break! You have completed \(completedToday) today." - notification.soundName = "Glass" - NSUserNotificationCenter.default.deliver(notification) + deliverCompletionNotification() + } + + private func requestNotificationAccess() { + Task { + _ = try? await UNUserNotificationCenter.current() + .requestAuthorization(options: [.alert, .sound]) + } + } + + private func deliverCompletionNotification() { + let content = UNMutableNotificationContent() + content.title = "Pomodoro Finished" + content.body = "Time to take a break! You have completed \(completedToday) today." + content.sound = .default + + let request = UNNotificationRequest( + identifier: "voidbar.pomodoro.\(UUID().uuidString)", + content: content, + trigger: nil + ) + Task { + try? await UNUserNotificationCenter.current().add(request) + } } private func checkNewDay() { diff --git a/Sources/VoidBar/Services/WeatherStore.swift b/Sources/VoidBar/Services/WeatherStore.swift index 1b55540..434234c 100644 --- a/Sources/VoidBar/Services/WeatherStore.swift +++ b/Sources/VoidBar/Services/WeatherStore.swift @@ -1,14 +1,14 @@ import Foundation import Combine -struct HourlyWeather: Codable, Identifiable { +struct HourlyWeather: Identifiable { let id = UUID() let time: Date let temperature: Double let condition: Int } -struct WeatherData: Codable { +struct WeatherData { let temperature: Double let condition: Int let locationName: String? diff --git a/Sources/VoidBar/UI/CalendarPane.swift b/Sources/VoidBar/UI/CalendarPane.swift index 4641986..00b0c11 100644 --- a/Sources/VoidBar/UI/CalendarPane.swift +++ b/Sources/VoidBar/UI/CalendarPane.swift @@ -163,7 +163,7 @@ struct CalendarPane: View { Text("See your next meetings") .font(.system(size: 12, weight: .medium)) .foregroundStyle(Theme.secondary) - Text("VoidBar needs access to Calendar. It is the only permission\nthe app asks for, and only for this tab.") + Text("VoidBar needs Calendar access for this tab. Other features\nmay request their own permissions when used.") .font(.system(size: 10)) .foregroundStyle(Theme.tertiary) .multilineTextAlignment(.center) diff --git a/docs/releases/0.3.0.md b/docs/releases/0.3.0.md index 056cf6a..4373c71 100644 --- a/docs/releases/0.3.0.md +++ b/docs/releases/0.3.0.md @@ -18,6 +18,6 @@ --- -Установка описана в [README](https://github.com/xand0dev/voidbar/blob/main/README.ru.md#установка). +Установка описана в [README](https://github.com/xand0dev/VoidBar/blob/main/README.uk.md#збірка-з-вихідного-коду). Первый запуск потребует один раз разрешить приложение в Системных настройках: образ подписан ad-hoc, без Developer ID. diff --git a/docs/releases/0.4.0.md b/docs/releases/0.4.0.md index 24048aa..d53b71d 100644 --- a/docs/releases/0.4.0.md +++ b/docs/releases/0.4.0.md @@ -18,5 +18,5 @@ --- -Установка описана в [README](https://github.com/xand0dev/voidbar/blob/main/README.ru.md#установка). +Установка описана в [README](https://github.com/xand0dev/VoidBar/blob/main/README.uk.md#збірка-з-вихідного-коду). Обновление — открыть образ и заменить приложение; разрешать заново не придётся. diff --git a/docs/releases/0.5.0.md b/docs/releases/0.5.0.md index 018efb3..63b72b8 100644 --- a/docs/releases/0.5.0.md +++ b/docs/releases/0.5.0.md @@ -12,5 +12,5 @@ --- -Установка описана в [README](https://github.com/xand0dev/voidbar/blob/main/README.ru.md#установка). +Установка описана в [README](https://github.com/xand0dev/VoidBar/blob/main/README.uk.md#збірка-з-вихідного-коду). Обновление — открыть образ и заменить приложение; разрешать заново не придётся. diff --git a/docs/releases/0.5.1.md b/docs/releases/0.5.1.md index 8dfe881..f269f49 100644 --- a/docs/releases/0.5.1.md +++ b/docs/releases/0.5.1.md @@ -4,5 +4,5 @@ --- -Установка описана в [README](https://github.com/xand0dev/voidbar/blob/main/README.ru.md#установка). +Установка описана в [README](https://github.com/xand0dev/VoidBar/blob/main/README.uk.md#збірка-з-вихідного-коду). Обновление — открыть образ и заменить приложение; разрешать заново не придётся.