Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
9db1481
Rename Linux release bundle from 'todomax' to 'busymax'
albert-gee Jun 12, 2026
e782ca3
add busymax-logo.png to assets/branding
albert-gee Jun 12, 2026
808d71e
update app settings and refactor BusyMax logo handling
albert-gee Jun 12, 2026
f55a5af
remove 'Quit exits completely' setting from settings screen
albert-gee Jun 12, 2026
b43ce35
update logo asset format from SVG to PNG in task lists sidebar
albert-gee Jun 12, 2026
f91675b
refactor application activation logic and update desktop entry metada…
albert-gee Jun 12, 2026
deb737e
add snapcraft configuration for BusyMax application
albert-gee Jun 12, 2026
58de9b0
add PNG logo asset for BusyMax branding
albert-gee Jun 12, 2026
928ab20
update tests to verify PNG logo rendering and default settings
albert-gee Jun 12, 2026
32597d0
add beta snap release documentation and update README for installatio…
albert-gee Jun 12, 2026
21e5add
implement portal encrypted OAuth token storage for Linux and update t…
albert-gee Jun 14, 2026
59f67d4
refactor window management for GNOME Wayland support and enhance debu…
albert-gee Jun 14, 2026
6f9cb94
update snapcraft configuration for improved Wayland support and strea…
albert-gee Jun 14, 2026
e2e73b0
update beta snap release documentation to clarify OAuth token storage…
albert-gee Jun 14, 2026
d048e94
add .snap-local to .gitignore to exclude local Snap build artifacts
albert-gee Jun 14, 2026
0c2c58f
add compact agenda snapshot and data encoding/decoding functionality
albert-gee Jun 14, 2026
bdbbeca
Vendor patched xdg_status_notifier_item for Linux tray support and up…
albert-gee Jun 14, 2026
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
10 changes: 5 additions & 5 deletions .github/workflows/flutter-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,14 @@ jobs:
- name: Package Linux release bundle
run: |
cd build/linux/x64/release
zip -r todomax-linux-x64-release-bundle.zip bundle
sha256sum todomax-linux-x64-release-bundle.zip > todomax-linux-x64-release-bundle.zip.sha256
zip -r busymax-linux-x64-release-bundle.zip bundle
sha256sum busymax-linux-x64-release-bundle.zip > busymax-linux-x64-release-bundle.zip.sha256

- name: Upload Linux release bundle
uses: actions/upload-artifact@v4
with:
name: todomax-linux-x64-release-bundle
name: busymax-linux-x64-release-bundle
path: |
build/linux/x64/release/todomax-linux-x64-release-bundle.zip
build/linux/x64/release/todomax-linux-x64-release-bundle.zip.sha256
build/linux/x64/release/busymax-linux-x64-release-bundle.zip
build/linux/x64/release/busymax-linux-x64-release-bundle.zip.sha256
if-no-files-found: error
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,5 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release

.snap-local/
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,9 @@ flutter run -d linux \
--dart-define=GOOGLE_OAUTH_CLIENT_ID=<google-client-id> \
--dart-define=GOOGLE_OAUTH_CLIENT_SECRET=<google-secret-if-needed> \
--dart-define=MICROSOFT_OAUTH_CLIENT_ID=<microsoft-client-id>
```
```

## Beta snap

See [Beta Snap Release](docs/beta_snap_release.md) for local beta build,
install, and validation notes.
Binary file added assets/branding/busymax-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 80 additions & 0 deletions docs/beta_snap_release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# BusyMax Beta Snap Release

BusyMax `0.1.0+1` is a beta release target for public/listed visibility in
Ubuntu App Center and the Snap Store.

## Build

The snap build requires OAuth configuration at build time. Do not commit real
values.

```bash
mkdir -p .snap-local
$EDITOR .snap-local/busymax-dart-defines.json
snapcraft pack --use-lxd
```

The ignored `.snap-local/busymax-dart-defines.json` file must contain the
required Dart defines. The build fails if that local file is missing.

## Install The Beta

For local validation:

```bash
sudo snap install --dangerous busymax_0.1.0+1_amd64.snap
```

For store users after the revision is uploaded and released to beta:

```bash
sudo snap install busymax --beta
```

## Scope

- Snap confinement is strict.
- The tray/status-notifier feature and background-on-close behavior are enabled
by default for the beta. Users can disable them in settings.
- Tray support currently uses a vendored patched `xdg_status_notifier_item`
StatusNotifierItem/DBusMenu dependency under `third_party`.
- The tray menu is intentionally simple: Open BusyMax, Agenda, and Quit.
- The tray Agenda action opens the compact Agenda utility window. On GNOME
Wayland this window is a normal top-level utility window and may be placed by
Mutter instead of under the tray icon. Under X11/XWayland, BusyMax requests a
top-right position when the backend supports absolute movement.
- Settings, task data, and token metadata are stored inside the snap user data
sandbox. Normal app restarts preserve data. Removing the snap removes user
data unless snapd creates and restores a snapshot.
- OAuth uses the system browser and a local loopback callback listener.
- OAuth tokens use the XDG Secret portal in the snap to retrieve a
per-application encryption secret, then store only AES-GCM ciphertext under
`XDG_DATA_HOME`. BusyMax must not require the `password-manager-service`
interface.

