improved quick tab switcher & general tab design/logic;
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,8 @@ import 'package:weblibre/extensions/uri.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/pending_tab_selection.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/restore_complete.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_list.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||
@@ -456,6 +458,14 @@ class TabRepository extends _$TabRepository {
|
||||
}
|
||||
|
||||
Future<bool> selectTab(String tabId) async {
|
||||
// The tab is still a pre-restore placeholder (known to the DB but not to
|
||||
// the engine yet): queue the selection until the native state arrives.
|
||||
if (!ref.read(browserRestoreCompleteProvider) &&
|
||||
!ref.read(tabStatesProvider).containsKey(tabId)) {
|
||||
ref.read(pendingTabSelectionProvider.notifier).queue(tabId);
|
||||
return true;
|
||||
}
|
||||
|
||||
final containerData = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.getTabContainerData(tabId);
|
||||
@@ -768,6 +778,18 @@ class TabRepository extends _$TabRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _drainPendingIsolationCleanup() async {
|
||||
if (_pendingIsolationCleanup.isEmpty) return;
|
||||
|
||||
final pending = Set<String>.of(_pendingIsolationCleanup);
|
||||
_pendingIsolationCleanup.clear();
|
||||
|
||||
for (final contextId in pending) {
|
||||
if (!ref.mounted) break;
|
||||
await _cleanupIsolationContextIfEmpty(contextId);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> undoClose() {
|
||||
// Suppress the next reclose pass: undo can resurrect a tab whose
|
||||
// tombstone is still on disk (from a previous session); without this
|
||||
@@ -1037,9 +1059,14 @@ class TabRepository extends _$TabRepository {
|
||||
return;
|
||||
}
|
||||
|
||||
//Only sync tabs if there has been a previous value or is not empty
|
||||
//Only sync tabs if there has been a previous value or is not empty.
|
||||
//Additionally require the native session restore to have completed:
|
||||
//a partial pre-restore list (e.g. a share-intent tab arriving first)
|
||||
//must not delete the cached rows of tabs that are still being
|
||||
//restored.
|
||||
final shouldSyncTabs =
|
||||
next.value.isNotEmpty || (previous?.value.isNotEmpty ?? false);
|
||||
ref.read(browserRestoreCompleteProvider) &&
|
||||
(next.value.isNotEmpty || (previous?.value.isNotEmpty ?? false));
|
||||
|
||||
if (shouldSyncTabs) {
|
||||
final syncTabsResult = await db.tabDao.syncTabs(
|
||||
@@ -1054,14 +1081,7 @@ class TabRepository extends _$TabRepository {
|
||||
|
||||
// Process pending isolation context cleanups after syncTabs
|
||||
// has deleted the rows, so the count check is accurate.
|
||||
if (_pendingIsolationCleanup.isNotEmpty) {
|
||||
final pending = Set<String>.of(_pendingIsolationCleanup);
|
||||
_pendingIsolationCleanup.clear();
|
||||
for (final contextId in pending) {
|
||||
if (!ref.mounted) break;
|
||||
await _cleanupIsolationContextIfEmpty(contextId);
|
||||
}
|
||||
}
|
||||
await _drainPendingIsolationCleanup();
|
||||
|
||||
// One-shot orphan cleanup after tab list stabilizes (5s debounce).
|
||||
// Also runs for DB-only contexts whose rows were already deleted
|
||||
@@ -1084,6 +1104,26 @@ class TabRepository extends _$TabRepository {
|
||||
},
|
||||
);
|
||||
|
||||
// Catch up on the tab list emissions skipped while the restore-complete
|
||||
// gate above was closed: reconcile the DB once against the current list.
|
||||
ref.listen(browserRestoreCompleteProvider, (
|
||||
previous,
|
||||
restoreComplete,
|
||||
) async {
|
||||
if (restoreComplete && !(previous ?? false)) {
|
||||
final currentTabs = ref.read(tabListProvider).value;
|
||||
if (currentTabs.isNotEmpty) {
|
||||
final syncTabsResult = await db.tabDao.syncTabs(
|
||||
retainTabIds: currentTabs,
|
||||
);
|
||||
_pendingIsolationCleanup.addAll(
|
||||
syncTabsResult.deletedIsolationContextIds,
|
||||
);
|
||||
await _drainPendingIsolationCleanup();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
final tabStateDebouncer = Debouncer(const Duration(seconds: 1));
|
||||
Map<String, TabState>? debounceStartValue;
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$tabRepositoryHash() => r'd7b67460388c264f477325266a3299fe6df2df07';
|
||||
String _$tabRepositoryHash() => r'982b80b8ea7c694958bc10cc7e8e15310af6a238';
|
||||
|
||||
abstract class _$TabRepository extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
Reference in New Issue
Block a user