fix(app): a deleted Pomodoro profile takes its timer and choice along

Found testing on the laptop: deleting the chosen profile left the
choice pointing at it, and a timer running on it went on with the
deleted profile's lengths and name. Deleting it now chooses the first
profile left, and the timer goes on with that one, its phase keeping
its end.

The top bar's suggestion didn't show with the app started from
VSCodium's terminal, which sets XDG_CURRENT_DESKTOP to Unity for what
it starts. The app now asks the bus whether GNOME's Shell is there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-07 11:08:40 +02:00
commit ead9c95049
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
6 changed files with 77 additions and 26 deletions

View file

@ -28,13 +28,16 @@ class PomodoroBloc(
on<PomodoroEventStarted>(_onStarted);
on<PomodoroEventSettingsChanged>((event, emit) async {
final run = state.run;
final profile = event.settings.profiles
.where((each) => each.id == run?.profile.id)
.firstOrNull;
// The profile running, as it is now; the one chosen if it is gone.
final profile =
event.settings.profiles
.where((each) => each.id == run?.profile.id)
.firstOrNull ??
event.settings.selected;
await _emit(
emit,
settings: event.settings,
run: run == null || profile == null ? run : run.withProfile(profile),
run: run?.withProfile(profile),
);
});
on<PomodoroEventStartRequested>(

View file

@ -37,7 +37,6 @@ class LinuxPomodoroPlatform(
final Directory _folder, {
RunCommand run = Process.run,
Future<void> Function(Uri page) open = _openPage,
String? desktop,
}) implements PomodoroPlatform {
static const _notifications = 'org.freedesktop.Notifications';
static const _watcher = 'org.kde.StatusNotifierWatcher';
@ -55,14 +54,6 @@ class LinuxPomodoroPlatform(
final RunCommand _run = run;
final Future<void> Function(Uri page) _open = open;
final String? _desktop = desktop;
/// Whether the desktop is GNOME, where the extension can be suggested.
late final bool _gnome =
(_desktop ?? Platform.environment['XDG_CURRENT_DESKTOP'] ?? '')
.toUpperCase()
.split(':')
.contains('GNOME');
late final File _quietBefore = File(
p.join(_folder.path, 'pomodoro_quiet_before'),
@ -242,11 +233,14 @@ class LinuxPomodoroPlatform(
}
Future<bool> _suggestsTopBar() async {
if (!_gnome || _topBarDismissed.existsSync()) {
if (_topBarDismissed.existsSync()) {
return false;
}
try {
return !await _bus.nameHasOwner(_watcher);
// GNOME's Shell on the bus, rather than XDG_CURRENT_DESKTOP, which
// what starts the app may have changed (VS Code's terminal says Unity).
return await _bus.nameHasOwner('org.gnome.Shell') &&
!await _bus.nameHasOwner(_watcher);
} on Exception {
// No session bus to ask: nothing to set up either.
return false;

View file

@ -174,14 +174,20 @@ final class const PomodoroSettings({
selectedId: selectedId,
);
/// These settings without the profile [id].
PomodoroSettings withoutProfile(String id) => PomodoroSettings(
profiles: [
/// These settings without the profile [id]; the first one left is chosen
/// if it was.
PomodoroSettings withoutProfile(String id) {
final left = [
for (final each in profiles)
if (each.id != id) each,
],
selectedId: selectedId,
);
];
return PomodoroSettings(
profiles: left,
selectedId: selectedId == id && left.isNotEmpty
? left.first.id
: selectedId,
);
}
/// These settings with the profile [id] chosen.
PomodoroSettings withSelected(String id) =>

View file

@ -129,6 +129,30 @@ void main() {
});
});
test('a timer on a profile deleted goes on with the one chosen then', () {
withBloc((async, bloc, settings, store) {
const mine = PomodoroProfile(
id: 'mine',
name: 'Mine',
shortBreakMinutes: 9,
);
bloc
..add(const PomodoroEventProfileSaved(mine))
..add(const PomodoroEventProfileChosen('mine'));
async.flushMicrotasks();
bloc.add(const PomodoroEventStartRequested());
async.flushMicrotasks();
expect(bloc.state.run!.profile, mine);
bloc.add(const PomodoroEventProfileDeleted('mine'));
async.flushMicrotasks();
expect(bloc.state.settings.selected, PomodoroProfile.classic);
expect(bloc.state.run!.profile, PomodoroProfile.classic);
expect(bloc.state.run!.isRunning, isTrue, reason: 'it goes on');
});
});
test("adds, chooses and deletes profiles in the person's settings", () {
withBloc((async, bloc, settings, store) {
const mine = PomodoroProfile(id: 'mine', name: 'Mine');

View file

@ -85,10 +85,9 @@ void main() {
late List<Uri> opened;
late LinuxPomodoroPlatform platform;
LinuxPomodoroPlatform platformOn(String desktop) => LinuxPomodoroPlatform(
LinuxPomodoroPlatform newPlatform() => LinuxPomodoroPlatform(
app,
folder,
desktop: desktop,
run: (command, arguments) async {
if (arguments.first == 'set') {
banners = arguments.last;
@ -111,7 +110,7 @@ void main() {
folder = await Directory.systemTemp.createTemp('pomodoro_linux_test');
banners = 'true';
opened = [];
platform = platformOn('ubuntu:GNOME');
platform = newPlatform();
});
tearDown(() async {
await app.close();
@ -272,6 +271,14 @@ void main() {
});
group('without a top bar for it', () {
late DBusClient gnomeShell;
setUp(() async {
gnomeShell = DBusClient(address);
await gnomeShell.requestName('org.gnome.Shell');
});
tearDown(() => gnomeShell.close());
test('suggests setting one up on GNOME, until dismissed', () async {
final suggestions = <bool>[];
final subscription = platform.suggestsTopBar.listen(suggestions.add);
@ -282,11 +289,14 @@ void main() {
await platform.dismissTopBarSuggestion();
await Future<void>.delayed(const Duration(milliseconds: 100));
expect(suggestions, [true, false]);
expect(await platformOn('ubuntu:GNOME').suggestsTopBar.first, isFalse);
expect(await newPlatform().suggestsTopBar.first, isFalse);
});
test('suggests nothing elsewhere than on GNOME', () async {
expect(await platformOn('KDE').suggestsTopBar.first, isFalse);
await gnomeShell.close();
await Future<void>.delayed(const Duration(milliseconds: 100));
expect(await newPlatform().suggestsTopBar.first, isFalse);
});
test('stops suggesting once a watcher turns up', () async {

View file

@ -33,6 +33,20 @@ void main() {
expect(settings.selected, PomodoroProfile.classic);
});
test('deleting the chosen profile chooses the first one left', () {
const mine = PomodoroProfile(id: 'mine');
const settings = PomodoroSettings(
profiles: [PomodoroProfile.classic, mine],
selectedId: 'mine',
);
expect(settings.withoutProfile('mine').selectedId, 'classic');
expect(
settings.withSelected('classic').withoutProfile('mine').selectedId,
'classic',
);
});
test('a chosen profile that is gone falls back to the first', () {
const settings = PomodoroSettings(selectedId: 'gone');