performance improvements

This commit is contained in:
Fabian Freund
2026-07-31 05:14:27 +02:00
parent c2c7cc0729
commit bdfc851681
36 changed files with 1988 additions and 608 deletions
@@ -0,0 +1,186 @@
/*
* 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/>.
*/
/// High-churn per-tab state, deliberately kept *out* of [TabState].
///
/// [TabState] is fanned out to the always-visible browser chrome — the quick
/// tab switcher chips, the grouped tab list, the contextual toolbar — through
/// providers that watch the whole `Map<String, TabState>`. Any field that ticks
/// during a page load or on a background timer therefore rebuilt every chip on
/// screen, even though no chip renders that field.
///
/// The fields here are each consumed by exactly one or two widgets, so they
/// live in their own keyed notifiers and are read through per-tab selectors.
/// A progress tick or a background tab's screenshot now invalidates only the
/// widget that actually shows it.
library;
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/domain/entities/equatable_image.dart';
import 'package:weblibre/features/geckoview/domain/entities/states/find_result.dart';
import 'package:weblibre/features/geckoview/domain/entities/states/history.dart';
import 'package:weblibre/features/geckoview/domain/entities/states/translation.dart';
part 'tab_detail_state.g.dart';
/// Load progress (0-100) per tab. Ticks continuously while a page loads.
@Riverpod(keepAlive: true)
class TabProgressStates extends _$TabProgressStates {
@override
Map<String, int> build() => const {};
void update(String tabId, int progress) {
if (state[tabId] == progress) {
return;
}
state = {...state}..[tabId] = progress;
}
}
@Riverpod()
int tabProgress(Ref ref, String? tabId) {
if (tabId == null) {
return 0;
}
return ref.watch(tabProgressStatesProvider.select((s) => s[tabId] ?? 0));
}
/// Page screenshots per tab. Refreshed on a 10s timer for the selected tab and
/// consumed only by the tab tray previews.
@Riverpod(keepAlive: true)
class TabThumbnails extends _$TabThumbnails {
@override
Map<String, EquatableImage> build() => const {};
void update(String tabId, EquatableImage? thumbnail) {
if (thumbnail == null) {
if (!state.containsKey(tabId)) {
return;
}
state = {...state}..remove(tabId);
return;
}
if (state[tabId] == thumbnail) {
return;
}
state = {...state}..[tabId] = thumbnail;
}
}
@Riverpod()
EquatableImage? tabThumbnail(Ref ref, String? tabId) {
if (tabId == null) {
return null;
}
return ref.watch(tabThumbnailsProvider.select((s) => s[tabId]));
}
/// Session history (back/forward stack) per tab.
@Riverpod(keepAlive: true)
class TabHistoryStates extends _$TabHistoryStates {
@override
Map<String, HistoryState> build() => const {};
void update(String tabId, HistoryState history) {
if (state[tabId] == history) {
return;
}
state = {...state}..[tabId] = history;
}
}
@Riverpod()
HistoryState tabHistoryState(Ref ref, String? tabId) {
if (tabId == null) {
return HistoryState.$default();
}
return ref.watch(
tabHistoryStatesProvider.select((s) => s[tabId] ?? HistoryState.$default()),
);
}
/// Find-in-page match counters per tab. Emitted at a high rate by Gecko while
/// a search is running.
@Riverpod(keepAlive: true)
class TabFindResultStates extends _$TabFindResultStates {
@override
Map<String, FindResultState> build() => const {};
void update(String tabId, FindResultState result) {
if (state[tabId] == result) {
return;
}
state = {...state}..[tabId] = result;
}
FindResultState resultFor(String tabId) =>
state[tabId] ?? FindResultState.$default();
}
@Riverpod()
FindResultState tabFindResultState(Ref ref, String? tabId) {
if (tabId == null) {
return FindResultState.$default();
}
return ref.watch(
tabFindResultStatesProvider.select(
(s) => s[tabId] ?? FindResultState.$default(),
),
);
}
/// Translation progress/result per tab.
@Riverpod(keepAlive: true)
class TabTranslationStates extends _$TabTranslationStates {
@override
Map<String, TranslationState> build() => const {};
void update(String tabId, TranslationState translation) {
if (state[tabId] == translation) {
return;
}
state = {...state}..[tabId] = translation;
}
}
@Riverpod()
TranslationState tabTranslationState(Ref ref, String? tabId) {
if (tabId == null) {
return TranslationState.$default();
}
return ref.watch(
tabTranslationStatesProvider.select(
(s) => s[tabId] ?? TranslationState.$default(),
),
);
}
@@ -0,0 +1,727 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'tab_detail_state.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Load progress (0-100) per tab. Ticks continuously while a page loads.
@ProviderFor(TabProgressStates)
final tabProgressStatesProvider = TabProgressStatesProvider._();
/// Load progress (0-100) per tab. Ticks continuously while a page loads.
final class TabProgressStatesProvider
extends $NotifierProvider<TabProgressStates, Map<String, int>> {
/// Load progress (0-100) per tab. Ticks continuously while a page loads.
TabProgressStatesProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'tabProgressStatesProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabProgressStatesHash();
@$internal
@override
TabProgressStates create() => TabProgressStates();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Map<String, int> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Map<String, int>>(value),
);
}
}
String _$tabProgressStatesHash() => r'60bb03e33e4322eb2869b90b5cc5802d3cdc225d';
/// Load progress (0-100) per tab. Ticks continuously while a page loads.
abstract class _$TabProgressStates extends $Notifier<Map<String, int>> {
Map<String, int> build();
@$mustCallSuper
@override
WhenComplete runBuild() {
final ref = this.ref as $Ref<Map<String, int>, Map<String, int>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<Map<String, int>, Map<String, int>>,
Map<String, int>,
Object?,
Object?
>;
return element.handleCreate(ref, build);
}
}
@ProviderFor(tabProgress)
final tabProgressProvider = TabProgressFamily._();
final class TabProgressProvider extends $FunctionalProvider<int, int, int>
with $Provider<int> {
TabProgressProvider._({
required TabProgressFamily super.from,
required String? super.argument,
}) : super(
retry: null,
name: r'tabProgressProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabProgressHash();
@override
String toString() {
return r'tabProgressProvider'
''
'($argument)';
}
@$internal
@override
$ProviderElement<int> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
int create(Ref ref) {
final argument = this.argument as String?;
return tabProgress(ref, argument);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(int value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<int>(value),
);
}
@override
bool operator ==(Object other) {
return other is TabProgressProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$tabProgressHash() => r'83c4990d96f2160e919ee99882905c09872e8bb7';
final class TabProgressFamily extends $Family
with $FunctionalFamilyOverride<int, String?> {
TabProgressFamily._()
: super(
retry: null,
name: r'tabProgressProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
TabProgressProvider call(String? tabId) =>
TabProgressProvider._(argument: tabId, from: this);
@override
String toString() => r'tabProgressProvider';
}
/// Page screenshots per tab. Refreshed on a 10s timer for the selected tab and
/// consumed only by the tab tray previews.
@ProviderFor(TabThumbnails)
final tabThumbnailsProvider = TabThumbnailsProvider._();
/// Page screenshots per tab. Refreshed on a 10s timer for the selected tab and
/// consumed only by the tab tray previews.
final class TabThumbnailsProvider
extends $NotifierProvider<TabThumbnails, Map<String, EquatableImage>> {
/// Page screenshots per tab. Refreshed on a 10s timer for the selected tab and
/// consumed only by the tab tray previews.
TabThumbnailsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'tabThumbnailsProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabThumbnailsHash();
@$internal
@override
TabThumbnails create() => TabThumbnails();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Map<String, EquatableImage> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Map<String, EquatableImage>>(value),
);
}
}
String _$tabThumbnailsHash() => r'16c4bb6edff2e6413100d9ce02c1ae7948740cfe';
/// Page screenshots per tab. Refreshed on a 10s timer for the selected tab and
/// consumed only by the tab tray previews.
abstract class _$TabThumbnails extends $Notifier<Map<String, EquatableImage>> {
Map<String, EquatableImage> build();
@$mustCallSuper
@override
WhenComplete runBuild() {
final ref =
this.ref
as $Ref<Map<String, EquatableImage>, Map<String, EquatableImage>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
Map<String, EquatableImage>,
Map<String, EquatableImage>
>,
Map<String, EquatableImage>,
Object?,
Object?
>;
return element.handleCreate(ref, build);
}
}
@ProviderFor(tabThumbnail)
final tabThumbnailProvider = TabThumbnailFamily._();
final class TabThumbnailProvider
extends
$FunctionalProvider<EquatableImage?, EquatableImage?, EquatableImage?>
with $Provider<EquatableImage?> {
TabThumbnailProvider._({
required TabThumbnailFamily super.from,
required String? super.argument,
}) : super(
retry: null,
name: r'tabThumbnailProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabThumbnailHash();
@override
String toString() {
return r'tabThumbnailProvider'
''
'($argument)';
}
@$internal
@override
$ProviderElement<EquatableImage?> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
EquatableImage? create(Ref ref) {
final argument = this.argument as String?;
return tabThumbnail(ref, argument);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(EquatableImage? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<EquatableImage?>(value),
);
}
@override
bool operator ==(Object other) {
return other is TabThumbnailProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$tabThumbnailHash() => r'7c8b591abdb5e8cc914ab8e44247a4bca6bddd15';
final class TabThumbnailFamily extends $Family
with $FunctionalFamilyOverride<EquatableImage?, String?> {
TabThumbnailFamily._()
: super(
retry: null,
name: r'tabThumbnailProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
TabThumbnailProvider call(String? tabId) =>
TabThumbnailProvider._(argument: tabId, from: this);
@override
String toString() => r'tabThumbnailProvider';
}
/// Session history (back/forward stack) per tab.
@ProviderFor(TabHistoryStates)
final tabHistoryStatesProvider = TabHistoryStatesProvider._();
/// Session history (back/forward stack) per tab.
final class TabHistoryStatesProvider
extends $NotifierProvider<TabHistoryStates, Map<String, HistoryState>> {
/// Session history (back/forward stack) per tab.
TabHistoryStatesProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'tabHistoryStatesProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabHistoryStatesHash();
@$internal
@override
TabHistoryStates create() => TabHistoryStates();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Map<String, HistoryState> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Map<String, HistoryState>>(value),
);
}
}
String _$tabHistoryStatesHash() => r'3920410334c4354ca66da605d9bfe85b4853f3bf';
/// Session history (back/forward stack) per tab.
abstract class _$TabHistoryStates extends $Notifier<Map<String, HistoryState>> {
Map<String, HistoryState> build();
@$mustCallSuper
@override
WhenComplete runBuild() {
final ref =
this.ref as $Ref<Map<String, HistoryState>, Map<String, HistoryState>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<Map<String, HistoryState>, Map<String, HistoryState>>,
Map<String, HistoryState>,
Object?,
Object?
>;
return element.handleCreate(ref, build);
}
}
@ProviderFor(tabHistoryState)
final tabHistoryStateProvider = TabHistoryStateFamily._();
final class TabHistoryStateProvider
extends $FunctionalProvider<HistoryState, HistoryState, HistoryState>
with $Provider<HistoryState> {
TabHistoryStateProvider._({
required TabHistoryStateFamily super.from,
required String? super.argument,
}) : super(
retry: null,
name: r'tabHistoryStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabHistoryStateHash();
@override
String toString() {
return r'tabHistoryStateProvider'
''
'($argument)';
}
@$internal
@override
$ProviderElement<HistoryState> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
HistoryState create(Ref ref) {
final argument = this.argument as String?;
return tabHistoryState(ref, argument);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(HistoryState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<HistoryState>(value),
);
}
@override
bool operator ==(Object other) {
return other is TabHistoryStateProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$tabHistoryStateHash() => r'5bca69fddb6fc2f3db6caf5c84bd1a7f5728e1d9';
final class TabHistoryStateFamily extends $Family
with $FunctionalFamilyOverride<HistoryState, String?> {
TabHistoryStateFamily._()
: super(
retry: null,
name: r'tabHistoryStateProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
TabHistoryStateProvider call(String? tabId) =>
TabHistoryStateProvider._(argument: tabId, from: this);
@override
String toString() => r'tabHistoryStateProvider';
}
/// Find-in-page match counters per tab. Emitted at a high rate by Gecko while
/// a search is running.
@ProviderFor(TabFindResultStates)
final tabFindResultStatesProvider = TabFindResultStatesProvider._();
/// Find-in-page match counters per tab. Emitted at a high rate by Gecko while
/// a search is running.
final class TabFindResultStatesProvider
extends
$NotifierProvider<TabFindResultStates, Map<String, FindResultState>> {
/// Find-in-page match counters per tab. Emitted at a high rate by Gecko while
/// a search is running.
TabFindResultStatesProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'tabFindResultStatesProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabFindResultStatesHash();
@$internal
@override
TabFindResultStates create() => TabFindResultStates();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Map<String, FindResultState> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Map<String, FindResultState>>(value),
);
}
}
String _$tabFindResultStatesHash() =>
r'f57dee1658eae789002ac3cff15808cbc96b7883';
/// Find-in-page match counters per tab. Emitted at a high rate by Gecko while
/// a search is running.
abstract class _$TabFindResultStates
extends $Notifier<Map<String, FindResultState>> {
Map<String, FindResultState> build();
@$mustCallSuper
@override
WhenComplete runBuild() {
final ref =
this.ref
as $Ref<Map<String, FindResultState>, Map<String, FindResultState>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
Map<String, FindResultState>,
Map<String, FindResultState>
>,
Map<String, FindResultState>,
Object?,
Object?
>;
return element.handleCreate(ref, build);
}
}
@ProviderFor(tabFindResultState)
final tabFindResultStateProvider = TabFindResultStateFamily._();
final class TabFindResultStateProvider
extends
$FunctionalProvider<FindResultState, FindResultState, FindResultState>
with $Provider<FindResultState> {
TabFindResultStateProvider._({
required TabFindResultStateFamily super.from,
required String? super.argument,
}) : super(
retry: null,
name: r'tabFindResultStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabFindResultStateHash();
@override
String toString() {
return r'tabFindResultStateProvider'
''
'($argument)';
}
@$internal
@override
$ProviderElement<FindResultState> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
FindResultState create(Ref ref) {
final argument = this.argument as String?;
return tabFindResultState(ref, argument);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(FindResultState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<FindResultState>(value),
);
}
@override
bool operator ==(Object other) {
return other is TabFindResultStateProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$tabFindResultStateHash() =>
r'0f58040111bb43d830e14ae44569265b9ae804d9';
final class TabFindResultStateFamily extends $Family
with $FunctionalFamilyOverride<FindResultState, String?> {
TabFindResultStateFamily._()
: super(
retry: null,
name: r'tabFindResultStateProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
TabFindResultStateProvider call(String? tabId) =>
TabFindResultStateProvider._(argument: tabId, from: this);
@override
String toString() => r'tabFindResultStateProvider';
}
/// Translation progress/result per tab.
@ProviderFor(TabTranslationStates)
final tabTranslationStatesProvider = TabTranslationStatesProvider._();
/// Translation progress/result per tab.
final class TabTranslationStatesProvider
extends
$NotifierProvider<TabTranslationStates, Map<String, TranslationState>> {
/// Translation progress/result per tab.
TabTranslationStatesProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'tabTranslationStatesProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabTranslationStatesHash();
@$internal
@override
TabTranslationStates create() => TabTranslationStates();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Map<String, TranslationState> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Map<String, TranslationState>>(
value,
),
);
}
}
String _$tabTranslationStatesHash() =>
r'f04004026dc5fd638319528a55e2f10e0f64c9b3';
/// Translation progress/result per tab.
abstract class _$TabTranslationStates
extends $Notifier<Map<String, TranslationState>> {
Map<String, TranslationState> build();
@$mustCallSuper
@override
WhenComplete runBuild() {
final ref =
this.ref
as $Ref<
Map<String, TranslationState>,
Map<String, TranslationState>
>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
Map<String, TranslationState>,
Map<String, TranslationState>
>,
Map<String, TranslationState>,
Object?,
Object?
>;
return element.handleCreate(ref, build);
}
}
@ProviderFor(tabTranslationState)
final tabTranslationStateProvider = TabTranslationStateFamily._();
final class TabTranslationStateProvider
extends
$FunctionalProvider<
TranslationState,
TranslationState,
TranslationState
>
with $Provider<TranslationState> {
TabTranslationStateProvider._({
required TabTranslationStateFamily super.from,
required String? super.argument,
}) : super(
retry: null,
name: r'tabTranslationStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabTranslationStateHash();
@override
String toString() {
return r'tabTranslationStateProvider'
''
'($argument)';
}
@$internal
@override
$ProviderElement<TranslationState> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
TranslationState create(Ref ref) {
final argument = this.argument as String?;
return tabTranslationState(ref, argument);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(TranslationState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<TranslationState>(value),
);
}
@override
bool operator ==(Object other) {
return other is TabTranslationStateProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$tabTranslationStateHash() =>
r'886365e36167542b2e4dbdbeb97c942a414a752e';
final class TabTranslationStateFamily extends $Family
with $FunctionalFamilyOverride<TranslationState, String?> {
TabTranslationStateFamily._()
: super(
retry: null,
name: r'tabTranslationStateProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
TabTranslationStateProvider call(String? tabId) =>
TabTranslationStateProvider._(argument: tabId, from: this);
@override
String toString() => r'tabTranslationStateProvider';
}
@@ -19,6 +19,7 @@
*/
import 'dart:async';
import 'package:fast_equatable/fast_equatable.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:nullability/nullability.dart';
import 'package:riverpod/riverpod.dart';
@@ -34,6 +35,7 @@ import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
import 'package:weblibre/features/geckoview/domain/entities/states/translation.dart';
import 'package:weblibre/features/geckoview/domain/providers.dart';
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_detail_state.dart';
import 'package:weblibre/features/geckoview/features/find_in_page/domain/repositories/find_in_page.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/entities/isolation_context.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
@@ -46,8 +48,33 @@ import 'package:weblibre/features/user/domain/repositories/proxy_routing_setting
part 'tab_state.g.dart';
/// Decode width for tab thumbnails. The screenshots arrive at full device
/// resolution but are only ever shown in tab-tray previews a few hundred
/// logical pixels wide, so decoding them at native size burns main-isolate
/// time and GPU memory for detail nobody sees. Only the width is constrained
/// so the decoder keeps the aspect ratio; upscaling is disabled so smaller
/// sources are left alone.
const thumbnailDecodeWidth = 720;
@Riverpod(keepAlive: true)
class TabStates extends _$TabStates {
/// Replaces the entry for [tabId] — but only when [next] actually differs.
///
/// Every write here allocates a new map, and `Map` compares by identity, so
/// an unconditional assignment notifies *all* whole-map watchers (the quick
/// tab switcher, the grouped tab list, the FIFO list) even when nothing
/// changed. Gecko re-emits the full content state on every progress tick, so
/// without this guard a single page load pushed dozens of no-op rebuilds
/// through the entire browser chrome. [TabState] is `FastEquatable`, so the
/// comparison is a cached-hash check.
void _put(String tabId, TabState next) {
if (state[tabId] == next) {
return;
}
state = {...state}..[tabId] = next;
}
Future<void> _onTabContentStateChange(TabContentState contentState) async {
final current = await patchedState(contentState.id);
@@ -82,14 +109,19 @@ class TabStates extends _$TabStates {
contextId: contentState.contextId,
url: url,
title: resolvedTitle,
progress: contentState.progress,
tabMode: inferredTabMode,
isFullScreen: contentState.isFullScreen,
isLoading: contentState.isLoading,
showToolbarAsExpanded: contentState.showToolbarAsExpanded,
);
state = {...state}..[contentState.id] = newState;
_put(contentState.id, newState);
// Progress lives outside [TabState] so a load tick doesn't invalidate the
// whole map (and with it every chip in the quick tab switcher).
ref
.read(tabProgressStatesProvider.notifier)
.update(contentState.id, contentState.progress);
// Only reconcile DB hierarchy when the engine parent link actually changes.
// Content-state events also fire on every progress/title tick, and seeding
@@ -106,7 +138,7 @@ class TabStates extends _$TabStates {
);
}
if (newState.isFinishedLoading) {
if (!contentState.isLoading && contentState.progress == 100) {
ref
.read(geckoInferenceRepositoryProvider.notifier)
.markInitialLoadComplete();
@@ -138,97 +170,111 @@ class TabStates extends _$TabStates {
final IconChangeEvent(:tabId, :bytes) = event;
final image = await bytes.mapNotNull((bytes) => tryDecodeImage(bytes));
final current = state[tabId] ?? TabState.$default(tabId);
state = {...state}..[tabId] = current.copyWith.icon(image);
if (!ref.mounted) {
return;
}
final current = state[tabId] ?? TabState.$default(tabId);
_put(tabId, current.copyWith.icon(image));
}
Future<void> _onThumbnailChange(ThumbnailEvent event) async {
final ThumbnailEvent(:tabId, :bytes) = event;
final image = await bytes.mapNotNull((bytes) => tryDecodeImage(bytes));
final current = state[tabId] ?? TabState.$default(tabId);
final image = await bytes.mapNotNull(
(bytes) => tryDecodeImage(
bytes,
targetWidth: thumbnailDecodeWidth,
allowUpscaling: false,
),
);
state = {...state}..[tabId] = current.copyWith.thumbnail(image);
if (!ref.mounted) {
return;
}
ref.read(tabThumbnailsProvider.notifier).update(tabId, image);
}
void _onSecurityInfoStateChange(SecurityInfoEvent event) {
final SecurityInfoEvent(:tabId, :securityInfo) = event;
final current = state[tabId] ?? TabState.$default(tabId);
state = {...state}
..[tabId] = current.copyWith.securityInfoState(
_put(
tabId,
current.copyWith.securityInfoState(
SecurityState(
secure: securityInfo.secure,
host: securityInfo.host,
issuer: securityInfo.issuer,
),
);
),
);
}
void _onHistoryStateChange(HistoryEvent event) {
final HistoryEvent(:tabId, :history) = event;
final current = state[tabId] ?? TabState.$default(tabId);
state = {...state}
..[tabId] = current.copyWith.historyState(
HistoryState(
items: history.items.nonNulls
.map(
(item) =>
HistoryItem(url: Uri.parse(item.url), title: item.title),
)
.toList(),
currentIndex: history.currentIndex,
canGoBack: history.canGoBack,
canGoForward: history.canGoForward,
),
);
ref
.read(tabHistoryStatesProvider.notifier)
.update(
tabId,
HistoryState(
items: history.items.nonNulls
.map(
(item) =>
HistoryItem(url: Uri.parse(item.url), title: item.title),
)
.toList(),
currentIndex: history.currentIndex,
canGoBack: history.canGoBack,
canGoForward: history.canGoForward,
),
);
}
void _onReaderableStateChange(ReaderableEvent event) {
final ReaderableEvent(:tabId, :readerable) = event;
final current = state[tabId] ?? TabState.$default(tabId);
state = {...state}
..[tabId] = current.copyWith.readerableState(
_put(
tabId,
current.copyWith.readerableState(
ReaderableState(
readerable: readerable.readerable,
active: readerable.active,
),
);
),
);
}
void _onTabTranslationStateChange(TabTranslationEvent event) {
final TabTranslationEvent(:tabId, :state) = event;
final current = this.state[tabId] ?? TabState.$default(tabId);
this.state = {...this.state}
..[tabId] = current.copyWith.translationState(
TranslationState.fromData(state),
);
ref
.read(tabTranslationStatesProvider.notifier)
.update(tabId, TranslationState.fromData(state));
}
void _onFindResultsChange(FindResultsEvent event) {
final FindResultsEvent(:tabId, :results) = event;
final current = state[tabId] ?? TabState.$default(tabId);
final findResults = ref.read(tabFindResultStatesProvider.notifier);
if (results.isNotEmpty) {
final result = results.last;
state = {...state}
..[tabId] = current.copyWith.findResultState(
FindResultState(
lastSearchText: ref.read(findInPageRepositoryProvider(tabId)),
activeMatchOrdinal: result.activeMatchOrdinal,
numberOfMatches: result.numberOfMatches,
isDoneCounting: result.isDoneCounting,
),
);
} else if (current.findResultState.hasMatches) {
state = {
...state,
}..[tabId] = current.copyWith.findResultState(FindResultState.$default());
findResults.update(
tabId,
FindResultState(
lastSearchText: ref.read(findInPageRepositoryProvider(tabId)),
activeMatchOrdinal: result.activeMatchOrdinal,
numberOfMatches: result.numberOfMatches,
isDoneCounting: result.isDoneCounting,
),
);
} else if (findResults.resultFor(tabId).hasMatches) {
findResults.update(tabId, FindResultState.$default());
}
}
@@ -309,8 +355,15 @@ class TabStates extends _$TabStates {
);
},
),
// Gecko emits match counters at ~40/s while a find is running. 25ms let
// essentially every one of those through; 100ms is still well under the
// threshold where the counter feels laggy. Debounced *per tab* so a
// background tab still counting can't starve the foreground one.
eventService.findResultsEvent
.debounceTime(const Duration(milliseconds: 25))
.groupBy((event) => event.tabId)
.flatMap(
(group) => group.debounceTime(const Duration(milliseconds: 100)),
)
.listen(
(event) {
_onFindResultsChange(event);
@@ -381,6 +434,42 @@ TabState? tabState(Ref ref, String? tabId) {
return ref.watch(tabStatesProvider.select((tabs) => tabs[tabId]));
}
/// The only fields of [TabState] that tab filtering/sorting depends on.
class TabSortKeys with FastEquatable {
final TabMode tabMode;
final String titleOrAuthority;
final String url;
TabSortKeys({
required this.tabMode,
required this.titleOrAuthority,
required this.url,
});
@override
List<Object?> get hashParameters => [tabMode, titleOrAuthority, url];
}
/// Projection of [tabStatesProvider] for consumers that order or filter tabs
/// but render nothing from the state itself. Watching this instead of the full
/// map keeps the (expensive) grouping/sorting passes off the path of every
/// icon, security-info and readerable event.
@Riverpod(keepAlive: true)
EquatableValue<Map<String, TabSortKeys>> tabSortKeys(Ref ref) {
return ref.watch(
tabStatesProvider.select(
(states) => EquatableValue({
for (final MapEntry(:key, :value) in states.entries)
key: TabSortKeys(
tabMode: value.tabMode,
titleOrAuthority: value.titleOrAuthority,
url: value.url.toString(),
),
}),
),
);
}
@Riverpod()
Future<TabState> tabStateWithFallback(Ref ref, String tabId) async {
final state = ref.watch(tabStateProvider(tabId));
@@ -41,7 +41,7 @@ final class TabStatesProvider
}
}
String _$tabStatesHash() => r'04b7f1ea704575e368a0f1186fc1c58f317620b1';
String _$tabStatesHash() => r'1ae4e941890116eabf1803809014b3e575c33e73';
abstract class _$TabStates extends $Notifier<Map<String, TabState>> {
Map<String, TabState> build();
@@ -138,6 +138,68 @@ final class TabStateFamily extends $Family
String toString() => r'tabStateProvider';
}
/// Projection of [tabStatesProvider] for consumers that order or filter tabs
/// but render nothing from the state itself. Watching this instead of the full
/// map keeps the (expensive) grouping/sorting passes off the path of every
/// icon, security-info and readerable event.
@ProviderFor(tabSortKeys)
final tabSortKeysProvider = TabSortKeysProvider._();
/// Projection of [tabStatesProvider] for consumers that order or filter tabs
/// but render nothing from the state itself. Watching this instead of the full
/// map keeps the (expensive) grouping/sorting passes off the path of every
/// icon, security-info and readerable event.
final class TabSortKeysProvider
extends
$FunctionalProvider<
EquatableValue<Map<String, TabSortKeys>>,
EquatableValue<Map<String, TabSortKeys>>,
EquatableValue<Map<String, TabSortKeys>>
>
with $Provider<EquatableValue<Map<String, TabSortKeys>>> {
/// Projection of [tabStatesProvider] for consumers that order or filter tabs
/// but render nothing from the state itself. Watching this instead of the full
/// map keeps the (expensive) grouping/sorting passes off the path of every
/// icon, security-info and readerable event.
TabSortKeysProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'tabSortKeysProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabSortKeysHash();
@$internal
@override
$ProviderElement<EquatableValue<Map<String, TabSortKeys>>> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
EquatableValue<Map<String, TabSortKeys>> create(Ref ref) {
return tabSortKeys(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(EquatableValue<Map<String, TabSortKeys>> value) {
return $ProviderOverride(
origin: this,
providerOverride:
$SyncValueProvider<EquatableValue<Map<String, TabSortKeys>>>(value),
);
}
}
String _$tabSortKeysHash() => r'7bf660099d006b6df9846594d2a5d78a32fa3bdb';
@ProviderFor(tabStateWithFallback)
final tabStateWithFallbackProvider = TabStateWithFallbackFamily._();