improved quick tab switcher & general tab design/logic;

This commit is contained in:
Fabian Freund
2026-06-12 03:47:20 +02:00
parent 83c78b53d3
commit 3c5f472bdf
35 changed files with 2715 additions and 768 deletions
@@ -0,0 +1,80 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:async';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/core/logger.dart';
import 'package:weblibre/features/geckoview/domain/providers/restore_complete.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
part 'pending_tab_selection.g.dart';
/// Queues the selection of a tab that exists in the local DB but has not
/// been delivered by the native session restore yet (placeholder chips at
/// cold start). The queued tab is selected as soon as its native state
/// arrives; the queue clears itself if restore finishes without it.
@Riverpod(keepAlive: true)
class PendingTabSelection extends _$PendingTabSelection {
void queue(String tabId) {
if (state != tabId) {
state = tabId;
}
}
void clear() {
state = null;
}
@override
String? build() {
ref.listen(
tabStatesProvider,
(previous, next) {
final pendingTabId = state;
if (pendingTabId != null && next.containsKey(pendingTabId)) {
state = null;
unawaited(
ref.read(tabRepositoryProvider.notifier).selectTab(pendingTabId),
);
}
},
onError: (error, stackTrace) {
logger.e(
'Error listening to tabStatesProvider',
error: error,
stackTrace: stackTrace,
);
},
);
// Stale-tab safety: when restore finishes and the queued id never
// appeared, the tab no longer exists.
ref.listen(browserRestoreCompleteProvider, (previous, next) {
if (next &&
state != null &&
!ref.read(tabStatesProvider).containsKey(state)) {
state = null;
}
});
return null;
}
}
@@ -0,0 +1,80 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'pending_tab_selection.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Queues the selection of a tab that exists in the local DB but has not
/// been delivered by the native session restore yet (placeholder chips at
/// cold start). The queued tab is selected as soon as its native state
/// arrives; the queue clears itself if restore finishes without it.
@ProviderFor(PendingTabSelection)
final pendingTabSelectionProvider = PendingTabSelectionProvider._();
/// Queues the selection of a tab that exists in the local DB but has not
/// been delivered by the native session restore yet (placeholder chips at
/// cold start). The queued tab is selected as soon as its native state
/// arrives; the queue clears itself if restore finishes without it.
final class PendingTabSelectionProvider
extends $NotifierProvider<PendingTabSelection, String?> {
/// Queues the selection of a tab that exists in the local DB but has not
/// been delivered by the native session restore yet (placeholder chips at
/// cold start). The queued tab is selected as soon as its native state
/// arrives; the queue clears itself if restore finishes without it.
PendingTabSelectionProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'pendingTabSelectionProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$pendingTabSelectionHash();
@$internal
@override
PendingTabSelection create() => PendingTabSelection();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(String? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<String?>(value),
);
}
}
String _$pendingTabSelectionHash() =>
r'761d5814b40b1003b59db09a0bb7ef370530b317';
/// Queues the selection of a tab that exists in the local DB but has not
/// been delivered by the native session restore yet (placeholder chips at
/// cold start). The queued tab is selected as soon as its native state
/// arrives; the queue clears itself if restore finishes without it.
abstract class _$PendingTabSelection extends $Notifier<String?> {
String? build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<String?, String?>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<String?, String?>,
String?,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/core/logger.dart';
import 'package:weblibre/features/geckoview/domain/providers.dart';
part 'restore_complete.g.dart';
/// Mirrors the native `BrowserState.restoreComplete` flag: false until the
/// session restore has dispatched all persisted tabs into the BrowserStore.
/// While false, DB-cached tabs without a native state are rendered as
/// placeholders and the destructive tab DB sync is deferred.
@Riverpod(keepAlive: true)
class BrowserRestoreComplete extends _$BrowserRestoreComplete {
@override
bool build() {
final eventService = ref.watch(eventServiceProvider);
ref.listen(
fireImmediately: true,
engineReadyStateProvider,
(previous, next) async {
if (next) {
await GeckoTabService().syncEvents(onRestoreComplete: true);
}
},
onError: (error, stackTrace) {
logger.e(
'Error listening to engineReadyStateProvider',
error: error,
stackTrace: stackTrace,
);
},
);
final restoreCompleteSub = eventService.restoreCompleteEvents.listen(
(restoreComplete) {
if (restoreComplete != state) {
state = restoreComplete;
}
},
onError: (Object error, StackTrace stackTrace) {
logger.e(
'Error in restore complete events',
error: error,
stackTrace: stackTrace,
);
},
);
ref.onDispose(() async {
await restoreCompleteSub.cancel();
});
return eventService.restoreCompleteEvents.valueOrNull ?? false;
}
}
@@ -0,0 +1,80 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'restore_complete.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Mirrors the native `BrowserState.restoreComplete` flag: false until the
/// session restore has dispatched all persisted tabs into the BrowserStore.
/// While false, DB-cached tabs without a native state are rendered as
/// placeholders and the destructive tab DB sync is deferred.
@ProviderFor(BrowserRestoreComplete)
final browserRestoreCompleteProvider = BrowserRestoreCompleteProvider._();
/// Mirrors the native `BrowserState.restoreComplete` flag: false until the
/// session restore has dispatched all persisted tabs into the BrowserStore.
/// While false, DB-cached tabs without a native state are rendered as
/// placeholders and the destructive tab DB sync is deferred.
final class BrowserRestoreCompleteProvider
extends $NotifierProvider<BrowserRestoreComplete, bool> {
/// Mirrors the native `BrowserState.restoreComplete` flag: false until the
/// session restore has dispatched all persisted tabs into the BrowserStore.
/// While false, DB-cached tabs without a native state are rendered as
/// placeholders and the destructive tab DB sync is deferred.
BrowserRestoreCompleteProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'browserRestoreCompleteProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$browserRestoreCompleteHash();
@$internal
@override
BrowserRestoreComplete create() => BrowserRestoreComplete();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$browserRestoreCompleteHash() =>
r'971ce7722781b3cb581cfa9ada0884e91f723b85';
/// Mirrors the native `BrowserState.restoreComplete` flag: false until the
/// session restore has dispatched all persisted tabs into the BrowserStore.
/// While false, DB-cached tabs without a native state are rendered as
/// placeholders and the destructive tab DB sync is deferred.
abstract class _$BrowserRestoreComplete extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}