## Validation Matrix

Record the exact desktop environments tested before upload:

- Ubuntu GNOME on Wayland: pending local installed-snap validation.
- Ubuntu GNOME on X11/XWayland: pending local installed-snap validation.

Required smoke checks before upload:

- Launch from terminal and desktop launcher.
- Google and Microsoft sign-in open in the browser and complete the loopback
callback.
- Secure token storage survives app restart.
- `grep -R` over snap user data does not show plaintext OAuth tokens.
- Settings and task data survive app restart.
- Notifications appear through the desktop notification service.
- Tray icon appears, the menu opens, Open BusyMax restores the existing main
window, Agenda opens the compact Agenda window, and Quit exits cleanly.
- Compact Agenda data loads through the main-window bridge without opening a
second migrating SQLite connection.

## Reporting Bugs

Report beta issues at https://github.com/busystack/busymax/issues.

Source code is available at https://github.com/busystack/busymax.
10 changes: 9 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import 'src/app/app_bootstrap.dart';
import 'src/app/busymax_app.dart';
import 'src/config/build_config.dart';
import 'src/core/logging/redacting_logger.dart';
import 'src/features/schedule/application/compact_agenda_data.dart';
import 'src/features/schedule/presentation/compact_agenda_app.dart';
import 'src/platform/main_window_command_client.dart';
import 'src/platform/busymax_window_args.dart';

