prepare for multiple apps

This commit is contained in:
Fabian Freund
2026-04-06 12:23:11 +02:00
parent bd1600e8dc
commit 5afc323f04
904 changed files with 29 additions and 29 deletions
@@ -0,0 +1,50 @@
/*
* 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:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart';
part 'bottom_sheet.g.dart';
@Riverpod(keepAlive: true)
class BottomSheetController extends _$BottomSheetController {
@override
Sheet? build() {
return null;
}
///We depend on a listener that updates/syncs UI to open the sheet
// ignore: use_setters_to_change_properties api decision
void show(Sheet sheet) {
state = sheet;
}
///We depend on a listener that updates/syncs UI to close the sheet
void requestDismiss() {
state = null;
}
///This is called by UI when the sheet gets closed
void closed(Sheet sheet) {
if (sheet == stateOrNull) {
state = null;
}
}
}
@@ -0,0 +1,63 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'bottom_sheet.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(BottomSheetController)
final bottomSheetControllerProvider = BottomSheetControllerProvider._();
final class BottomSheetControllerProvider
extends $NotifierProvider<BottomSheetController, Sheet?> {
BottomSheetControllerProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'bottomSheetControllerProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$bottomSheetControllerHash();
@$internal
@override
BottomSheetController create() => BottomSheetController();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Sheet? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Sheet?>(value),
);
}
}
String _$bottomSheetControllerHash() =>
r'54112ccef72ad7777c746b9f2dc02da1e383c127';
abstract class _$BottomSheetController extends $Notifier<Sheet?> {
Sheet? build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<Sheet?, Sheet?>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<Sheet?, Sheet?>,
Sheet?,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@@ -0,0 +1,40 @@
/*
* 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/widgets.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'overlay.g.dart';
@Riverpod(keepAlive: true)
class OverlayController extends _$OverlayController {
@override
WidgetBuilder? build() {
return null;
}
// ignore: use_setters_to_change_properties api decision
void show(WidgetBuilder builder) {
state = builder;
}
void dismiss() {
state = null;
}
}
@@ -0,0 +1,62 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'overlay.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(OverlayController)
final overlayControllerProvider = OverlayControllerProvider._();
final class OverlayControllerProvider
extends $NotifierProvider<OverlayController, WidgetBuilder?> {
OverlayControllerProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'overlayControllerProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$overlayControllerHash();
@$internal
@override
OverlayController create() => OverlayController();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(WidgetBuilder? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<WidgetBuilder?>(value),
);
}
}
String _$overlayControllerHash() => r'd0cd7c4cf867397f10f801ec2f7733be56520fb3';
abstract class _$OverlayController extends $Notifier<WidgetBuilder?> {
WidgetBuilder? build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<WidgetBuilder?, WidgetBuilder?>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<WidgetBuilder?, WidgetBuilder?>,
WidgetBuilder?,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@@ -0,0 +1,56 @@
/*
* 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:typed_data';
import 'dart:ui';
import 'package:fast_equatable/fast_equatable.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:weblibre/domain/entities/equatable_image.dart';
import 'package:weblibre/features/geckoview/utils/image_helper.dart';
class BrowserIcon with FastEquatable {
final EquatableImage image;
final Color? dominantColor;
final IconSource source;
static Future<BrowserIcon> fromBytes(
Uint8List bytes, {
required Color? dominantColor,
required IconSource source,
}) async {
final image = await tryDecodeImage(bytes);
return BrowserIcon(
image: image!,
dominantColor: dominantColor,
source: source,
);
}
BrowserIcon({
required this.image,
required this.dominantColor,
required this.source,
});
@override
List<Object?> get hashParameters => [image, dominantColor, source];
}
@@ -0,0 +1,52 @@
/*
* 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:fast_equatable/fast_equatable.dart';
class FindResultState with FastEquatable {
final String? lastSearchText;
final int activeMatchOrdinal;
final int numberOfMatches;
final bool isDoneCounting;
bool get hasMatches => numberOfMatches > 0;
FindResultState({
required this.lastSearchText,
required this.activeMatchOrdinal,
required this.numberOfMatches,
required this.isDoneCounting,
});
factory FindResultState.$default() => FindResultState(
lastSearchText: null,
activeMatchOrdinal: -1,
numberOfMatches: 0,
isDoneCounting: false,
);
@override
List<Object?> get hashParameters => [
lastSearchText,
activeMatchOrdinal,
numberOfMatches,
isDoneCounting,
];
}
@@ -0,0 +1,60 @@
/*
* 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:fast_equatable/fast_equatable.dart';
class HistoryItem with FastEquatable {
final Uri url;
final String title;
HistoryItem({required this.url, required this.title});
@override
List<Object?> get hashParameters => [url, title];
}
class HistoryState with FastEquatable {
final List<HistoryItem> items;
final int currentIndex;
final bool canGoBack;
final bool canGoForward;
HistoryState({
required this.items,
required this.currentIndex,
required this.canGoBack,
required this.canGoForward,
});
factory HistoryState.$default() => HistoryState(
items: const [],
currentIndex: 0,
canGoBack: false,
canGoForward: false,
);
@override
List<Object?> get hashParameters => [
items,
currentIndex,
canGoBack,
canGoForward,
];
}
@@ -0,0 +1,34 @@
/*
* 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:fast_equatable/fast_equatable.dart';
class ReaderableState with FastEquatable {
final bool readerable;
final bool active;
ReaderableState({required this.readerable, required this.active});
factory ReaderableState.$default() =>
ReaderableState(readerable: false, active: false);
@override
List<Object?> get hashParameters => [readerable, active];
}
@@ -0,0 +1,38 @@
/*
* 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:fast_equatable/fast_equatable.dart';
class SecurityState with FastEquatable {
final bool secure;
final String host;
final String issuer;
SecurityState({
required this.secure,
required this.host,
required this.issuer,
});
factory SecurityState.$default() =>
SecurityState(secure: false, host: "", issuer: "");
@override
List<Object?> get hashParameters => [secure, host, issuer];
}
@@ -0,0 +1,139 @@
/*
* 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:copy_with_extension/copy_with_extension.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:nullability/nullability.dart';
import 'package:weblibre/data/models/web_page_info.dart';
import 'package:weblibre/domain/entities/equatable_image.dart';
import 'package:weblibre/features/geckoview/domain/entities/browser_icon.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/readerable.dart';
import 'package:weblibre/features/geckoview/domain/entities/states/security.dart';
import 'package:weblibre/features/geckoview/domain/entities/states/translation.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
part 'tab.g.dart';
@CopyWith()
class TabState extends WebPageInfo {
static final defaultUrl = Uri.parse('about:blank');
@CopyWithField(immutable: true)
final String id;
final String? parentId;
final String? contextId;
@override
String get title => super.title!;
String get titleOrAuthority => (title.isNotEmpty) ? title : url.authority;
final EquatableImage? icon;
@override
BrowserIcon? get favicon => icon.mapNotNull(
(icon) => BrowserIcon(
image: icon,
dominantColor: null,
source: IconSource.memory,
),
);
final EquatableImage? thumbnail;
final int progress;
final TabMode tabMode;
String? get isolationContextId => tabMode.isolationContextId;
final bool isFullScreen;
final bool isLoading;
final bool showToolbarAsExpanded;
bool get isFinishedLoading => !isLoading && progress == 100;
final SecurityState securityInfoState;
final HistoryState historyState;
final ReaderableState readerableState;
final FindResultState findResultState;
final TranslationState translationState;
TabState({
required this.id,
required this.parentId,
required this.contextId,
required super.url,
required String title,
required this.icon,
required this.thumbnail,
required this.progress,
this.tabMode = TabMode.regular,
required this.isFullScreen,
required this.isLoading,
required this.showToolbarAsExpanded,
required this.securityInfoState,
required this.historyState,
required this.readerableState,
required this.findResultState,
required this.translationState,
}) : super(title: title.trim());
factory TabState.$default(String tabId) => TabState(
id: tabId,
parentId: null,
contextId: null,
url: defaultUrl,
title: "",
icon: null,
thumbnail: null,
progress: 0,
isFullScreen: false,
isLoading: false,
showToolbarAsExpanded: false,
securityInfoState: SecurityState.$default(),
historyState: HistoryState.$default(),
readerableState: ReaderableState.$default(),
findResultState: FindResultState.$default(),
translationState: TranslationState.$default(),
);
@override
List<Object?> get hashParameters => [
...super.hashParameters,
id,
parentId,
contextId,
icon,
thumbnail,
progress,
tabMode,
isFullScreen,
isLoading,
showToolbarAsExpanded,
securityInfoState,
historyState,
readerableState,
findResultState,
translationState,
];
}
@@ -0,0 +1,243 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'tab.dart';
// **************************************************************************
// CopyWithGenerator
// **************************************************************************
abstract class _$TabStateCWProxy {
TabState parentId(String? parentId);
TabState contextId(String? contextId);
TabState url(Uri url);
TabState title(String title);
TabState icon(EquatableImage? icon);
TabState thumbnail(EquatableImage? thumbnail);
TabState progress(int progress);
TabState tabMode(TabMode tabMode);
TabState isFullScreen(bool isFullScreen);
TabState isLoading(bool isLoading);
TabState showToolbarAsExpanded(bool showToolbarAsExpanded);
TabState securityInfoState(SecurityState securityInfoState);
TabState historyState(HistoryState historyState);
TabState readerableState(ReaderableState readerableState);
TabState findResultState(FindResultState findResultState);
TabState translationState(TranslationState translationState);
/// Creates a new instance with the provided field values.
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `TabState(...).copyWith.fieldName(value)`.
///
/// Example:
/// ```dart
/// TabState(...).copyWith(id: 12, name: "My name")
/// ```
TabState call({
String? parentId,
String? contextId,
Uri url,
String title,
EquatableImage? icon,
EquatableImage? thumbnail,
int progress,
TabMode tabMode,
bool isFullScreen,
bool isLoading,
bool showToolbarAsExpanded,
SecurityState securityInfoState,
HistoryState historyState,
ReaderableState readerableState,
FindResultState findResultState,
TranslationState translationState,
});
}
/// Callable proxy for `copyWith` functionality.
/// Use as `instanceOfTabState.copyWith(...)` or call `instanceOfTabState.copyWith.fieldName(value)` for a single field.
class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
const _$TabStateCWProxyImpl(this._value);
final TabState _value;
@override
TabState parentId(String? parentId) => call(parentId: parentId);
@override
TabState contextId(String? contextId) => call(contextId: contextId);
@override
TabState url(Uri url) => call(url: url);
@override
TabState title(String title) => call(title: title);
@override
TabState icon(EquatableImage? icon) => call(icon: icon);
@override
TabState thumbnail(EquatableImage? thumbnail) => call(thumbnail: thumbnail);
@override
TabState progress(int progress) => call(progress: progress);
@override
TabState tabMode(TabMode tabMode) => call(tabMode: tabMode);
@override
TabState isFullScreen(bool isFullScreen) => call(isFullScreen: isFullScreen);
@override
TabState isLoading(bool isLoading) => call(isLoading: isLoading);
@override
TabState showToolbarAsExpanded(bool showToolbarAsExpanded) =>
call(showToolbarAsExpanded: showToolbarAsExpanded);
@override
TabState securityInfoState(SecurityState securityInfoState) =>
call(securityInfoState: securityInfoState);
@override
TabState historyState(HistoryState historyState) =>
call(historyState: historyState);
@override
TabState readerableState(ReaderableState readerableState) =>
call(readerableState: readerableState);
@override
TabState findResultState(FindResultState findResultState) =>
call(findResultState: findResultState);
@override
TabState translationState(TranslationState translationState) =>
call(translationState: translationState);
@override
/// Creates a new instance with the provided field values.
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `TabState(...).copyWith.fieldName(value)`.
///
/// Example:
/// ```dart
/// TabState(...).copyWith(id: 12, name: "My name")
/// ```
TabState call({
Object? parentId = const $CopyWithPlaceholder(),
Object? contextId = const $CopyWithPlaceholder(),
Object? url = const $CopyWithPlaceholder(),
Object? title = const $CopyWithPlaceholder(),
Object? icon = const $CopyWithPlaceholder(),
Object? thumbnail = const $CopyWithPlaceholder(),
Object? progress = const $CopyWithPlaceholder(),
Object? tabMode = const $CopyWithPlaceholder(),
Object? isFullScreen = const $CopyWithPlaceholder(),
Object? isLoading = const $CopyWithPlaceholder(),
Object? showToolbarAsExpanded = const $CopyWithPlaceholder(),
Object? securityInfoState = const $CopyWithPlaceholder(),
Object? historyState = const $CopyWithPlaceholder(),
Object? readerableState = const $CopyWithPlaceholder(),
Object? findResultState = const $CopyWithPlaceholder(),
Object? translationState = const $CopyWithPlaceholder(),
}) {
return TabState(
id: _value.id,
parentId: parentId == const $CopyWithPlaceholder()
? _value.parentId
// ignore: cast_nullable_to_non_nullable
: parentId as String?,
contextId: contextId == const $CopyWithPlaceholder()
? _value.contextId
// ignore: cast_nullable_to_non_nullable
: contextId as String?,
url: url == const $CopyWithPlaceholder() || url == null
? _value.url
// ignore: cast_nullable_to_non_nullable
: url as Uri,
title: title == const $CopyWithPlaceholder() || title == null
? _value.title
// ignore: cast_nullable_to_non_nullable
: title as String,
icon: icon == const $CopyWithPlaceholder()
? _value.icon
// ignore: cast_nullable_to_non_nullable
: icon as EquatableImage?,
thumbnail: thumbnail == const $CopyWithPlaceholder()
? _value.thumbnail
// ignore: cast_nullable_to_non_nullable
: thumbnail as EquatableImage?,
progress: progress == const $CopyWithPlaceholder() || progress == null
? _value.progress
// ignore: cast_nullable_to_non_nullable
: progress as int,
tabMode: tabMode == const $CopyWithPlaceholder() || tabMode == null
? _value.tabMode
// ignore: cast_nullable_to_non_nullable
: tabMode as TabMode,
isFullScreen:
isFullScreen == const $CopyWithPlaceholder() || isFullScreen == null
? _value.isFullScreen
// ignore: cast_nullable_to_non_nullable
: isFullScreen as bool,
isLoading: isLoading == const $CopyWithPlaceholder() || isLoading == null
? _value.isLoading
// ignore: cast_nullable_to_non_nullable
: isLoading as bool,
showToolbarAsExpanded:
showToolbarAsExpanded == const $CopyWithPlaceholder() ||
showToolbarAsExpanded == null
? _value.showToolbarAsExpanded
// ignore: cast_nullable_to_non_nullable
: showToolbarAsExpanded as bool,
securityInfoState:
securityInfoState == const $CopyWithPlaceholder() ||
securityInfoState == null
? _value.securityInfoState
// ignore: cast_nullable_to_non_nullable
: securityInfoState as SecurityState,
historyState:
historyState == const $CopyWithPlaceholder() || historyState == null
? _value.historyState
// ignore: cast_nullable_to_non_nullable
: historyState as HistoryState,
readerableState:
readerableState == const $CopyWithPlaceholder() ||
readerableState == null
? _value.readerableState
// ignore: cast_nullable_to_non_nullable
: readerableState as ReaderableState,
findResultState:
findResultState == const $CopyWithPlaceholder() ||
findResultState == null
? _value.findResultState
// ignore: cast_nullable_to_non_nullable
: findResultState as FindResultState,
translationState:
translationState == const $CopyWithPlaceholder() ||
translationState == null
? _value.translationState
// ignore: cast_nullable_to_non_nullable
: translationState as TranslationState,
);
}
}
extension $TabStateCopyWith on TabState {
/// Returns a callable class used to build a new instance with modified fields.
/// Example: `instanceOfTabState.copyWith(...)` or `instanceOfTabState.copyWith.fieldName(...)`.
// ignore: library_private_types_in_public_api
_$TabStateCWProxy get copyWith => _$TabStateCWProxyImpl(this);
}
@@ -0,0 +1,84 @@
/*
* 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:fast_equatable/fast_equatable.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
class TranslationState with FastEquatable {
final bool isTranslated;
final bool isTranslateProcessing;
final bool isOfferTranslate;
final bool isExpectedTranslate;
final String? detectedLanguageCode;
final String? userPreferredLanguageCode;
final String? requestedFromLanguage;
final String? requestedToLanguage;
final String? translationErrorName;
final bool? displayError;
TranslationState({
required this.isTranslated,
required this.isTranslateProcessing,
required this.isOfferTranslate,
required this.isExpectedTranslate,
this.detectedLanguageCode,
this.userPreferredLanguageCode,
this.requestedFromLanguage,
this.requestedToLanguage,
this.translationErrorName,
this.displayError,
});
factory TranslationState.$default() => TranslationState(
isTranslated: false,
isTranslateProcessing: false,
isOfferTranslate: false,
isExpectedTranslate: false,
);
factory TranslationState.fromData(TabTranslationStateData data) =>
TranslationState(
isTranslated: data.isTranslated,
isTranslateProcessing: data.isTranslateProcessing,
isOfferTranslate: data.isOfferTranslate,
isExpectedTranslate: data.isExpectedTranslate,
detectedLanguageCode: data.detectedLanguageCode,
userPreferredLanguageCode: data.userPreferredLanguageCode,
requestedFromLanguage: data.requestedFromLanguage,
requestedToLanguage: data.requestedToLanguage,
translationErrorName: data.translationErrorName,
displayError: data.displayError,
);
bool get hasError => translationErrorName != null && (displayError ?? true);
@override
List<Object?> get hashParameters => [
isTranslated,
isTranslateProcessing,
isOfferTranslate,
isExpectedTranslate,
detectedLanguageCode,
userPreferredLanguageCode,
requestedFromLanguage,
requestedToLanguage,
translationErrorName,
displayError,
];
}
@@ -0,0 +1,60 @@
/*
* 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:ui';
import 'package:copy_with_extension/copy_with_extension.dart';
import 'package:fast_equatable/fast_equatable.dart';
import 'package:weblibre/domain/entities/equatable_image.dart';
part 'web_extension.g.dart';
@CopyWith()
class WebExtensionState with FastEquatable {
String extensionId;
String? title;
bool enabled;
String? badgeText;
Color? badgeTextColor;
Color? badgeBackgroundColor;
final EquatableImage? icon;
WebExtensionState({
required this.extensionId,
required this.enabled,
this.title,
this.badgeText,
this.badgeTextColor,
this.badgeBackgroundColor,
this.icon,
});
@override
List<Object?> get hashParameters => [
extensionId,
title,
enabled,
badgeText,
badgeTextColor,
badgeBackgroundColor,
icon,
];
}
@@ -0,0 +1,130 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'web_extension.dart';
// **************************************************************************
// CopyWithGenerator
// **************************************************************************
abstract class _$WebExtensionStateCWProxy {
WebExtensionState extensionId(String extensionId);
WebExtensionState enabled(bool enabled);
WebExtensionState title(String? title);
WebExtensionState badgeText(String? badgeText);
WebExtensionState badgeTextColor(Color? badgeTextColor);
WebExtensionState badgeBackgroundColor(Color? badgeBackgroundColor);
WebExtensionState icon(EquatableImage? icon);
/// Creates a new instance with the provided field values.
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `WebExtensionState(...).copyWith.fieldName(value)`.
///
/// Example:
/// ```dart
/// WebExtensionState(...).copyWith(id: 12, name: "My name")
/// ```
WebExtensionState call({
String extensionId,
bool enabled,
String? title,
String? badgeText,
Color? badgeTextColor,
Color? badgeBackgroundColor,
EquatableImage? icon,
});
}
/// Callable proxy for `copyWith` functionality.
/// Use as `instanceOfWebExtensionState.copyWith(...)` or call `instanceOfWebExtensionState.copyWith.fieldName(value)` for a single field.
class _$WebExtensionStateCWProxyImpl implements _$WebExtensionStateCWProxy {
const _$WebExtensionStateCWProxyImpl(this._value);
final WebExtensionState _value;
@override
WebExtensionState extensionId(String extensionId) =>
call(extensionId: extensionId);
@override
WebExtensionState enabled(bool enabled) => call(enabled: enabled);
@override
WebExtensionState title(String? title) => call(title: title);
@override
WebExtensionState badgeText(String? badgeText) => call(badgeText: badgeText);
@override
WebExtensionState badgeTextColor(Color? badgeTextColor) =>
call(badgeTextColor: badgeTextColor);
@override
WebExtensionState badgeBackgroundColor(Color? badgeBackgroundColor) =>
call(badgeBackgroundColor: badgeBackgroundColor);
@override
WebExtensionState icon(EquatableImage? icon) => call(icon: icon);
@override
/// Creates a new instance with the provided field values.
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `WebExtensionState(...).copyWith.fieldName(value)`.
///
/// Example:
/// ```dart
/// WebExtensionState(...).copyWith(id: 12, name: "My name")
/// ```
WebExtensionState call({
Object? extensionId = const $CopyWithPlaceholder(),
Object? enabled = const $CopyWithPlaceholder(),
Object? title = const $CopyWithPlaceholder(),
Object? badgeText = const $CopyWithPlaceholder(),
Object? badgeTextColor = const $CopyWithPlaceholder(),
Object? badgeBackgroundColor = const $CopyWithPlaceholder(),
Object? icon = const $CopyWithPlaceholder(),
}) {
return WebExtensionState(
extensionId:
extensionId == const $CopyWithPlaceholder() || extensionId == null
? _value.extensionId
// ignore: cast_nullable_to_non_nullable
: extensionId as String,
enabled: enabled == const $CopyWithPlaceholder() || enabled == null
? _value.enabled
// ignore: cast_nullable_to_non_nullable
: enabled as bool,
title: title == const $CopyWithPlaceholder()
? _value.title
// ignore: cast_nullable_to_non_nullable
: title as String?,
badgeText: badgeText == const $CopyWithPlaceholder()
? _value.badgeText
// ignore: cast_nullable_to_non_nullable
: badgeText as String?,
badgeTextColor: badgeTextColor == const $CopyWithPlaceholder()
? _value.badgeTextColor
// ignore: cast_nullable_to_non_nullable
: badgeTextColor as Color?,
badgeBackgroundColor: badgeBackgroundColor == const $CopyWithPlaceholder()
? _value.badgeBackgroundColor
// ignore: cast_nullable_to_non_nullable
: badgeBackgroundColor as Color?,
icon: icon == const $CopyWithPlaceholder()
? _value.icon
// ignore: cast_nullable_to_non_nullable
: icon as EquatableImage?,
);
}
}
extension $WebExtensionStateCopyWith on WebExtensionState {
/// Returns a callable class used to build a new instance with modified fields.
/// Example: `instanceOfWebExtensionState.copyWith(...)` or `instanceOfWebExtensionState.copyWith.fieldName(...)`.
// ignore: library_private_types_in_public_api
_$WebExtensionStateCWProxy get copyWith =>
_$WebExtensionStateCWProxyImpl(this);
}
@@ -0,0 +1,48 @@
/*
* 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:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
sealed class TabContainerSelection {
const TabContainerSelection();
const factory TabContainerSelection.useSelected() =
UseSelectedContainerTabSelection;
const factory TabContainerSelection.unassigned() =
UnassignedContainerTabSelection;
const factory TabContainerSelection.specific(ContainerData container) =
SpecificContainerTabSelection;
}
final class UseSelectedContainerTabSelection extends TabContainerSelection {
const UseSelectedContainerTabSelection();
}
final class UnassignedContainerTabSelection extends TabContainerSelection {
const UnassignedContainerTabSelection();
}
final class SpecificContainerTabSelection extends TabContainerSelection {
final ContainerData container;
const SpecificContainerTabSelection(this.container);
}
@@ -0,0 +1,303 @@
/*
* 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:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:weblibre/core/logger.dart';
import 'package:weblibre/core/providers/router.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/bangs/domain/providers/bangs.dart';
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
import 'package:weblibre/features/geckoview/domain/repositories/tab.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/tab_mode.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
part 'providers.g.dart';
@Riverpod(keepAlive: true)
GeckoSelectionActionService selectionActionService(Ref ref) {
final service = GeckoSelectionActionService.setUp();
unawaited(
service.setActions([
NewTabAction((text) async {
if (ref.mounted) {
final router = await ref.read(routerProvider.future);
if (ref.mounted) {
final settings = ref.read(generalSettingsWithDefaultsProvider);
final selectedTabState = ref.read(
tabStatesProvider,
)[ref.read(selectedTabProvider)];
final selectedTabType = selectedTabState?.tabMode.toTabType();
final route = SearchRoute(
tabType:
selectedTabType ?? settings.effectiveDefaultCreateTabType,
searchText: text,
);
await router.push(route.location);
}
}
}),
DefaultSearchAction((text) async {
if (ref.mounted) {
final defaultSearchBang = await ref.read(
defaultSearchBangDataProvider.future,
);
if (ref.mounted && defaultSearchBang != null) {
final currentTab = ref.read(
tabStatesProvider,
)[ref.read(selectedTabProvider)];
final tabMode =
currentTab?.tabMode ??
TabMode.fromTabType(
ref
.read(generalSettingsWithDefaultsProvider)
.effectiveDefaultCreateTabType,
);
await ref
.read(tabRepositoryProvider.notifier)
.addTab(
url: defaultSearchBang.getTemplateUrl(text),
parentId: currentTab?.id,
tabMode: tabMode,
selectTab: true,
);
} else {
logger.e('No default search bang found');
}
}
}),
FindInPageAction((text) async {
if (ref.mounted) {
final tabId = ref.read(selectedTabProvider);
if (tabId != null) {
await ref
.read(findInPageRepositoryProvider(tabId).notifier)
.findAll(text);
}
}
}),
ShareAction((text) async {
await SharePlus.instance.share(ShareParams(text: text));
}),
CallAction((text) async {
final uri = Uri.tryParse('tel:${text.replaceAll(' ', '')}');
if (uri != null) {
final canLaunch = await canLaunchUrl(uri);
if (canLaunch) {
await launchUrl(uri);
}
}
}),
EmailAction((text) async {
final uri = Uri.tryParse('mailto:$text');
if (uri != null) {
final canLaunch = await canLaunchUrl(uri);
if (canLaunch) {
await launchUrl(uri);
}
}
}),
]),
);
return service;
}
@Riverpod(keepAlive: true)
GeckoEventService eventService(Ref ref) {
final service = GeckoEventService.setUp();
ref.onDispose(() async {
await service.dispose();
});
return service;
}
@Riverpod(keepAlive: true)
GeckoAddonService addonService(Ref ref) {
final service = GeckoAddonService.setUp();
ref.onDispose(() async {
await service.dispose();
});
return service;
}
@Riverpod(keepAlive: true)
GeckoTabContentService tabContentService(Ref ref) {
final service = GeckoTabContentService.setUp();
ref.onDispose(() async {
await service.dispose();
});
return service;
}
@Riverpod(keepAlive: true)
GeckoSuggestionsService engineSuggestionsService(Ref ref) {
final service = GeckoSuggestionsService.setUp();
ref.onDispose(() async {
await service.dispose();
});
return service;
}
@Riverpod(keepAlive: true)
GeckoViewportService viewportService(Ref ref) {
final service = GeckoViewportService();
service.setUp();
ref.onDispose(() async {
await service.dispose();
});
return service;
}
@Riverpod(keepAlive: true)
class EngineReadyState extends _$EngineReadyState {
@override
bool build() {
final eventService = ref.watch(eventServiceProvider);
final currentState =
eventService.engineReadyStateEvents.valueOrNull ?? false;
if (!currentState) {
unawaited(
eventService.engineReadyStateEvents
.firstWhere((value) => value == true)
.timeout(
const Duration(seconds: 3),
onTimeout: () {
logger.w('Waiting for engine ready state timed out');
return true;
},
)
.whenComplete(() {
if (ref.mounted) {
state = true;
}
}),
);
}
final sub = eventService.engineReadyStateEvents.listen((value) {
if (ref.mounted) {
state = value;
}
});
ref.onDispose(() async {
await sub.cancel();
});
return currentState;
}
}
/// Stream of ML model progress events
@Riverpod(keepAlive: true)
Stream<MlProgressData> mlProgressEvents(Ref ref) {
final service = ref.watch(eventServiceProvider);
return service.mlProgressEvents;
}
/// Translation engine state (browser-level: supported languages, engine availability)
@Riverpod(keepAlive: true)
class TranslationEngineState extends _$TranslationEngineState {
@override
TranslationEngineStateData? build() {
final eventService = ref.watch(eventServiceProvider);
final sub = eventService.translationEngineEvents.listen((value) {
if (ref.mounted) {
state = value;
}
});
ref.onDispose(() async {
await sub.cancel();
});
return eventService.translationEngineEvents.valueOrNull;
}
}
/// Tracks active ML model downloads
@Riverpod()
class MlDownloadState extends _$MlDownloadState {
Timer? _clearTimer;
@override
MlProgressData? build() {
ref.listen(mlProgressEventsProvider, (previous, next) {
next.whenData((progress) {
if (!ref.mounted) return;
if (progress.type == MlProgressType.downloading) {
if (progress.status == MlProgressStatus.done) {
// Keep showing for 2 seconds after completion
_clearTimer?.cancel();
_clearTimer = Timer(const Duration(seconds: 2), () {
if (ref.mounted && state != null && state!.id == progress.id) {
state = null;
}
});
} else {
state = progress;
}
}
});
});
ref.onDispose(() {
_clearTimer?.cancel();
_clearTimer = null;
});
return null;
}
void clear() {
_clearTimer?.cancel();
_clearTimer = null;
state = null;
}
}
@@ -0,0 +1,514 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(selectionActionService)
final selectionActionServiceProvider = SelectionActionServiceProvider._();
final class SelectionActionServiceProvider
extends
$FunctionalProvider<
GeckoSelectionActionService,
GeckoSelectionActionService,
GeckoSelectionActionService
>
with $Provider<GeckoSelectionActionService> {
SelectionActionServiceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'selectionActionServiceProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$selectionActionServiceHash();
@$internal
@override
$ProviderElement<GeckoSelectionActionService> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
GeckoSelectionActionService create(Ref ref) {
return selectionActionService(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(GeckoSelectionActionService value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<GeckoSelectionActionService>(value),
);
}
}
String _$selectionActionServiceHash() =>
r'5faf8c13c414406dfc2eb55fdd372677dbccdf8c';
@ProviderFor(eventService)
final eventServiceProvider = EventServiceProvider._();
final class EventServiceProvider
extends
$FunctionalProvider<
GeckoEventService,
GeckoEventService,
GeckoEventService
>
with $Provider<GeckoEventService> {
EventServiceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'eventServiceProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$eventServiceHash();
@$internal
@override
$ProviderElement<GeckoEventService> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
GeckoEventService create(Ref ref) {
return eventService(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(GeckoEventService value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<GeckoEventService>(value),
);
}
}
String _$eventServiceHash() => r'3a297348fadda05dc60433d7ce8f662b2ff62c26';
@ProviderFor(addonService)
final addonServiceProvider = AddonServiceProvider._();
final class AddonServiceProvider
extends
$FunctionalProvider<
GeckoAddonService,
GeckoAddonService,
GeckoAddonService
>
with $Provider<GeckoAddonService> {
AddonServiceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'addonServiceProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$addonServiceHash();
@$internal
@override
$ProviderElement<GeckoAddonService> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
GeckoAddonService create(Ref ref) {
return addonService(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(GeckoAddonService value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<GeckoAddonService>(value),
);
}
}
String _$addonServiceHash() => r'30fedb35c68943159246df79b5f1b62a25767fa0';
@ProviderFor(tabContentService)
final tabContentServiceProvider = TabContentServiceProvider._();
final class TabContentServiceProvider
extends
$FunctionalProvider<
GeckoTabContentService,
GeckoTabContentService,
GeckoTabContentService
>
with $Provider<GeckoTabContentService> {
TabContentServiceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'tabContentServiceProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabContentServiceHash();
@$internal
@override
$ProviderElement<GeckoTabContentService> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
GeckoTabContentService create(Ref ref) {
return tabContentService(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(GeckoTabContentService value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<GeckoTabContentService>(value),
);
}
}
String _$tabContentServiceHash() => r'12d8322c37ded4ad3344af327d884bbf7f089594';
@ProviderFor(engineSuggestionsService)
final engineSuggestionsServiceProvider = EngineSuggestionsServiceProvider._();
final class EngineSuggestionsServiceProvider
extends
$FunctionalProvider<
GeckoSuggestionsService,
GeckoSuggestionsService,
GeckoSuggestionsService
>
with $Provider<GeckoSuggestionsService> {
EngineSuggestionsServiceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'engineSuggestionsServiceProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$engineSuggestionsServiceHash();
@$internal
@override
$ProviderElement<GeckoSuggestionsService> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
GeckoSuggestionsService create(Ref ref) {
return engineSuggestionsService(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(GeckoSuggestionsService value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<GeckoSuggestionsService>(value),
);
}
}
String _$engineSuggestionsServiceHash() =>
r'1ec1192f0c5c86cecc7ad448ee2b039f7a48e32b';
@ProviderFor(viewportService)
final viewportServiceProvider = ViewportServiceProvider._();
final class ViewportServiceProvider
extends
$FunctionalProvider<
GeckoViewportService,
GeckoViewportService,
GeckoViewportService
>
with $Provider<GeckoViewportService> {
ViewportServiceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'viewportServiceProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$viewportServiceHash();
@$internal
@override
$ProviderElement<GeckoViewportService> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
GeckoViewportService create(Ref ref) {
return viewportService(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(GeckoViewportService value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<GeckoViewportService>(value),
);
}
}
String _$viewportServiceHash() => r'bab39db3180bb6a1cf8c055966b7f5910b41b424';
@ProviderFor(EngineReadyState)
final engineReadyStateProvider = EngineReadyStateProvider._();
final class EngineReadyStateProvider
extends $NotifierProvider<EngineReadyState, bool> {
EngineReadyStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'engineReadyStateProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$engineReadyStateHash();
@$internal
@override
EngineReadyState create() => EngineReadyState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$engineReadyStateHash() => r'ab3fe385fbda4d7d8acaf8f5428b3b48b75f3f46';
abstract class _$EngineReadyState 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);
}
}
/// Stream of ML model progress events
@ProviderFor(mlProgressEvents)
final mlProgressEventsProvider = MlProgressEventsProvider._();
/// Stream of ML model progress events
final class MlProgressEventsProvider
extends
$FunctionalProvider<
AsyncValue<MlProgressData>,
MlProgressData,
Stream<MlProgressData>
>
with $FutureModifier<MlProgressData>, $StreamProvider<MlProgressData> {
/// Stream of ML model progress events
MlProgressEventsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'mlProgressEventsProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$mlProgressEventsHash();
@$internal
@override
$StreamProviderElement<MlProgressData> $createElement(
$ProviderPointer pointer,
) => $StreamProviderElement(pointer);
@override
Stream<MlProgressData> create(Ref ref) {
return mlProgressEvents(ref);
}
}
String _$mlProgressEventsHash() => r'41c1e6aece7f9ee2bebe5d189c6bec753c2fefc8';
/// Translation engine state (browser-level: supported languages, engine availability)
@ProviderFor(TranslationEngineState)
final translationEngineStateProvider = TranslationEngineStateProvider._();
/// Translation engine state (browser-level: supported languages, engine availability)
final class TranslationEngineStateProvider
extends
$NotifierProvider<TranslationEngineState, TranslationEngineStateData?> {
/// Translation engine state (browser-level: supported languages, engine availability)
TranslationEngineStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'translationEngineStateProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$translationEngineStateHash();
@$internal
@override
TranslationEngineState create() => TranslationEngineState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(TranslationEngineStateData? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<TranslationEngineStateData?>(value),
);
}
}
String _$translationEngineStateHash() =>
r'04592e18715b0e237e9bc027287a880a4bb023da';
/// Translation engine state (browser-level: supported languages, engine availability)
abstract class _$TranslationEngineState
extends $Notifier<TranslationEngineStateData?> {
TranslationEngineStateData? build();
@$mustCallSuper
@override
void runBuild() {
final ref =
this.ref
as $Ref<TranslationEngineStateData?, TranslationEngineStateData?>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
TranslationEngineStateData?,
TranslationEngineStateData?
>,
TranslationEngineStateData?,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
/// Tracks active ML model downloads
@ProviderFor(MlDownloadState)
final mlDownloadStateProvider = MlDownloadStateProvider._();
/// Tracks active ML model downloads
final class MlDownloadStateProvider
extends $NotifierProvider<MlDownloadState, MlProgressData?> {
/// Tracks active ML model downloads
MlDownloadStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'mlDownloadStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$mlDownloadStateHash();
@$internal
@override
MlDownloadState create() => MlDownloadState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(MlProgressData? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<MlProgressData?>(value),
);
}
}
String _$mlDownloadStateHash() => r'4b943f0a950bf63c9a1b628d4104fdd6fa0d8d07';
/// Tracks active ML model downloads
abstract class _$MlDownloadState extends $Notifier<MlProgressData?> {
MlProgressData? build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<MlProgressData?, MlProgressData?>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<MlProgressData?, MlProgressData?>,
MlProgressData?,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@@ -0,0 +1,40 @@
/*
* 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';
part 'browser_extension.g.dart';
@Riverpod(keepAlive: true)
GeckoBrowserExtensionService browserExtensionService(Ref ref) {
final service = GeckoBrowserExtensionService.setUp();
ref.onDispose(() {
service.dispose();
});
return service;
}
@Riverpod()
Stream<String> feedRequested(Ref ref) {
final service = ref.watch(browserExtensionServiceProvider);
return service.feedRequested;
}
@@ -0,0 +1,91 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'browser_extension.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(browserExtensionService)
final browserExtensionServiceProvider = BrowserExtensionServiceProvider._();
final class BrowserExtensionServiceProvider
extends
$FunctionalProvider<
GeckoBrowserExtensionService,
GeckoBrowserExtensionService,
GeckoBrowserExtensionService
>
with $Provider<GeckoBrowserExtensionService> {
BrowserExtensionServiceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'browserExtensionServiceProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$browserExtensionServiceHash();
@$internal
@override
$ProviderElement<GeckoBrowserExtensionService> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
GeckoBrowserExtensionService create(Ref ref) {
return browserExtensionService(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(GeckoBrowserExtensionService value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<GeckoBrowserExtensionService>(value),
);
}
}
String _$browserExtensionServiceHash() =>
r'c3f67763e0abb10039b407fced6e175e5ec7c6c3';
@ProviderFor(feedRequested)
final feedRequestedProvider = FeedRequestedProvider._();
final class FeedRequestedProvider
extends $FunctionalProvider<AsyncValue<String>, String, Stream<String>>
with $FutureModifier<String>, $StreamProvider<String> {
FeedRequestedProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'feedRequestedProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$feedRequestedHash();
@$internal
@override
$StreamProviderElement<String> $createElement($ProviderPointer pointer) =>
$StreamProviderElement(pointer);
@override
Stream<String> create(Ref ref) {
return feedRequested(ref);
}
}
String _$feedRequestedHash() => r'4f179d0878072a77a87422ff6afdac53a3d57c04';
@@ -0,0 +1,48 @@
/*
* 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:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
part 'desktop_mode.g.dart';
@Riverpod(keepAlive: true)
class DesktopMode extends _$DesktopMode {
// ignore: use_setters_to_change_properties
void enabled(bool value) {
state = value;
}
void toggle() {
state = !state;
}
@override
bool build(String tabId) {
listenSelf((previous, next) async {
if (previous != null) {
await ref
.read(tabSessionProvider(tabId: tabId).notifier)
.requestDesktopSite(next);
}
});
return false;
}
}
@@ -0,0 +1,99 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'desktop_mode.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(DesktopMode)
final desktopModeProvider = DesktopModeFamily._();
final class DesktopModeProvider extends $NotifierProvider<DesktopMode, bool> {
DesktopModeProvider._({
required DesktopModeFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'desktopModeProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$desktopModeHash();
@override
String toString() {
return r'desktopModeProvider'
''
'($argument)';
}
@$internal
@override
DesktopMode create() => DesktopMode();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
@override
bool operator ==(Object other) {
return other is DesktopModeProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$desktopModeHash() => r'18550009e95a23ff82d30206ce81daa859c57c26';
final class DesktopModeFamily extends $Family
with $ClassFamilyOverride<DesktopMode, bool, bool, bool, String> {
DesktopModeFamily._()
: super(
retry: null,
name: r'desktopModeProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: false,
);
DesktopModeProvider call(String tabId) =>
DesktopModeProvider._(argument: tabId, from: this);
@override
String toString() => r'desktopModeProvider';
}
abstract class _$DesktopMode extends $Notifier<bool> {
late final _$args = ref.$arg as String;
String get tabId => _$args;
bool build(String tabId);
@$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(_$args));
}
}
@@ -0,0 +1,69 @@
/*
* 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 'selected_tab.g.dart';
@Riverpod(keepAlive: true)
class SelectedTab extends _$SelectedTab {
@override
String? build() {
final eventSerivce = ref.watch(eventServiceProvider);
ref.listen(
fireImmediately: true,
engineReadyStateProvider,
(previous, next) async {
if (next) {
await GeckoTabService().syncEvents(onSelectedTabChange: true);
}
},
onError: (error, stackTrace) {
logger.e(
'Error listening to engineReadyStateProvider',
error: error,
stackTrace: stackTrace,
);
},
);
final selectedTabSub = eventSerivce.selectedTabEvents.listen(
(tabId) {
state = tabId;
},
onError: (Object error, StackTrace stackTrace) {
logger.e(
'Error in selected tab events',
error: error,
stackTrace: stackTrace,
);
},
);
ref.onDispose(() async {
await selectedTabSub.cancel();
});
return null;
}
}
@@ -0,0 +1,62 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'selected_tab.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(SelectedTab)
final selectedTabProvider = SelectedTabProvider._();
final class SelectedTabProvider
extends $NotifierProvider<SelectedTab, String?> {
SelectedTabProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'selectedTabProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$selectedTabHash();
@$internal
@override
SelectedTab create() => SelectedTab();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(String? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<String?>(value),
);
}
}
String _$selectedTabHash() => r'5caa359376e9103767e2d2e94c548d53cba79d0f';
abstract class _$SelectedTab 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,74 @@
/*
* 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:fast_equatable/fast_equatable.dart';
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 'tab_list.g.dart';
@Riverpod(keepAlive: true)
class TabList extends _$TabList {
@override
EquatableValue<List<String>> build() {
final eventService = ref.watch(eventServiceProvider);
ref.listen(
fireImmediately: true,
engineReadyStateProvider,
(previous, next) async {
if (next) {
await GeckoTabService().syncEvents(onTabListChange: true);
}
},
onError: (error, stackTrace) {
logger.e(
'Error listening to eventServiceProvider',
error: error,
stackTrace: stackTrace,
);
},
);
final tabListSub = eventService.tabListEvents.listen(
(tabs) {
final equatableTabs = EquatableValue(tabs);
if (equatableTabs != state) {
state = equatableTabs;
}
},
onError: (Object error, StackTrace stackTrace) {
logger.e(
'Error in tab list events',
error: error,
stackTrace: stackTrace,
);
},
);
ref.onDispose(() async {
await tabListSub.cancel();
});
return EquatableValue([]);
}
}
@@ -0,0 +1,67 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'tab_list.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(TabList)
final tabListProvider = TabListProvider._();
final class TabListProvider
extends $NotifierProvider<TabList, EquatableValue<List<String>>> {
TabListProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'tabListProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabListHash();
@$internal
@override
TabList create() => TabList();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(EquatableValue<List<String>> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<EquatableValue<List<String>>>(value),
);
}
}
String _$tabListHash() => r'88c2598a4d586fee9e3d0f1b418cf88ea5c7da75';
abstract class _$TabList extends $Notifier<EquatableValue<List<String>>> {
EquatableValue<List<String>> build();
@$mustCallSuper
@override
void runBuild() {
final ref =
this.ref
as $Ref<EquatableValue<List<String>>, EquatableValue<List<String>>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
EquatableValue<List<String>>,
EquatableValue<List<String>>
>,
EquatableValue<List<String>>,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@@ -0,0 +1,130 @@
/*
* 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:typed_data';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'tab_session.g.dart';
@Riverpod(keepAlive: true)
class TabSession extends _$TabSession {
late GeckoSessionService _sessionService;
Future<void> loadUrl({
required Uri url,
LoadUrlFlags flags = LoadUrlFlags.NONE,
Map<String, String>? additionalHeaders,
}) {
return _sessionService.loadUrl(
url: url,
flags: flags,
additionalHeaders: additionalHeaders,
);
}
Future<void> stopLoading() {
return _sessionService.stopLoading();
}
Future<void> reload({LoadUrlFlags flags = LoadUrlFlags.NONE}) {
return _sessionService.reload(flags: flags);
}
Future<void> goBack() {
return _sessionService.goBack();
}
Future<void> goForward() {
return _sessionService.goForward();
}
Future<void> goToHistoryIndex({required int index}) {
return _sessionService.goToHistoryIndex(index: index);
}
Future<void> exitFullscreen() {
return _sessionService.exitFullscreen();
}
Future<Uint8List?> requestScreenshot({bool requireImageResult = true}) {
return _sessionService.requestScreenshot(requireImageResult);
}
Future<void> requestDesktopSite(bool enable) {
return _sessionService.requestDesktopSite(enable: enable);
}
Future<void> saveToPdf() {
return _sessionService.saveToPdf();
}
Future<void> printContent() {
return _sessionService.printContent();
}
Future<void> translate({
required String fromLanguage,
required String toLanguage,
bool? downloadModel,
}) {
return _sessionService.translate(
fromLanguage: fromLanguage,
toLanguage: toLanguage,
options: TranslationOptions(downloadModel: downloadModel ?? true),
);
}
Future<void> translateRestore() {
return _sessionService.translateRestore();
}
Future<void> pageUp() {
// Android KeyEvent.KEYCODE_PAGE_UP = 92
return _sessionService.dispatchKeyEvent(keyCode: 92);
}
Future<void> pageDown() {
// Android KeyEvent.KEYCODE_PAGE_DOWN = 93
return _sessionService.dispatchKeyEvent(keyCode: 93);
}
Future<void> scrollToTop() {
// Android KeyEvent.KEYCODE_MOVE_HOME = 122
return _sessionService.dispatchKeyEvent(keyCode: 122);
}
Future<void> scrollToBottom() {
// Android KeyEvent.KEYCODE_MOVE_END = 123
return _sessionService.dispatchKeyEvent(keyCode: 123);
}
@override
void build({required String? tabId}) {
_sessionService = (tabId != null)
? GeckoSessionService(tabId: tabId)
: GeckoSessionService.forActiveTab();
}
}
@Riverpod(keepAlive: true)
Raw<TabSession> selectedTabSessionNotifier(Ref ref) {
return ref.watch(tabSessionProvider(tabId: null).notifier);
}
@@ -0,0 +1,142 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'tab_session.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(TabSession)
final tabSessionProvider = TabSessionFamily._();
final class TabSessionProvider extends $NotifierProvider<TabSession, void> {
TabSessionProvider._({
required TabSessionFamily super.from,
required String? super.argument,
}) : super(
retry: null,
name: r'tabSessionProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabSessionHash();
@override
String toString() {
return r'tabSessionProvider'
''
'($argument)';
}
@$internal
@override
TabSession create() => TabSession();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(void value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<void>(value),
);
}
@override
bool operator ==(Object other) {
return other is TabSessionProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$tabSessionHash() => r'636f0940425d689ff98fabfc61687de828a8eeb9';
final class TabSessionFamily extends $Family
with $ClassFamilyOverride<TabSession, void, void, void, String?> {
TabSessionFamily._()
: super(
retry: null,
name: r'tabSessionProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: false,
);
TabSessionProvider call({required String? tabId}) =>
TabSessionProvider._(argument: tabId, from: this);
@override
String toString() => r'tabSessionProvider';
}
abstract class _$TabSession extends $Notifier<void> {
late final _$args = ref.$arg as String?;
String? get tabId => _$args;
void build({required String? tabId});
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<void, void>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<void, void>,
void,
Object?,
Object?
>;
element.handleCreate(ref, () => build(tabId: _$args));
}
}
@ProviderFor(selectedTabSessionNotifier)
final selectedTabSessionProvider = SelectedTabSessionNotifierProvider._();
final class SelectedTabSessionNotifierProvider
extends
$FunctionalProvider<Raw<TabSession>, Raw<TabSession>, Raw<TabSession>>
with $Provider<Raw<TabSession>> {
SelectedTabSessionNotifierProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'selectedTabSessionProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$selectedTabSessionNotifierHash();
@$internal
@override
$ProviderElement<Raw<TabSession>> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
Raw<TabSession> create(Ref ref) {
return selectedTabSessionNotifier(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Raw<TabSession> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Raw<TabSession>>(value),
);
}
}
String _$selectedTabSessionNotifierHash() =>
r'3945a3a3faf615e1c95819abda276f3e251d5b4c';
@@ -0,0 +1,467 @@
/*
* 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:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:nullability/nullability.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:rxdart/rxdart.dart';
import 'package:weblibre/core/logger.dart';
import 'package:weblibre/core/routing/routes.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/readerable.dart';
import 'package:weblibre/features/geckoview/domain/entities/states/security.dart';
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/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';
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
import 'package:weblibre/features/geckoview/utils/image_helper.dart';
import 'package:weblibre/features/user/data/models/tor_settings.dart';
import 'package:weblibre/features/user/domain/repositories/tor_settings.dart';
part 'tab_state.g.dart';
@Riverpod(keepAlive: true)
class TabStates extends _$TabStates {
/// Disposes images from a TabState to free GPU memory.
void _disposeTabImages(TabState tab) {
tab.icon?.dispose();
tab.thumbnail?.dispose();
}
/// Updates state while disposing images from removed tabs.
void _updateState(Map<String, TabState> newState) {
// Find and dispose images from tabs that are being removed
for (final tabId in state.keys) {
if (!newState.containsKey(tabId)) {
_disposeTabImages(state[tabId]!);
}
}
state = newState;
}
Future<void> _onTabContentStateChange(TabContentState contentState) async {
final current = await patchedState(contentState.id);
final url = Uri.parse(contentState.url);
// Determine title based on priority: new non-empty title > existing title if URL authority unchanged > new title
final String resolvedTitle;
if (contentState.title.isNotEmpty) {
resolvedTitle = contentState.title;
} else if (current.url.authority == url.authority) {
resolvedTitle = current.title;
} else {
resolvedTitle = contentState.title;
}
// Infer tabMode from context ID if not already set and context is isolated
final inferredTabMode = switch (current.tabMode) {
IsolatedTabMode() => current.tabMode,
_ when isIsolatedContextId(contentState.contextId) => TabMode.isolated(
contentState.contextId!,
),
_ when contentState.isPrivate => TabMode.private,
_ => TabMode.regular,
};
final newState = current.copyWith(
parentId: contentState.parentId,
contextId: contentState.contextId,
url: url,
title: resolvedTitle,
progress: contentState.progress,
tabMode: inferredTabMode,
isFullScreen: contentState.isFullScreen,
isLoading: contentState.isLoading,
showToolbarAsExpanded: contentState.showToolbarAsExpanded,
);
_updateState({...state}..[contentState.id] = newState);
if (newState.isFinishedLoading) {
ref
.read(geckoInferenceRepositoryProvider.notifier)
.markInitialLoadComplete();
}
}
Future<TabState> patchedState(String id) async {
var current = stateOrNull?[id];
if (current == null || current.url == TabState.defaultUrl) {
current ??= TabState.$default(id);
final tabData = await ref
.read(tabDataRepositoryProvider.notifier)
.getTabDataById(id);
if (tabData?.url != null) {
current = current.copyWith(
title: tabData!.title ?? current.title,
url: tabData.url ?? current.url,
);
}
}
return current;
}
Future<void> _onIconChange(IconChangeEvent event) async {
final IconChangeEvent(:tabId, :bytes) = event;
final image = await bytes.mapNotNull((bytes) => tryDecodeImage(bytes));
final current = state[tabId] ?? TabState.$default(tabId);
// Dispose old icon only after successfully creating new one
current.icon?.dispose();
state = {...state}..[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);
// Dispose old thumbnail only after successfully creating new one
current.thumbnail?.dispose();
state = {...state}..[tabId] = current.copyWith.thumbnail(image);
}
void _onSecurityInfoStateChange(SecurityInfoEvent event) {
final SecurityInfoEvent(:tabId, :securityInfo) = event;
final current = state[tabId] ?? TabState.$default(tabId);
state = {...state}
..[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,
),
);
}
void _onReaderableStateChange(ReaderableEvent event) {
final ReaderableEvent(:tabId, :readerable) = event;
final current = state[tabId] ?? TabState.$default(tabId);
state = {...state}
..[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),
);
}
void _onFindResultsChange(FindResultsEvent event) {
final FindResultsEvent(:tabId, :results) = event;
final current = state[tabId] ?? TabState.$default(tabId);
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());
}
}
@override
Map<String, TabState> build() {
final eventService = ref.watch(eventServiceProvider);
final subscriptions = [
eventService.tabContentEvents.listen(
(event) async {
await _onTabContentStateChange(event);
},
onError: (Object error, StackTrace stackTrace) {
logger.e(
'Error in tab content events',
error: error,
stackTrace: stackTrace,
);
},
),
eventService.iconChangeEvents.listen(
(event) async {
await _onIconChange(event);
},
onError: (Object error, StackTrace stackTrace) {
logger.e(
'Error in icon change events',
error: error,
stackTrace: stackTrace,
);
},
),
eventService.thumbnailEvents.listen(
(event) async {
await _onThumbnailChange(event);
},
onError: (Object error, StackTrace stackTrace) {
logger.e(
'Error in thumbnail events',
error: error,
stackTrace: stackTrace,
);
},
),
eventService.securityInfoEvents.listen(
(event) {
_onSecurityInfoStateChange(event);
},
onError: (Object error, StackTrace stackTrace) {
logger.e(
'Error in security info events',
error: error,
stackTrace: stackTrace,
);
},
),
eventService.historyEvents.listen(
(event) {
_onHistoryStateChange(event);
},
onError: (Object error, StackTrace stackTrace) {
logger.e(
'Error in history events',
error: error,
stackTrace: stackTrace,
);
},
),
eventService.readerableEvents.listen(
(event) {
_onReaderableStateChange(event);
},
onError: (Object error, StackTrace stackTrace) {
logger.e(
'Error in readerable events',
error: error,
stackTrace: stackTrace,
);
},
),
eventService.findResultsEvent
.debounceTime(const Duration(milliseconds: 25))
.listen(
(event) {
_onFindResultsChange(event);
},
onError: (Object error, StackTrace stackTrace) {
logger.e(
'Error in find results events',
error: error,
stackTrace: stackTrace,
);
},
),
eventService.tabTranslationEvents.listen(
(event) {
_onTabTranslationStateChange(event);
},
onError: (Object error, StackTrace stackTrace) {
logger.e(
'Error in tab translation events',
error: error,
stackTrace: stackTrace,
);
},
),
];
ref.listen(
fireImmediately: true,
engineReadyStateProvider,
(previous, next) async {
if (next) {
await GeckoTabService().syncEvents(
onTabContentStateChange: true,
onIconChange: true,
onThumbnailChange: true,
onSecurityInfoStateChange: true,
onHistoryStateChange: true,
onFindResults: true,
onTranslationStateChange: true,
);
}
},
onError: (error, stackTrace) {
logger.e(
'Error listening to engineReadyStateProvider',
error: error,
stackTrace: stackTrace,
);
},
);
ref.onDispose(() async {
// Dispose all remaining tab images
for (final tab in state.values) {
_disposeTabImages(tab);
}
// Cancel all stream subscriptions
for (final sub in subscriptions) {
await sub.cancel();
}
});
return {};
}
}
@Riverpod()
TabState? tabState(Ref ref, String? tabId) {
if (tabId == null) {
return null;
}
return ref.watch(tabStatesProvider.select((tabs) => tabs[tabId]));
}
@Riverpod()
Future<TabState> tabStateWithFallback(Ref ref, String tabId) async {
final state = ref.watch(tabStateProvider(tabId));
if (state != null) {
return state;
}
return await ref.read(tabStatesProvider.notifier).patchedState(tabId);
}
@Riverpod()
Future<bool> isTabTunneled(Ref ref, String? tabId) async {
final tabState = ref.watch(tabStateProvider(tabId));
final torSettings = ref.watch(torSettingsWithDefaultsProvider);
if (tabState != null) {
// Isolated tabs follow the same proxy rules as regular tabs
// (container-based routing via proxy aliasing)
if (tabState.tabMode is PrivateTabMode) {
return torSettings.proxyPrivateTabsTor;
} else {
switch (torSettings.proxyRegularTabsMode) {
case TorRegularTabProxyMode.container:
final containerData = await ref
.read(tabDataRepositoryProvider.notifier)
.getTabContainerData(tabState.id);
if (!ref.mounted) return false;
return containerData?.metadata.useProxy ?? false;
case TorRegularTabProxyMode.all:
return true;
}
}
}
return false;
}
@Riverpod()
TabState? selectedTabState(Ref ref) {
final tabId = ref.watch(selectedTabProvider);
return ref.watch(tabStateProvider(tabId));
}
@Riverpod()
TabType? selectedTabType(Ref ref) {
final selectedState = ref.watch(selectedTabStateProvider);
return selectedState?.tabMode.toTabType();
}
@Riverpod()
AsyncValue<String?> selectedTabContainerId(Ref ref) {
final tabId = ref.watch(selectedTabProvider);
if (tabId != null) {
return ref.watch(watchContainerTabIdProvider(tabId));
}
return const AsyncData(null);
}
// @Riverpod()
// Stream<int> tabScrollY(Ref ref, String? tabId, Duration sampleTime) {
// final eventService = ref.watch(eventServiceProvider);
// return eventService.scrollEvent
// .where((event) => event.tabId == tabId)
// .sampleTime(sampleTime)
// .map((event) => event.scrollY)
// .asBroadcastStream();
// }
@@ -0,0 +1,409 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'tab_state.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(TabStates)
final tabStatesProvider = TabStatesProvider._();
final class TabStatesProvider
extends $NotifierProvider<TabStates, Map<String, TabState>> {
TabStatesProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'tabStatesProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabStatesHash();
@$internal
@override
TabStates create() => TabStates();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Map<String, TabState> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Map<String, TabState>>(value),
);
}
}
String _$tabStatesHash() => r'6fc9a91b1343f9ae0b442100bed20e42448afa37';
abstract class _$TabStates extends $Notifier<Map<String, TabState>> {
Map<String, TabState> build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<Map<String, TabState>, Map<String, TabState>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<Map<String, TabState>, Map<String, TabState>>,
Map<String, TabState>,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(tabState)
final tabStateProvider = TabStateFamily._();
final class TabStateProvider
extends $FunctionalProvider<TabState?, TabState?, TabState?>
with $Provider<TabState?> {
TabStateProvider._({
required TabStateFamily super.from,
required String? super.argument,
}) : super(
retry: null,
name: r'tabStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabStateHash();
@override
String toString() {
return r'tabStateProvider'
''
'($argument)';
}
@$internal
@override
$ProviderElement<TabState?> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
TabState? create(Ref ref) {
final argument = this.argument as String?;
return tabState(ref, argument);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(TabState? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<TabState?>(value),
);
}
@override
bool operator ==(Object other) {
return other is TabStateProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$tabStateHash() => r'f57b2353acc99ca73670d504b45ea5ff19df38c7';
final class TabStateFamily extends $Family
with $FunctionalFamilyOverride<TabState?, String?> {
TabStateFamily._()
: super(
retry: null,
name: r'tabStateProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
TabStateProvider call(String? tabId) =>
TabStateProvider._(argument: tabId, from: this);
@override
String toString() => r'tabStateProvider';
}
@ProviderFor(tabStateWithFallback)
final tabStateWithFallbackProvider = TabStateWithFallbackFamily._();
final class TabStateWithFallbackProvider
extends
$FunctionalProvider<AsyncValue<TabState>, TabState, FutureOr<TabState>>
with $FutureModifier<TabState>, $FutureProvider<TabState> {
TabStateWithFallbackProvider._({
required TabStateWithFallbackFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'tabStateWithFallbackProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabStateWithFallbackHash();
@override
String toString() {
return r'tabStateWithFallbackProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<TabState> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<TabState> create(Ref ref) {
final argument = this.argument as String;
return tabStateWithFallback(ref, argument);
}
@override
bool operator ==(Object other) {
return other is TabStateWithFallbackProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$tabStateWithFallbackHash() =>
r'061011ac79738dbe4e662a76fccbc8592cba21cf';
final class TabStateWithFallbackFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<TabState>, String> {
TabStateWithFallbackFamily._()
: super(
retry: null,
name: r'tabStateWithFallbackProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
TabStateWithFallbackProvider call(String tabId) =>
TabStateWithFallbackProvider._(argument: tabId, from: this);
@override
String toString() => r'tabStateWithFallbackProvider';
}
@ProviderFor(isTabTunneled)
final isTabTunneledProvider = IsTabTunneledFamily._();
final class IsTabTunneledProvider
extends $FunctionalProvider<AsyncValue<bool>, bool, FutureOr<bool>>
with $FutureModifier<bool>, $FutureProvider<bool> {
IsTabTunneledProvider._({
required IsTabTunneledFamily super.from,
required String? super.argument,
}) : super(
retry: null,
name: r'isTabTunneledProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$isTabTunneledHash();
@override
String toString() {
return r'isTabTunneledProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<bool> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<bool> create(Ref ref) {
final argument = this.argument as String?;
return isTabTunneled(ref, argument);
}
@override
bool operator ==(Object other) {
return other is IsTabTunneledProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$isTabTunneledHash() => r'b11134b33ad0c98ffddd14afc2c790fb65735e93';
final class IsTabTunneledFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<bool>, String?> {
IsTabTunneledFamily._()
: super(
retry: null,
name: r'isTabTunneledProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
IsTabTunneledProvider call(String? tabId) =>
IsTabTunneledProvider._(argument: tabId, from: this);
@override
String toString() => r'isTabTunneledProvider';
}
@ProviderFor(selectedTabState)
final selectedTabStateProvider = SelectedTabStateProvider._();
final class SelectedTabStateProvider
extends $FunctionalProvider<TabState?, TabState?, TabState?>
with $Provider<TabState?> {
SelectedTabStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'selectedTabStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$selectedTabStateHash();
@$internal
@override
$ProviderElement<TabState?> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
TabState? create(Ref ref) {
return selectedTabState(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(TabState? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<TabState?>(value),
);
}
}
String _$selectedTabStateHash() => r'dbd36af7af286bd9d079f30e8e11bbda23bf7728';
@ProviderFor(selectedTabType)
final selectedTabTypeProvider = SelectedTabTypeProvider._();
final class SelectedTabTypeProvider
extends $FunctionalProvider<TabType?, TabType?, TabType?>
with $Provider<TabType?> {
SelectedTabTypeProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'selectedTabTypeProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$selectedTabTypeHash();
@$internal
@override
$ProviderElement<TabType?> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
TabType? create(Ref ref) {
return selectedTabType(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(TabType? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<TabType?>(value),
);
}
}
String _$selectedTabTypeHash() => r'4cf99c9175357a7835fc3c4eff29a9f328931e4f';
@ProviderFor(selectedTabContainerId)
final selectedTabContainerIdProvider = SelectedTabContainerIdProvider._();
final class SelectedTabContainerIdProvider
extends
$FunctionalProvider<
AsyncValue<String?>,
AsyncValue<String?>,
AsyncValue<String?>
>
with $Provider<AsyncValue<String?>> {
SelectedTabContainerIdProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'selectedTabContainerIdProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$selectedTabContainerIdHash();
@$internal
@override
$ProviderElement<AsyncValue<String?>> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
AsyncValue<String?> create(Ref ref) {
return selectedTabContainerId(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(AsyncValue<String?> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<AsyncValue<String?>>(value),
);
}
}
String _$selectedTabContainerIdHash() =>
r'07899d29f69654d3b314d0da945ad402b1003b41';
@@ -0,0 +1,200 @@
/*
* 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 'dart:ui';
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/domain/entities/equatable_image.dart';
import 'package:weblibre/features/geckoview/domain/entities/states/web_extension.dart';
import 'package:weblibre/features/geckoview/domain/providers.dart';
import 'package:weblibre/features/geckoview/utils/image_helper.dart';
import 'package:weblibre/utils/lru_cache.dart';
part 'web_extensions_state.g.dart';
@Riverpod(keepAlive: true)
class WebExtensionsState extends _$WebExtensionsState {
late final LRUCache<String, EquatableImage> _imageCache;
WebExtensionsState()
: _imageCache = LRUCache(50, onEvict: (image) => image.dispose());
void _onExtensionUpdate(ExtensionDataEvent event) {
final ExtensionDataEvent(:extensionId, :data) = event;
if (data != null) {
final cachedIcon = _imageCache.get(extensionId);
if (cachedIcon != null && cachedIcon.value == null) {
_imageCache.remove(extensionId);
}
final current =
state[extensionId] ??
WebExtensionState(
extensionId: extensionId,
icon: _imageCache.get(extensionId),
enabled: false,
);
state = {...state}
..[extensionId] = current.copyWith(
title: data.title,
enabled: data.enabled ?? current.enabled,
badgeText: data.badgeText,
badgeTextColor: data.badgeTextColor != null
? Color(data.badgeTextColor!)
: null,
badgeBackgroundColor: data.badgeBackgroundColor != null
? Color(data.badgeBackgroundColor!)
: null,
);
} else {
if (state.containsKey(extensionId)) {
state = {...state}..remove(extensionId);
// remove() triggers onEvict which handles disposal
_imageCache.remove(extensionId);
}
}
}
Future<void> _onIconChange(ExtensionIconEvent event) async {
final ExtensionIconEvent(:extensionId, :bytes) = event;
final image = await tryDecodeImage(bytes);
if (image != null) {
// set() will evict the old entry via onEvict callback, which handles disposal
_imageCache.set(extensionId, image);
if (state.containsKey(extensionId)) {
state = {...state}
..[extensionId] = state[extensionId]!.copyWith.icon(image);
}
}
}
@override
Map<String, WebExtensionState> build(WebExtensionActionType actionType) {
final addonService = ref.watch(addonServiceProvider);
final subscriptions = switch (actionType) {
WebExtensionActionType.browser => [
addonService.browserExtensionStream.listen(
(event) {
_onExtensionUpdate(event);
},
onError: (Object error, StackTrace stackTrace) {
logger.e(
'Error in browser extension stream',
error: error,
stackTrace: stackTrace,
);
},
),
addonService.browserIconStream.listen(
(event) async {
await _onIconChange(event);
},
onError: (Object error, StackTrace stackTrace) {
logger.e(
'Error in browser icon stream',
error: error,
stackTrace: stackTrace,
);
},
),
],
WebExtensionActionType.page => [
addonService.pageExtensionStream.listen(
(event) {
_onExtensionUpdate(event);
},
onError: (Object error, StackTrace stackTrace) {
logger.e(
'Error in page extension stream',
error: error,
stackTrace: stackTrace,
);
},
),
addonService.pageIconStream.listen(
(event) async {
await _onIconChange(event);
},
onError: (Object error, StackTrace stackTrace) {
logger.e(
'Error in page icon stream',
error: error,
stackTrace: stackTrace,
);
},
),
],
};
// Sync extension events after engine is ready and listeners are set up
// This ensures we get the current state even if we missed initial events
ref.listen(
fireImmediately: true,
engineReadyStateProvider,
(previous, next) async {
if (next) {
try {
await GeckoTabService().syncEvents(
onBrowserExtensionsChange:
actionType == WebExtensionActionType.browser,
onPageExtensionsChange: actionType == WebExtensionActionType.page,
onBrowserExtensionIcons:
actionType == WebExtensionActionType.browser,
onPageExtensionIcons: actionType == WebExtensionActionType.page,
);
} catch (e, s) {
logger.w(
'Failed to sync extension events for $actionType',
error: e,
stackTrace: s,
);
}
}
},
onError: (error, stackTrace) {
logger.e(
'Error listening to engineReadyStateProvider',
error: error,
stackTrace: stackTrace,
);
},
);
ref.onDispose(() async {
// Dispose all cached images
_imageCache.clear();
// Cancel all stream subscriptions
for (final sub in subscriptions) {
await sub.cancel();
}
});
return {};
}
}
@@ -0,0 +1,120 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'web_extensions_state.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(WebExtensionsState)
final webExtensionsStateProvider = WebExtensionsStateFamily._();
final class WebExtensionsStateProvider
extends
$NotifierProvider<WebExtensionsState, Map<String, WebExtensionState>> {
WebExtensionsStateProvider._({
required WebExtensionsStateFamily super.from,
required WebExtensionActionType super.argument,
}) : super(
retry: null,
name: r'webExtensionsStateProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$webExtensionsStateHash();
@override
String toString() {
return r'webExtensionsStateProvider'
''
'($argument)';
}
@$internal
@override
WebExtensionsState create() => WebExtensionsState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Map<String, WebExtensionState> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Map<String, WebExtensionState>>(
value,
),
);
}
@override
bool operator ==(Object other) {
return other is WebExtensionsStateProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$webExtensionsStateHash() =>
r'13cbbea409b5b643eb6766689f6b23a4d1701204';
final class WebExtensionsStateFamily extends $Family
with
$ClassFamilyOverride<
WebExtensionsState,
Map<String, WebExtensionState>,
Map<String, WebExtensionState>,
Map<String, WebExtensionState>,
WebExtensionActionType
> {
WebExtensionsStateFamily._()
: super(
retry: null,
name: r'webExtensionsStateProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: false,
);
WebExtensionsStateProvider call(WebExtensionActionType actionType) =>
WebExtensionsStateProvider._(argument: actionType, from: this);
@override
String toString() => r'webExtensionsStateProvider';
}
abstract class _$WebExtensionsState
extends $Notifier<Map<String, WebExtensionState>> {
late final _$args = ref.$arg as WebExtensionActionType;
WebExtensionActionType get actionType => _$args;
Map<String, WebExtensionState> build(WebExtensionActionType actionType);
@$mustCallSuper
@override
void runBuild() {
final ref =
this.ref
as $Ref<
Map<String, WebExtensionState>,
Map<String, WebExtensionState>
>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
Map<String, WebExtensionState>,
Map<String, WebExtensionState>
>,
Map<String, WebExtensionState>,
Object?,
Object?
>;
element.handleCreate(ref, () => build(_$args));
}
}
@@ -0,0 +1,850 @@
/*
* 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:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:nullability/nullability.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:synchronized/synchronized.dart';
import 'package:weblibre/core/logger.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/selected_tab.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_list.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_data.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';
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_source.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
import 'package:weblibre/features/tor/domain/repositories/tor_proxy.dart';
import 'package:weblibre/features/user/data/models/general_settings.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/utils/debouncer.dart';
part 'tab.g.dart';
@Riverpod(keepAlive: true)
class TabRepository extends _$TabRepository {
final _tabsService = GeckoTabService();
final _tabFromIntent = <String>{};
final _closeLock = Lock();
final _pendingIsolationCleanup = <String>{};
bool hasLaunchedFromIntent(String? tabId) {
if (tabId == null) {
return false;
}
return _tabFromIntent.contains(tabId);
}
void clearLaunchedFromIntent(String tabId) {
_tabFromIntent.remove(tabId);
}
NewTabPosition _newTabPositionForParent(String? parentId) {
if (parentId != null) {
return NewTabPosition.first;
}
return ref.read(generalSettingsWithDefaultsProvider).newTabPosition;
}
Future<String?> _resolveParentIdForContext({
required String? parentId,
required String? targetContextId,
}) async {
if (parentId == null) {
return null;
}
final parentContainerData = await ref
.read(tabDatabaseProvider)
.tabDao
.getTabContainerData(parentId)
.getSingleOrNull();
final parentContextId = parentContainerData?.metadata.contextualIdentity;
return (parentContextId == targetContextId) ? parentId : null;
}
Future<String> addTab({
required TabMode tabMode,
Uri? url,
required bool selectTab,
bool startLoading = true,
String? parentId,
LoadUrlFlags flags = LoadUrlFlags.NONE,
Source source = Internal.newTab,
HistoryMetadataKey? historyMetadata,
Map<String, String>? additionalHeaders,
TabContainerSelection containerSelection =
const TabContainerSelection.useSelected(),
bool launchedFromIntent = false,
}) async {
final tabDao = ref.read(tabDatabaseProvider).tabDao;
final assignedContainer = switch (containerSelection) {
UseSelectedContainerTabSelection() =>
await ref.read(selectedContainerProvider.notifier).fetchData(),
UnassignedContainerTabSelection() => null,
SpecificContainerTabSelection(:final container) => container,
};
// For isolated tabs, skip parent context validation since
// isolated tabs use their own immutable context ID.
final validatedParentId = tabMode is IsolatedTabMode
? parentId
: await _resolveParentIdForContext(
parentId: parentId,
targetContextId: assignedContainer?.metadata.contextualIdentity,
);
final effectiveIsolationContextId = tabMode.isolationContextId;
final effectiveContextId = tabMode is IsolatedTabMode
? effectiveIsolationContextId
: assignedContainer?.metadata.contextualIdentity;
final newTabPosition = _newTabPositionForParent(validatedParentId);
final newTabId = await tabDao.upsertTabTransactional(
() {
return _tabsService.addTab(
url: url,
selectTab: selectTab,
startLoading: startLoading,
parentId: validatedParentId,
flags: flags,
contextId: effectiveContextId,
source: source,
private: tabMode is PrivateTabMode,
historyMetadata: historyMetadata,
additionalHeaders: additionalHeaders,
);
},
parentId: Value(validatedParentId),
newTabPosition: newTabPosition,
containerId: Value(assignedContainer?.id),
url: Value(url),
tabMode: Value(tabMode),
);
if (launchedFromIntent) {
_tabFromIntent.add(newTabId);
}
return newTabId;
}
Future<List<String>> addMultipleTabs({
required List<AddTabParams> tabs,
String? selectTabId,
TabContainerSelection containerSelection =
const TabContainerSelection.unassigned(),
}) async {
final tabDao = ref.read(tabDatabaseProvider).tabDao;
final db = ref.read(tabDatabaseProvider);
final assignedContainer = switch (containerSelection) {
UseSelectedContainerTabSelection() =>
await ref.read(selectedContainerProvider.notifier).fetchData(),
UnassignedContainerTabSelection() => null,
SpecificContainerTabSelection(:final container) => container,
};
return await db.transaction(() async {
final createdTabIds = await _tabsService.addMultipleTabs(
tabs: tabs,
selectTabId: selectTabId,
);
// Build sets for validation
final creatingTabIds = createdTabIds.toSet();
final parentIdsToValidate = tabs
.map((tab) => tab.parentId)
.whereType<String>()
.where((id) => !creatingTabIds.contains(id))
.toSet();
// Batch validate parent IDs that aren't in the current creation batch
final existingParentIds = await tabDao
.getExistingTabIds(parentIdsToValidate)
.get()
.then((ids) => ids.toSet());
// Upsert all tabs in the database
for (var i = 0; i < createdTabIds.length; i++) {
final tabId = createdTabIds[i];
final tab = tabs[i];
// Validate parent exists in either the batch being created or database
String? validatedParentId;
if (tab.parentId != null) {
if (creatingTabIds.contains(tab.parentId) ||
existingParentIds.contains(tab.parentId)) {
validatedParentId = tab.parentId;
}
}
await tabDao.insertTab(
tabId,
parentId: Value(validatedParentId),
source: TabSource.manual,
newTabPosition: _newTabPositionForParent(validatedParentId),
containerId: Value(assignedContainer?.id),
url: Value(Uri.tryParse(tab.url)),
tabMode: Value(
isIsolatedContextId(tab.contextId)
? TabMode.isolated(tab.contextId!)
: tab.private
? TabMode.private
: TabMode.regular,
),
);
}
return createdTabIds;
});
}
Future<String> duplicateTab({
required String selectTabId,
required ContainerData? containerData,
required bool selectTab,
}) async {
final tabDao = ref.read(tabDatabaseProvider).tabDao;
final sourceTabMode =
await tabDao.getTabMode(selectTabId).getSingleOrNull() ??
TabMode.regular;
// Duplicating an isolated tab creates a new isolation group
final duplicateIsolationContextId = sourceTabMode is IsolatedTabMode
? newIsolatedContextId()
: null;
final duplicateTabMode = sourceTabMode is IsolatedTabMode
? TabMode.isolated(duplicateIsolationContextId!)
: sourceTabMode;
// Isolated tabs always use their isolation context ID
final effectiveContextId = sourceTabMode is IsolatedTabMode
? duplicateIsolationContextId
: containerData?.metadata.contextualIdentity;
return await tabDao.upsertTabTransactional(
() {
return _tabsService.duplicateTab(
selectTabId: selectTabId,
newContextId: effectiveContextId,
selectNewTab: selectTab,
);
},
parentId: const Value.absent(),
newTabPosition: _newTabPositionForParent(null),
containerId: Value(containerData?.id),
tabMode: Value(duplicateTabMode),
);
}
Future<bool> selectPreviouslyOpenedTab(String tabId) async {
final previousTabId = await ref
.read(tabDatabaseProvider)
.definitionsDrift
.previousTabByTimestamp(tabId: tabId)
.getSingleOrNull();
if (ref.mounted && previousTabId != null) {
return selectTab(previousTabId);
}
return false;
}
Future<bool> resumeLatestTab() async {
final latestTab = await ref
.read(tabDatabaseProvider)
.tabDao
.getTabsFifo(limit: 1)
.getSingleOrNull();
if (!ref.mounted || latestTab == null) {
return false;
}
return selectTab(latestTab.id);
}
Future<bool> resumeLatestContainerTab(String? containerId) async {
final latestTab = await ref
.read(tabDatabaseProvider)
.tabDao
.getContainerTabsFifo(containerId, limit: 1)
.getSingleOrNull();
if (!ref.mounted || latestTab == null) {
return false;
}
return selectTab(latestTab.id);
}
Future<bool> selectPreviousTab(
String tabId, {
String? containerId,
bool skipContainerCheck = true,
}) async {
final previousTabId = await ref
.read(tabDatabaseProvider)
.definitionsDrift
.previousTabByOrderKey(
tabId: tabId,
containerId: containerId,
skipContainerCheck: skipContainerCheck,
)
.getSingleOrNull();
if (ref.mounted && previousTabId != null) {
return selectTab(previousTabId);
}
return false;
}
Future<bool> selectNextTab(
String tabId, {
String? containerId,
bool skipContainerCheck = true,
}) async {
final previousTabId = await ref
.read(tabDatabaseProvider)
.definitionsDrift
.nextTabByOrderKey(
tabId: tabId,
containerId: containerId,
skipContainerCheck: skipContainerCheck,
)
.getSingleOrNull();
if (ref.mounted && previousTabId != null) {
return selectTab(previousTabId);
}
return false;
}
Future<bool> selectTab(String tabId) async {
final containerData = await ref
.read(tabDataRepositoryProvider.notifier)
.getTabContainerData(tabId);
if (!ref.mounted) return false;
if (containerData != null) {
if (containerData.metadata.useProxy) {
final proxyPluginHealthy = await GeckoContainerProxyService()
.healthcheck();
if (!proxyPluginHealthy) {
logger.w(
'Tried to open proxied tab $tabId but proxy plugin not responding',
);
return false;
}
}
}
await _tabsService.selectTab(tabId: tabId);
return true;
}
Future<void> _selectNextTab(String tabId) async {
final tabState = ref.read(tabStatesProvider)[tabId];
final currentContainerId = await ref
.read(tabDataRepositoryProvider.notifier)
.getTabContainerId(tabId);
if (!ref.mounted) return;
final sameContainerTabs = await ref
.read(containerRepositoryProvider.notifier)
.getContainerTabIds(currentContainerId)
.then((tabs) => tabs.where((tab) => tab != tabId).toList());
if (!ref.mounted) return;
// Priority 1: Check for parent tab first
if (tabState?.parentId != null) {
return _tabsService.selectTab(tabId: tabState!.parentId!);
}
// Priority 2: Check for previous tab by timestamp
final previousTabId = await ref
.read(tabDatabaseProvider)
.definitionsDrift
.previousTabByTimestamp(tabId: tabId)
.getSingleOrNull();
if (previousTabId != null) {
if (sameContainerTabs.any((tab) => tab == previousTabId)) {
return _tabsService.selectTab(tabId: previousTabId);
}
}
if (!ref.mounted) return;
final previousOrderedTabId = await ref
.read(tabDatabaseProvider)
.definitionsDrift
.previousTabByOrderKey(
tabId: tabId,
containerId: currentContainerId,
skipContainerCheck: false,
)
.getSingleOrNull();
if (previousOrderedTabId != null) {
return _tabsService.selectTab(tabId: previousOrderedTabId);
}
if (!ref.mounted) return;
final nextOrderedTabId = await ref
.read(tabDatabaseProvider)
.definitionsDrift
.nextTabByOrderKey(
tabId: tabId,
containerId: currentContainerId,
skipContainerCheck: false,
)
.getSingleOrNull();
if (nextOrderedTabId != null) {
return _tabsService.selectTab(tabId: nextOrderedTabId);
}
if (!ref.mounted) return;
final unassignedTabs = await ref
.read(containerRepositoryProvider.notifier)
.getContainerTabIds(null)
.then((tabs) => tabs.where((tab) => tab != tabId).toList());
if (unassignedTabs.isNotEmpty) {
return _tabsService.selectTab(tabId: unassignedTabs.first);
}
if (!ref.mounted) return;
final availableContainers = await ref
.read(containerRepositoryProvider.notifier)
.getAllContainersWithCount();
final nextAvailableContainer = availableContainers.firstOrNull;
if (!ref.mounted) return;
final nextContainerTabs = await nextAvailableContainer.mapNotNull(
(container) => ref
.read(containerRepositoryProvider.notifier)
.getContainerTabIds(container.id)
.then((tabs) => tabs.where((tab) => tab != tabId).toList()),
);
if (nextContainerTabs.isNotEmpty) {
return _tabsService.selectTab(tabId: nextContainerTabs!.first);
}
}
Future<void> closeTab(String tabId) {
return _closeLock.synchronized(() async {
// Collect isolation context before close
final isolationContextId = ref
.read(tabStatesProvider)[tabId]
?.isolationContextId;
if (ref.read(selectedTabProvider) == tabId) {
await _selectNextTab(tabId);
}
await _tabsService.removeTab(tabId: tabId);
// Queue isolation cleanup — actual cleanup runs after syncTabs
// deletes the DB row, so the count check is accurate.
if (isolationContextId != null) {
_pendingIsolationCleanup.add(isolationContextId);
}
});
}
Future<void> closeTabs(List<String> tabIds) {
return _closeLock.synchronized(() async {
// Collect isolation contexts from tabs being closed
for (final tabId in tabIds) {
final contextId = ref
.read(tabStatesProvider)[tabId]
?.isolationContextId;
if (contextId != null) {
_pendingIsolationCleanup.add(contextId);
}
}
final selectedTab = ref.read(selectedTabProvider);
if (selectedTab.mapNotNull(tabIds.contains) ?? false) {
await _selectNextTab(selectedTab!);
}
await _tabsService.removeTabs(ids: tabIds);
});
}
/// Clears Gecko browsing data and removes proxy alias for an isolation
/// context if no more tabs share it.
Future<void> _cleanupIsolationContextIfEmpty(String contextId) async {
final tabDao = ref.read(tabDatabaseProvider).tabDao;
// Re-verify count after close (handles concurrent close races)
final remaining = await tabDao.tabsInIsolationGroup(contextId).getSingle();
if (remaining > 0) return;
// Guard against debounced DB persistence: a sibling tab can already be
// active in-memory for this context before isolation_context_id is written.
final activeTabs = ref.read(tabListProvider).value;
final activeStates = ref.read(tabStatesProvider);
final hasActiveSibling = activeTabs.any((tabId) {
final state = activeStates[tabId];
if (state == null) return false;
return state.isolationContextId == contextId ||
state.contextId == contextId;
});
if (hasActiveSibling) {
logger.i(
'Skipping isolation cleanup for active context still in memory: $contextId',
);
return;
}
logger.i('Cleaning up isolation context: $contextId');
// Clear Gecko browsing data for this context
try {
await ref
.read(browserDataServiceProvider.notifier)
.clearDataForContext(contextId);
} catch (e, st) {
logger.e(
'Failed to clear data for isolation context $contextId',
error: e,
stackTrace: st,
);
}
// Best-effort: remove proxy alias (no-op if never set)
try {
await ref
.read(torProxyRepositoryProvider.notifier)
.removeContainerProxy(contextId);
} catch (e, st) {
logger.e(
'Failed to remove proxy for isolation context $contextId',
error: e,
stackTrace: st,
);
}
}
Future<void> undoClose() {
return _tabsService.undo();
}
/// Cleans up isolation contexts from previous crashed sessions.
/// Called once after tab list stabilizes on startup.
// Future<void> _cleanupOrphanedIsolationContexts() async {
// final tabDao = ref.read(tabDatabaseProvider).tabDao;
// try {
// await _closeLock.synchronized(() async {
// if (!ref.mounted) return;
// // Reconcile DB rows against the current engine tab snapshot, including
// // valid empty-tab sessions (retainTabIds can be empty here).
// final syncTabsResult = await tabDao.syncTabs(
// retainTabIds: ref.read(tabListProvider).value,
// );
// _pendingIsolationCleanup.addAll(
// syncTabsResult.deletedIsolationContextIds,
// );
// if (_pendingIsolationCleanup.isNotEmpty) {
// final pending = Set<String>.of(_pendingIsolationCleanup);
// _pendingIsolationCleanup.clear();
// for (final contextId in pending) {
// if (!ref.mounted) return;
// logger.i('Cleaning orphaned isolation context: $contextId');
// await _cleanupIsolationContextIfEmpty(contextId);
// }
// }
// });
// } catch (e, st) {
// logger.e(
// 'Error during orphan isolation context cleanup',
// error: e,
// stackTrace: st,
// );
// }
// }
@override
void build() {
final eventSerivce = ref.watch(eventServiceProvider);
final tabContentService = ref.watch(tabContentServiceProvider);
final db = ref.watch(tabDatabaseProvider);
final tabAddedSub = eventSerivce.tabAddedStream.listen(
(tabId) async {
final containerId = ref.read(selectedContainerProvider);
await db.tabDao.insertTab(
tabId,
parentId: const Value.absent(),
source: TabSource.addedEvent,
newTabPosition: _newTabPositionForParent(null),
containerId: Value(containerId),
);
},
onError: (Object error, StackTrace stackTrace) {
logger.e(
'Error in tab added stream',
error: error,
stackTrace: stackTrace,
);
},
);
final containerSiteAssignementSub = eventSerivce.siteAssignementEvent.listen(
(event) async {
if (event.tabId != null) {
final tabState = ref.read(tabStatesProvider)[event.tabId];
if (tabState != null) {
final uri = Uri.parse(event.url);
final originUri = event.originUrl.mapNotNull(Uri.parse);
final targetContainerId = await ref
.read(containerRepositoryProvider.notifier)
.siteAssignedContainerId(Uri.parse(uri.origin));
final containerData = await targetContainerId.mapNotNull(
(id) => ref
.read(containerRepositoryProvider.notifier)
.getContainerData(id),
);
if (containerData != null) {
final tabIsEmpty =
tabState.url == TabState.defaultUrl &&
tabState.historyState.items.isEmpty;
if (event.blocked || tabIsEmpty) {
await addTab(
url: uri,
tabMode: tabState.tabMode,
containerSelection: TabContainerSelection.specific(
containerData,
),
parentId: tabState.id,
selectTab: true,
);
if (tabState.historyState.items.isEmpty) {
await closeTab(tabState.id);
}
} else {
final tabContainerId = await ref
.read(tabDataRepositoryProvider.notifier)
.getTabContainerId(tabState.id);
if (targetContainerId != tabContainerId) {
if (originUri == null) {
await ref
.read(tabDataRepositoryProvider.notifier)
.assignContainer(tabState.id, containerData);
} else if (tabState.url == originUri) {
await ref
.read(tabDataRepositoryProvider.notifier)
.assignContainer(
tabState.id,
containerData,
closeOldTab: false,
);
} else {
logger.w(
'Could not match origin url for assignment ${tabState.url} to request ${event.originUrl}',
);
}
}
}
}
} else {
logger.w('Could not get tab for assignement ${tabState?.url}');
}
}
},
onError: (Object error, StackTrace stackTrace) {
logger.e(
'Error in container site assignment stream',
error: error,
stackTrace: stackTrace,
);
},
);
final tabContentSub = tabContentService.tabContentStream.listen(
(content) async {
await db.tabDao.updateTabContent(
content.tabId,
isProbablyReaderable: content.isProbablyReaderable,
extractedContentMarkdown: content.extractedContentMarkdown,
extractedContentPlain: content.extractedContentPlain,
fullContentMarkdown: content.fullContentMarkdown,
fullContentPlain: content.fullContentPlain,
);
},
onError: (Object error, StackTrace stackTrace) {
logger.e(
'Error in tab content stream',
error: error,
stackTrace: stackTrace,
);
},
);
ref.listen(
fireImmediately: true,
selectedTabProvider,
(previous, tabId) async {
if (tabId != null) {
await db.tabDao.touchTab(tabId, timestamp: DateTime.now());
}
},
onError: (Object error, StackTrace stackTrace) {
logger.e(
'Error listening to selectedTabProvider',
error: error,
stackTrace: stackTrace,
);
},
);
ref.listen(
tabListProvider,
(previous, next) async {
//Only sync tabs if there has been a previous value or is not empty
final shouldSyncTabs =
next.value.isNotEmpty || (previous?.value.isNotEmpty ?? false);
if (shouldSyncTabs) {
final syncTabsResult = await db.tabDao.syncTabs(
retainTabIds: next.value,
);
// Capture isolation contexts from rows deleted by syncTabs
// (orphaned tabs from crashes, or tabs the engine dropped).
_pendingIsolationCleanup.addAll(
syncTabsResult.deletedIsolationContextIds,
);
}
// 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);
}
}
// One-shot orphan cleanup after tab list stabilizes (5s debounce).
// Also runs for DB-only contexts whose rows were already deleted
// by syncTabs above (those are handled via _pendingIsolationCleanup).
// if (!orphanCleanupDone) {
// orphanCleanupTimer?.cancel();
// orphanCleanupTimer = Timer(const Duration(seconds: 5), () async {
// if (orphanCleanupDone || !ref.mounted) return;
// orphanCleanupDone = true;
// await _cleanupOrphanedIsolationContexts();
// });
// }
},
onError: (Object error, StackTrace stackTrace) {
logger.e(
'Error listening to tabListProvider',
error: error,
stackTrace: stackTrace,
);
},
);
final tabStateDebouncer = Debouncer(const Duration(seconds: 1));
Map<String, TabState>? debounceStartValue;
ref.listen(
tabStatesProvider,
(previous, next) {
//Since state changes occure pretty often and our map always contains
//the latest state, we cache the value before starting debouncing and
//later diff to that, to avoid frequent database writes
if (!tabStateDebouncer.isDebouncing) {
debounceStartValue = previous;
}
tabStateDebouncer.eventOccured(() async {
await db.tabDao.updateTabs(debounceStartValue, next);
});
},
onError: (Object error, StackTrace stackTrace) {
logger.e(
'Error listening to tabStatesProvider',
error: error,
stackTrace: stackTrace,
);
},
);
ref.onDispose(() async {
tabStateDebouncer.dispose();
await tabAddedSub.cancel();
await tabContentSub.cancel();
await containerSiteAssignementSub.cancel();
});
}
}
@@ -0,0 +1,62 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'tab.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(TabRepository)
final tabRepositoryProvider = TabRepositoryProvider._();
final class TabRepositoryProvider
extends $NotifierProvider<TabRepository, void> {
TabRepositoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'tabRepositoryProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabRepositoryHash();
@$internal
@override
TabRepository create() => TabRepository();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(void value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<void>(value),
);
}
}
String _$tabRepositoryHash() => r'ce4ac2f2efbb859ba66e98c6e6d21f8205d9b4ed';
abstract class _$TabRepository extends $Notifier<void> {
void build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<void, void>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<void, void>,
void,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}