Future<void> main(List<String> args) async {
Expand All @@ -29,7 +31,13 @@ Future<void> main(List<String> args) async {
await configureCompactAgendaNativeWindow();
runApp(
ProviderScope(
overrides: overrides,
overrides: [
...overrides,
compactAgendaDataLoaderProvider.overrideWithValue(
(ref, query) =>
const MainWindowCommandClient().compactAgendaSnapshot(query),
),
],
child: BusyMaxCompactAgendaApp(
windowController: windowController,
windowArgs: windowArgs,
Expand Down
7 changes: 7 additions & 0 deletions lib/src/app/app_bootstrap.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';

import 'package:drift/drift.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
Expand Down Expand Up @@ -26,6 +27,7 @@ import '../google_tasks/http/retrying_http_client.dart';
import '../google_tasks/oauth/oauth_loopback_flow.dart';
import '../google_tasks/oauth/oauth_service.dart';
import '../google_tasks/oauth/oauth_token_store.dart';
import '../google_tasks/oauth/portal_encrypted_oauth_token_store.dart';
import '../google_calendar/google_calendar_api_client.dart';
import '../microsoft_calendar/microsoft_calendar_api_client.dart';
import '../microsoft_todo/api/microsoft_todo_api_client.dart';
Expand Down Expand Up @@ -67,6 +69,11 @@ final retryingHttpClientProvider = Provider<http.Client>((ref) {
});

final oAuthTokenStoreProvider = Provider<OAuthTokenStore>((ref) {
if (Platform.isLinux && (Platform.environment['SNAP']?.isNotEmpty ?? false)) {
// flutter_secure_storage_linux warms up direct libsecret first, so
// SECRET_BACKEND=file cannot avoid a locked keyring inside strict snaps.
return PortalEncryptedOAuthTokenStore();
}
return SecureOAuthTokenStore(ref.watch(secureStorageProvider));
});

Expand Down
2 changes: 1 addition & 1 deletion lib/src/app/app_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class AppSettings {
notifyDueToday: false,
notifyEventReminders: true,
notifyTaskReminders: true,
runInBackgroundWhenClosed: false,
runInBackgroundWhenClosed: true,
showTrayIcon: true,
startMinimizedToTray: false,
quitExitsCompletely: true,
Expand Down
66 changes: 8 additions & 58 deletions lib/src/app/busymax_about_dialog.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import 'dart:async';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path/path.dart' as p;
import 'package:url_launcher/url_launcher.dart';
import 'package:yaru/yaru.dart';

Expand Down Expand Up @@ -131,69 +128,22 @@ class BusyMaxAboutDialog extends StatelessWidget {
}
}

class _BusyMaxLogo extends StatefulWidget {
class _BusyMaxLogo extends StatelessWidget {
const _BusyMaxLogo({required this.size});

final double size;

@override
State<_BusyMaxLogo> createState() => _BusyMaxLogoState();
}

class _BusyMaxLogoState extends State<_BusyMaxLogo> {
late final Future<Uint8List?> _logoBytes = _loadLogoBytes();

@override
Widget build(BuildContext context) {
return FutureBuilder<Uint8List?>(
future: _logoBytes,
builder: (context, snapshot) {
final bytes = snapshot.data;
if (bytes != null) {
return Image.memory(
bytes,
width: widget.size,
height: widget.size,
filterQuality: FilterQuality.high,
);
}
return SizedBox.square(dimension: widget.size);
},
return Image.asset(
'assets/branding/busymax-logo.png',
width: size,
height: size,
filterQuality: FilterQuality.high,
errorBuilder: (context, error, stackTrace) =>
SizedBox.square(dimension: size),
);
}

Future<Uint8List?> _loadLogoBytes() async {
const assetPath = 'assets/branding/busymax-logo.svg';
try {
final data = await rootBundle.load(assetPath);
return Uint8List.view(
data.buffer,
data.offsetInBytes,
data.lengthInBytes,
);
} on Object {
return _loadLogoFileBytes(assetPath);
}
}

Future<Uint8List?> _loadLogoFileBytes(String assetPath) async {
final executableDir = File(Platform.resolvedExecutable).parent.path;
final candidates = [
p.join(executableDir, 'data', 'flutter_assets', assetPath),
assetPath,
];
for (final candidate in candidates) {
try {
final file = File(candidate);
if (await file.exists()) {
return file.readAsBytes();
}
} on Object {
// Try the next known location.
}
}
return null;
}
}

class _VersionTag extends StatelessWidget {
Expand Down
52 changes: 44 additions & 8 deletions lib/src/app/busymax_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:ubuntu_localizations/ubuntu_localizations.dart';
import '../platform/busymax_tray_service.dart';
import '../platform/gtk_font_service.dart';
import '../platform/linux_header_bar_service.dart';
import '../platform/linux_window_service.dart';
import '../platform/main_window_command_bridge.dart';
import 'app_bootstrap.dart';
import 'app_router.dart';
Expand All @@ -28,6 +29,7 @@ class _BusyMaxAppState extends ConsumerState<BusyMaxApp> {
BusyMaxTrayService? _trayService;
bool? _lastHideOnClose;
bool? _lastTrayEnabled;
bool _startMinimizedHandled = false;

@override
void dispose() {
Expand Down Expand Up @@ -87,7 +89,7 @@ class _BusyMaxAppState extends ConsumerState<BusyMaxApp> {
ref,
settings,
BusyMaxTrayLabels(
openBusyMax: l10n.trayAgendaOpenBusyMax,
openBusyMax: l10n.compactAgendaOpenBusyMax,
agenda: l10n.viewAgenda,
quitBusyMax: l10n.exit,
),
Expand Down Expand Up @@ -164,17 +166,17 @@ class _BusyMaxAppState extends ConsumerState<BusyMaxApp> {
BusyMaxTrayLabels labels,
) {
final windowService = ref.read(linuxWindowServiceProvider);
if (_lastHideOnClose != settings.runInBackgroundWhenClosed) {
_lastHideOnClose = settings.runInBackgroundWhenClosed;
unawaited(
windowService.setHideOnClose(settings.runInBackgroundWhenClosed),
);
}

final trayEnabled =
settings.showTrayIcon ||
settings.runInBackgroundWhenClosed ||
settings.startMinimizedToTray;
_setHideOnClose(
windowService,
settings.runInBackgroundWhenClosed &&
trayEnabled &&
(_trayService?.available ?? false),
);
if (_trayService != null) {
unawaited(_trayService!.updateLabels(labels));
}
Expand All @@ -190,11 +192,45 @@ class _BusyMaxAppState extends ConsumerState<BusyMaxApp> {
onBeforeQuit: compactAgendaWindows.closeIfOpen,
);
if (trayEnabled) {
unawaited(tray.start());
unawaited(
_startTray(
tray,
windowService,
runInBackgroundWhenClosed: settings.runInBackgroundWhenClosed,
startMinimizedToTray: settings.startMinimizedToTray,
),
);
} else {
_setHideOnClose(windowService, false);
unawaited(tray.stop());
}
}

void _setHideOnClose(LinuxWindowService windowService, bool enabled) {
if (_lastHideOnClose == enabled) {
return;
}
_lastHideOnClose = enabled;
unawaited(windowService.setHideOnClose(enabled));
}

Future<void> _startTray(
BusyMaxTrayService tray,
LinuxWindowService windowService, {
required bool runInBackgroundWhenClosed,
required bool startMinimizedToTray,
}) async {
await tray.start();
if (!mounted) {
return;
}
_setHideOnClose(windowService, runInBackgroundWhenClosed && tray.available);
if (!startMinimizedToTray || _startMinimizedHandled || !tray.available) {
return;
}
_startMinimizedHandled = true;
await windowService.hideWindow();
}
}

class _BusyMaxWindowCornerClip extends StatelessWidget {
Expand Down
17 changes: 10 additions & 7 deletions lib/src/core/logging/redacting_logger.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';

final _sensitivePatterns = <RegExp>[
Expand Down Expand Up @@ -38,14 +41,14 @@ String redactForLog(Object? value) {
void configureLogging() {
Logger.root.level = Level.INFO;
Logger.root.onRecord.listen((record) {
assert(() {
// ignore: avoid_print
print(
final line =
'[${record.level.name}] ${record.loggerName}: '
'${redactForLog(record.message)}',
);
return true;
}());
'${redactForLog(record.message)}';
if (kDebugMode) {
debugPrint(line);
} else {
stderr.writeln(line);
}
});
}

Expand Down
Loading
Loading