added support for translations and printing

This commit is contained in:
Fabian Freund
2026-03-05 07:29:15 +01:00
parent ce014e40bf
commit c3e39b3a1d
28 changed files with 3547 additions and 2837 deletions
@@ -27,6 +27,7 @@ import 'package:weblibre/features/geckoview/domain/entities/states/find_result.d
import 'package:weblibre/features/geckoview/domain/entities/states/history.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/readerable.dart';
import 'package:weblibre/features/geckoview/domain/entities/states/security.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'; import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
part 'tab.g.dart'; part 'tab.g.dart';
@@ -75,6 +76,7 @@ class TabState extends WebPageInfo {
final HistoryState historyState; final HistoryState historyState;
final ReaderableState readerableState; final ReaderableState readerableState;
final FindResultState findResultState; final FindResultState findResultState;
final TranslationState translationState;
TabState({ TabState({
required this.id, required this.id,
@@ -93,6 +95,7 @@ class TabState extends WebPageInfo {
required this.historyState, required this.historyState,
required this.readerableState, required this.readerableState,
required this.findResultState, required this.findResultState,
required this.translationState,
}) : super(title: title.trim()); }) : super(title: title.trim());
factory TabState.$default(String tabId) => TabState( factory TabState.$default(String tabId) => TabState(
@@ -111,6 +114,7 @@ class TabState extends WebPageInfo {
historyState: HistoryState.$default(), historyState: HistoryState.$default(),
readerableState: ReaderableState.$default(), readerableState: ReaderableState.$default(),
findResultState: FindResultState.$default(), findResultState: FindResultState.$default(),
translationState: TranslationState.$default(),
); );
@override @override
@@ -130,5 +134,6 @@ class TabState extends WebPageInfo {
historyState, historyState,
readerableState, readerableState,
findResultState, findResultState,
translationState,
]; ];
} }
@@ -37,6 +37,8 @@ abstract class _$TabStateCWProxy {
TabState findResultState(FindResultState findResultState); TabState findResultState(FindResultState findResultState);
TabState translationState(TranslationState translationState);
/// Creates a new instance with the provided field values. /// 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)`. /// 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)`.
/// ///
@@ -60,6 +62,7 @@ abstract class _$TabStateCWProxy {
HistoryState historyState, HistoryState historyState,
ReaderableState readerableState, ReaderableState readerableState,
FindResultState findResultState, FindResultState findResultState,
TranslationState translationState,
}); });
} }
@@ -120,6 +123,10 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
TabState findResultState(FindResultState findResultState) => TabState findResultState(FindResultState findResultState) =>
call(findResultState: findResultState); call(findResultState: findResultState);
@override
TabState translationState(TranslationState translationState) =>
call(translationState: translationState);
@override @override
/// Creates a new instance with the provided field values. /// 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)`. /// 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)`.
@@ -144,6 +151,7 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
Object? historyState = const $CopyWithPlaceholder(), Object? historyState = const $CopyWithPlaceholder(),
Object? readerableState = const $CopyWithPlaceholder(), Object? readerableState = const $CopyWithPlaceholder(),
Object? findResultState = const $CopyWithPlaceholder(), Object? findResultState = const $CopyWithPlaceholder(),
Object? translationState = const $CopyWithPlaceholder(),
}) { }) {
return TabState( return TabState(
id: _value.id, id: _value.id,
@@ -217,6 +225,12 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
? _value.findResultState ? _value.findResultState
// ignore: cast_nullable_to_non_nullable // ignore: cast_nullable_to_non_nullable
: findResultState as FindResultState, : findResultState as FindResultState,
translationState:
translationState == const $CopyWithPlaceholder() ||
translationState == null
? _value.translationState
// ignore: cast_nullable_to_non_nullable
: translationState as TranslationState,
); );
} }
} }
@@ -0,0 +1,85 @@
/*
* 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,
];
}
@@ -239,6 +239,27 @@ Stream<MlProgressData> mlProgressEvents(Ref ref) {
return service.mlProgressEvents; 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 /// Tracks active ML model downloads
@Riverpod() @Riverpod()
class MlDownloadState extends _$MlDownloadState { class MlDownloadState extends _$MlDownloadState {
@@ -389,6 +389,72 @@ final class MlProgressEventsProvider
String _$mlProgressEventsHash() => r'41c1e6aece7f9ee2bebe5d189c6bec753c2fefc8'; 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 /// Tracks active ML model downloads
@ProviderFor(MlDownloadState) @ProviderFor(MlDownloadState)
@@ -76,6 +76,26 @@ class TabSession extends _$TabSession {
return _sessionService.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();
}
@override @override
void build({required String? tabId}) { void build({required String? tabId}) {
_sessionService = (tabId != null) _sessionService = (tabId != null)
@@ -57,7 +57,7 @@ final class TabSessionProvider extends $NotifierProvider<TabSession, void> {
} }
} }
String _$tabSessionHash() => r'5be471574bb868889cc1eee37ae7e4570a9e21a4'; String _$tabSessionHash() => r'b9381c334672fd3f733a4cc86ce696fb7de6fa5d';
final class TabSessionFamily extends $Family final class TabSessionFamily extends $Family
with $ClassFamilyOverride<TabSession, void, void, void, String?> { with $ClassFamilyOverride<TabSession, void, void, void, String?> {
@@ -31,6 +31,7 @@ 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/readerable.dart';
import 'package:weblibre/features/geckoview/domain/entities/states/security.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/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.dart';
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.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/find_in_page/domain/repositories/find_in_page.dart';
@@ -203,6 +204,16 @@ class TabStates extends _$TabStates {
); );
} }
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) { void _onFindResultsChange(FindResultsEvent event) {
final FindResultsEvent(:tabId, :results) = event; final FindResultsEvent(:tabId, :results) = event;
final current = state[tabId] ?? TabState.$default(tabId); final current = state[tabId] ?? TabState.$default(tabId);
@@ -317,6 +328,18 @@ class TabStates extends _$TabStates {
); );
}, },
), ),
eventService.tabTranslationEvents.listen(
(event) {
_onTabTranslationStateChange(event);
},
onError: (Object error, StackTrace stackTrace) {
logger.e(
'Error in tab translation events',
error: error,
stackTrace: stackTrace,
);
},
),
]; ];
ref.listen( ref.listen(
@@ -331,6 +354,7 @@ class TabStates extends _$TabStates {
onSecurityInfoStateChange: true, onSecurityInfoStateChange: true,
onHistoryStateChange: true, onHistoryStateChange: true,
onFindResults: true, onFindResults: true,
onTranslationStateChange: true,
); );
} }
}, },
@@ -41,7 +41,7 @@ final class TabStatesProvider
} }
} }
String _$tabStatesHash() => r'0fd97193ff963006e1943436b25b025dd3885c65'; String _$tabStatesHash() => r'6fc9a91b1343f9ae0b442100bed20e42448afa37';
abstract class _$TabStates extends $Notifier<Map<String, TabState>> { abstract class _$TabStates extends $Notifier<Map<String, TabState>> {
Map<String, TabState> build(); Map<String, TabState> build();
@@ -49,6 +49,7 @@ import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/content_selection_dialog.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/content_selection_dialog.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/qr_code.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/qr_code.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/translation_bottom_sheet.dart';
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart'; import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart';
import 'package:weblibre/features/geckoview/features/open_link_tools/domain/services/url_cleaner_catalog_service.dart'; import 'package:weblibre/features/geckoview/features/open_link_tools/domain/services/url_cleaner_catalog_service.dart';
import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/hooks/url_cleaner_controller.dart'; import 'package:weblibre/features/geckoview/features/open_link_tools/presentation/hooks/url_cleaner_controller.dart';
@@ -395,6 +396,9 @@ class _PageActionsCard extends HookConsumerWidget {
}, },
), ),
// Translate Page
_TranslatePageTile(selectedTabId: selectedTabId),
// Add to Home Screen (conditional) // Add to Home Screen (conditional)
_AddToHomeScreenTile(selectedTabId: selectedTabId), _AddToHomeScreenTile(selectedTabId: selectedTabId),
], ],
@@ -571,6 +575,48 @@ class _AddToHomeScreenTile extends ConsumerWidget {
} }
} }
class _TranslatePageTile extends ConsumerWidget {
final String selectedTabId;
const _TranslatePageTile({required this.selectedTabId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final engineState = ref.watch(translationEngineStateProvider);
final translationState = ref.watch(
tabStateProvider(selectedTabId).select((s) => s?.translationState),
);
final readerActive = ref.watch(
tabStateProvider(selectedTabId)
.select((s) => s?.readerableState.active ?? false),
);
// Hide when reader mode is active (Fenix-aligned)
if (readerActive || engineState?.isEngineSupported != true) {
return const SizedBox.shrink();
}
final isTranslated = translationState?.isTranslated ?? false;
return ListTile(
leading: Icon(
Icons.translate,
color: isTranslated ? Theme.of(context).colorScheme.primary : null,
),
title: Text(isTranslated ? 'Translated' : 'Translate Page'),
onTap: () async {
Navigator.pop(context);
if (context.mounted) {
await showTranslationBottomSheet(
context,
selectedTabId: selectedTabId,
);
}
},
);
}
}
class _FetchFeedsTile extends HookConsumerWidget { class _FetchFeedsTile extends HookConsumerWidget {
final String selectedTabId; final String selectedTabId;
@@ -1385,6 +1431,24 @@ class _ExportExpansion extends ConsumerWidget {
if (context.mounted) Navigator.pop(context); if (context.mounted) Navigator.pop(context);
}, },
), ),
// Print
_buildSubTile(
'Print',
icon: MdiIcons.printer,
onTap: () async {
try {
await ref
.read(tabSessionProvider(tabId: selectedTabId).notifier)
.printContent();
} catch (e) {
if (context.mounted) {
ui_helper.showErrorMessage(context, 'Failed to print page');
}
}
if (context.mounted) Navigator.pop(context);
},
),
], ],
), ),
); );
@@ -109,6 +109,36 @@ class SaveToPdfMenuItemButton extends HookConsumerWidget {
} }
} }
class PrintMenuItemButton extends HookConsumerWidget {
const PrintMenuItemButton({super.key, required this.selectedTabId});
final String? selectedTabId;
@override
Widget build(BuildContext context, WidgetRef ref) {
return MenuItemButton(
leadingIcon: const Icon(MdiIcons.printer),
closeOnActivate: false,
onPressed: () async {
try {
await ref
.read(tabSessionProvider(tabId: selectedTabId).notifier)
.printContent();
} catch (e) {
if (context.mounted) {
ui_helper.showErrorMessage(context, 'Failed to print page');
}
}
if (context.mounted) {
MenuController.maybeOf(context)?.close();
}
},
child: const Text('Print'),
);
}
}
class ShareMarkdownActionMenuItemButton extends HookConsumerWidget { class ShareMarkdownActionMenuItemButton extends HookConsumerWidget {
const ShareMarkdownActionMenuItemButton({ const ShareMarkdownActionMenuItemButton({
super.key, super.key,
@@ -31,12 +31,14 @@ import 'package:weblibre/core/design/app_colors.dart';
import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart'; import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart';
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.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/desktop_mode.dart'; import 'package:weblibre/features/geckoview/domain/providers/desktop_mode.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.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/domain/repositories/tab.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/menu_item_buttons.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/menu_item_buttons.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/navigation_buttons.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/navigation_buttons.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/translation_bottom_sheet.dart';
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart'; import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart';
import 'package:weblibre/features/geckoview/features/pwa/domain/providers.dart'; import 'package:weblibre/features/geckoview/features/pwa/domain/providers.dart';
import 'package:weblibre/features/geckoview/features/pwa/presentation/widgets/pwa_install_button.dart'; import 'package:weblibre/features/geckoview/features/pwa/presentation/widgets/pwa_install_button.dart';
@@ -577,10 +579,53 @@ class TabMenu extends HookConsumerWidget {
}, },
), ),
SaveToPdfMenuItemButton(selectedTabId: selectedTabId), SaveToPdfMenuItemButton(selectedTabId: selectedTabId),
PrintMenuItemButton(selectedTabId: selectedTabId),
], ],
leadingIcon: const Icon(MdiIcons.fileExport), leadingIcon: const Icon(MdiIcons.fileExport),
child: const Text('Export'), child: const Text('Export'),
), ),
Consumer(
builder: (context, ref, child) {
final engineState = ref.watch(translationEngineStateProvider);
final translationState = ref.watch(
tabStateProvider(selectedTabId)
.select((s) => s?.translationState),
);
final readerActive = ref.watch(
tabStateProvider(selectedTabId)
.select((s) => s?.readerableState.active ?? false),
);
// Hide when reader mode is active (Fenix-aligned)
if (readerActive || engineState?.isEngineSupported != true) {
return const SizedBox.shrink();
}
final isTranslated = translationState?.isTranslated ?? false;
return MenuItemButton(
closeOnActivate: false,
leadingIcon: Icon(
Icons.translate,
color: isTranslated
? Theme.of(context).colorScheme.primary
: null,
),
onPressed: () async {
controller.close();
if (context.mounted) {
await showTranslationBottomSheet(
context,
selectedTabId: selectedTabId,
);
}
},
child: Text(
isTranslated ? 'Translated' : 'Translate Page',
),
);
},
),
if (enablePinTab) if (enablePinTab)
Consumer( Consumer(
builder: (context, childRef, child) { builder: (context, childRef, child) {
@@ -0,0 +1,309 @@
/*
* 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/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/core/logger.dart';
import 'package:weblibre/features/geckoview/domain/providers.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
enum _TranslatePhase { idle, submitting, processing }
Future<void> showTranslationBottomSheet(
BuildContext context, {
required String selectedTabId,
}) {
unawaited(
GeckoTabService().syncEvents(onTranslationStateChange: true).catchError((
Object e,
StackTrace s,
) {
logger.w(
'Failed to refresh translation state before opening sheet',
error: e,
stackTrace: s,
);
}),
);
return showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => TranslationBottomSheet(selectedTabId: selectedTabId),
);
}
class TranslationBottomSheet extends HookConsumerWidget {
final String selectedTabId;
const TranslationBottomSheet({super.key, required this.selectedTabId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
final tabState = ref.watch(tabStateProvider(selectedTabId));
final translationState = tabState?.translationState;
final engineState = ref.watch(translationEngineStateProvider);
final fromLanguages = engineState?.fromLanguages?.nonNulls.toList() ?? [];
final toLanguages = engineState?.toLanguages?.nonNulls.toList() ?? [];
final suggestedFrom =
translationState?.detectedLanguageCode ??
translationState?.requestedFromLanguage;
final suggestedTo =
translationState?.userPreferredLanguageCode ??
translationState?.requestedToLanguage;
final selectedFrom = useState<String?>(suggestedFrom);
final selectedTo = useState<String?>(suggestedTo);
useEffect(() {
if (selectedFrom.value == null && suggestedFrom != null) {
selectedFrom.value = suggestedFrom;
}
if (selectedTo.value == null && suggestedTo != null) {
selectedTo.value = suggestedTo;
}
return null;
}, [suggestedFrom, suggestedTo]);
final isTranslated = translationState?.isTranslated ?? false;
final isProcessing = translationState?.isTranslateProcessing ?? false;
final hasError = translationState?.hasError ?? false;
final errorName = translationState?.translationErrorName;
final translatePhase = useState(_TranslatePhase.idle);
final effectiveProcessing =
isProcessing || translatePhase.value != _TranslatePhase.idle;
useEffect(() {
switch (translatePhase.value) {
case _TranslatePhase.idle:
return null;
case _TranslatePhase.submitting:
if (isProcessing) {
translatePhase.value = _TranslatePhase.processing;
} else if (hasError) {
translatePhase.value = _TranslatePhase.idle;
}
return null;
case _TranslatePhase.processing:
if (!isProcessing && (isTranslated || hasError)) {
translatePhase.value = _TranslatePhase.idle;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (context.mounted) Navigator.pop(context);
});
}
// Otherwise stay in processing — transient stop during retranslate.
return null;
}
}, [isProcessing, isTranslated, hasError, translatePhase.value]);
final canTranslate =
selectedFrom.value != null &&
selectedTo.value != null &&
selectedFrom.value != selectedTo.value &&
!effectiveProcessing;
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header
Row(
children: [
Icon(Icons.translate, color: colorScheme.primary),
const SizedBox(width: 8),
Text(
'Translate Page',
style: Theme.of(context).textTheme.titleMedium,
),
const Spacer(),
if (effectiveProcessing)
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
],
),
const SizedBox(height: 16),
// From language dropdown
_LanguageDropdown(
label: 'From',
languages: fromLanguages,
selectedCode: selectedFrom.value,
onChanged: effectiveProcessing
? null
: (code) => selectedFrom.value = code,
),
const SizedBox(height: 12),
// To language dropdown
_LanguageDropdown(
label: 'To',
languages: toLanguages,
selectedCode: selectedTo.value,
onChanged: effectiveProcessing
? null
: (code) => selectedTo.value = code,
),
const SizedBox(height: 16),
// Error display
if (hasError && errorName != null)
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Translation error: $errorName',
style: TextStyle(color: colorScheme.onErrorContainer),
),
),
),
// Action buttons
Row(
children: [
if (isTranslated)
Expanded(
child: OutlinedButton(
onPressed: effectiveProcessing
? null
: () async {
try {
await ref
.read(
tabSessionProvider(
tabId: selectedTabId,
).notifier,
)
.translateRestore();
} catch (e) {
if (context.mounted) {
ui_helper.showErrorMessage(
context,
'Failed to restore page',
);
}
}
if (context.mounted) Navigator.pop(context);
},
child: const Text('Show Original'),
),
),
if (isTranslated) const SizedBox(width: 12),
Expanded(
child: FilledButton(
onPressed: canTranslate
? () async {
translatePhase.value = _TranslatePhase.submitting;
try {
await ref
.read(
tabSessionProvider(
tabId: selectedTabId,
).notifier,
)
.translate(
fromLanguage: selectedFrom.value!,
toLanguage: selectedTo.value!,
);
} catch (e) {
translatePhase.value = _TranslatePhase.idle;
if (context.mounted) {
ui_helper.showErrorMessage(
context,
'Failed to translate page',
);
}
}
}
: null,
child: Text(isTranslated ? 'Retranslate' : 'Translate'),
),
),
],
),
],
),
),
);
}
}
class _LanguageDropdown extends StatelessWidget {
final String label;
final List<TranslationLanguage> languages;
final String? selectedCode;
final ValueChanged<String?>? onChanged;
const _LanguageDropdown({
required this.label,
required this.languages,
required this.selectedCode,
this.onChanged,
});
@override
Widget build(BuildContext context) {
// Ensure selectedCode exists in the list, otherwise use null
final effectiveSelection = languages.any((l) => l.code == selectedCode)
? selectedCode
: null;
return DropdownButtonFormField<String>(
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
initialValue: effectiveSelection,
isExpanded: true,
items: languages
.map(
(lang) => DropdownMenuItem(
value: lang.code,
child: Text(lang.localizedDisplayName),
),
)
.toList(),
onChanged: onChanged,
);
}
}
@@ -227,8 +227,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
val engineView = createEngine(components) val engineView = createEngine(components)
fragmentEngineView = engineView // Track for lifecycle management fragmentEngineView = engineView // Track for lifecycle management
val originalContext = ActivityContextWrapper.getOriginalContext(requireActivity()) val originalContext =
?.let { ProfileContext(it, components.profileApplicationContext.relativePath) } ActivityContextWrapper.getOriginalContext(requireActivity()) ?: requireActivity()
val engineNativeView = engineView.asView() val engineNativeView = engineView.asView()
engineNativeView.layoutParams = layoutParams engineNativeView.layoutParams = layoutParams
@@ -27,6 +27,9 @@ import eu.weblibre.flutter_mozilla_components.pigeons.ReaderableState
import eu.weblibre.flutter_mozilla_components.pigeons.RestoreLocation import eu.weblibre.flutter_mozilla_components.pigeons.RestoreLocation
import eu.weblibre.flutter_mozilla_components.pigeons.SecurityInfoState import eu.weblibre.flutter_mozilla_components.pigeons.SecurityInfoState
import eu.weblibre.flutter_mozilla_components.pigeons.TabContentState import eu.weblibre.flutter_mozilla_components.pigeons.TabContentState
import eu.weblibre.flutter_mozilla_components.pigeons.TabTranslationStateData
import eu.weblibre.flutter_mozilla_components.pigeons.TranslationEngineStateData
import eu.weblibre.flutter_mozilla_components.pigeons.TranslationLanguage
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
@@ -39,6 +42,7 @@ import mozilla.components.browser.icons.IconRequest
import mozilla.components.browser.session.storage.RecoverableBrowserState import mozilla.components.browser.session.storage.RecoverableBrowserState
import mozilla.components.browser.state.action.EngineAction import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.action.TabListAction import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.action.TranslationsAction
import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.LastMediaAccessState import mozilla.components.browser.state.state.LastMediaAccessState
@@ -55,6 +59,7 @@ import org.json.JSONObject
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
import mozilla.components.feature.addons.logger import mozilla.components.feature.addons.logger
import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.translate.TranslationOperation
class GeckoTabsApiImpl : GeckoTabsApi { class GeckoTabsApiImpl : GeckoTabsApi {
companion object { companion object {
@@ -185,6 +190,7 @@ class GeckoTabsApiImpl : GeckoTabsApi {
onPageExtensionsChange: Boolean, onPageExtensionsChange: Boolean,
onBrowserExtensionIcons: Boolean, onBrowserExtensionIcons: Boolean,
onPageExtensionIcons: Boolean, onPageExtensionIcons: Boolean,
onTranslationStateChange: Boolean,
) { ) {
try { try {
val tabs = components.core.store.state.tabs.map { it.copy() } val tabs = components.core.store.state.tabs.map { it.copy() }
@@ -293,11 +299,85 @@ class GeckoTabsApiImpl : GeckoTabsApi {
onPageExtensionIcons onPageExtensionIcons
) )
} }
// Sync translation events
if (onTranslationStateChange) {
syncTranslationEvents(tabs)
}
} catch (e: Exception) { } catch (e: Exception) {
logger.error("$TAG: Failed to sync events", e) logger.error("$TAG: Failed to sync events", e)
} }
} }
private fun syncTranslationEvents(tabs: List<mozilla.components.browser.state.state.TabSessionState>) {
try {
val browserState = components.core.store.state
// Retry initialization when support status is still unknown.
if (browserState.translationEngine.isEngineSupported == null) {
components.core.store.dispatch(TranslationsAction.InitTranslationsBrowserState)
}
// Fetch supported languages when engine is supported but languages are still missing.
if (
browserState.translationEngine.isEngineSupported == true &&
(
browserState.translationEngine.supportedLanguages?.fromLanguages == null ||
browserState.translationEngine.supportedLanguages?.toLanguages == null
)
) {
components.core.store.dispatch(
TranslationsAction.OperationRequestedAction(
tabId = browserState.selectedTabId,
operation = TranslationOperation.FETCH_SUPPORTED_LANGUAGES,
)
)
}
val translationEngine = browserState.translationEngine
// Emit browser-level engine state
val fromLanguages = translationEngine.supportedLanguages?.fromLanguages?.map { lang ->
TranslationLanguage(code = lang.code, localizedDisplayName = lang.localizedDisplayName ?: lang.code)
}
val toLanguages = translationEngine.supportedLanguages?.toLanguages?.map { lang ->
TranslationLanguage(code = lang.code, localizedDisplayName = lang.localizedDisplayName ?: lang.code)
}
components.flutterEvents.onTranslationEngineStateChange(
EventSequence.next(),
TranslationEngineStateData(
isEngineSupported = translationEngine.isEngineSupported,
fromLanguages = fromLanguages,
toLanguages = toLanguages,
)
) { }
// Emit per-tab translation state
tabs.forEach { tab ->
val translationsState = tab.translationsState
components.flutterEvents.onTabTranslationStateChange(
EventSequence.next(),
TabTranslationStateData(
tabId = tab.id,
isTranslated = translationsState.isTranslated,
isTranslateProcessing = translationsState.isTranslateProcessing,
isOfferTranslate = translationsState.isOfferTranslate,
isExpectedTranslate = translationsState.isExpectedTranslate,
detectedLanguageCode = translationsState.translationEngineState?.detectedLanguages?.documentLangTag,
userPreferredLanguageCode = translationsState.translationEngineState?.detectedLanguages?.userPreferredLangTag,
requestedFromLanguage = translationsState.translationEngineState?.requestedTranslationPair?.fromLanguage,
requestedToLanguage = translationsState.translationEngineState?.requestedTranslationPair?.toLanguage,
translationErrorName = translationsState.translationError?.errorName,
displayError = translationsState.translationError?.displayError,
)
) { }
}
} catch (e: Exception) {
logger.error("$TAG: Failed to sync translation events", e)
}
}
private fun syncExtensionEvents( private fun syncExtensionEvents(
onBrowserExtensionsChange: Boolean, onBrowserExtensionsChange: Boolean,
onPageExtensionsChange: Boolean, onPageExtensionsChange: Boolean,
@@ -30,6 +30,7 @@ import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.session.storage.SessionStorage import mozilla.components.browser.session.storage.SessionStorage
import mozilla.components.browser.state.engine.EngineMiddleware import mozilla.components.browser.state.engine.EngineMiddleware
import mozilla.components.browser.state.engine.middleware.SessionPrioritizationMiddleware import mozilla.components.browser.state.engine.middleware.SessionPrioritizationMiddleware
import mozilla.components.browser.state.engine.middleware.TranslationsMiddleware
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
import mozilla.components.browser.storage.sync.PlacesHistoryStorage import mozilla.components.browser.storage.sync.PlacesHistoryStorage
@@ -67,6 +68,7 @@ import mozilla.components.concept.base.crash.Breadcrumb
import mozilla.components.concept.base.crash.CrashReporting import mozilla.components.concept.base.crash.CrashReporting
import mozilla.components.support.base.worker.Frequency import mozilla.components.support.base.worker.Frequency
import org.mozilla.geckoview.GeckoRuntime import org.mozilla.geckoview.GeckoRuntime
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -207,6 +209,9 @@ class Core(
SaveToPDFMiddleware(), SaveToPDFMiddleware(),
FileUploadsDirCleanerMiddleware(fileUploadsDirCleaner), FileUploadsDirCleanerMiddleware(fileUploadsDirCleaner),
LastMediaAccessMiddleware(), LastMediaAccessMiddleware(),
// Keep parity with Fenix: do not initialize translations on store init.
// EngineObserver will dispatch InitTranslationsBrowserState after first completed page load.
TranslationsMiddleware(engine, MainScope(), false),
) + EngineMiddleware.create( ) + EngineMiddleware.create(
engine, engine,
// We are disabling automatic suspending of engine sessions under memory pressure. // We are disabling automatic suspending of engine sessions under memory pressure.
@@ -23,6 +23,9 @@ import eu.weblibre.flutter_mozilla_components.pigeons.ShareTarget
import eu.weblibre.flutter_mozilla_components.pigeons.ShareTargetFiles import eu.weblibre.flutter_mozilla_components.pigeons.ShareTargetFiles
import eu.weblibre.flutter_mozilla_components.pigeons.ShareTargetParams import eu.weblibre.flutter_mozilla_components.pigeons.ShareTargetParams
import eu.weblibre.flutter_mozilla_components.pigeons.TabContentState import eu.weblibre.flutter_mozilla_components.pigeons.TabContentState
import eu.weblibre.flutter_mozilla_components.pigeons.TabTranslationStateData
import eu.weblibre.flutter_mozilla_components.pigeons.TranslationEngineStateData
import eu.weblibre.flutter_mozilla_components.pigeons.TranslationLanguage
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
@@ -222,6 +225,71 @@ class Events(
} }
} }
// Translation engine state (browser-level)
stateFlow.flowScoped { flow ->
flow.map { state -> state.translationEngine }
.distinctUntilChanged { old, new ->
old.isEngineSupported == new.isEngineSupported &&
old.supportedLanguages == new.supportedLanguages
}
.debounce(50)
.collect { translationEngine ->
val fromLanguages = translationEngine.supportedLanguages?.fromLanguages?.map { lang ->
TranslationLanguage(code = lang.code, localizedDisplayName = lang.localizedDisplayName ?: lang.code)
}
val toLanguages = translationEngine.supportedLanguages?.toLanguages?.map { lang ->
TranslationLanguage(code = lang.code, localizedDisplayName = lang.localizedDisplayName ?: lang.code)
}
flutterEvents.onTranslationEngineStateChange(
EventSequence.next(),
TranslationEngineStateData(
isEngineSupported = translationEngine.isEngineSupported,
fromLanguages = fromLanguages,
toLanguages = toLanguages,
)
) { _ -> }
}
}
// Per-tab translation state
stateFlow.flowScoped { flow ->
flow.mapNotNull { state -> state.tabs }
.filterChanged {
it.translationsState
}
.ifAnyChanged {
arrayOf(
it.translationsState.isTranslated,
it.translationsState.isTranslateProcessing,
it.translationsState.isOfferTranslate,
it.translationsState.isExpectedTranslate,
it.translationsState.translationEngineState,
it.translationsState.translationError,
)
}
.debounce(25)
.collect { tab ->
val ts = tab.translationsState
flutterEvents.onTabTranslationStateChange(
EventSequence.next(),
TabTranslationStateData(
tabId = tab.id,
isTranslated = ts.isTranslated,
isTranslateProcessing = ts.isTranslateProcessing,
isOfferTranslate = ts.isOfferTranslate,
isExpectedTranslate = ts.isExpectedTranslate,
detectedLanguageCode = ts.translationEngineState?.detectedLanguages?.documentLangTag,
userPreferredLanguageCode = ts.translationEngineState?.detectedLanguages?.userPreferredLangTag,
requestedFromLanguage = ts.translationEngineState?.requestedTranslationPair?.fromLanguage,
requestedToLanguage = ts.translationEngineState?.requestedTranslationPair?.toLanguage,
translationErrorName = ts.translationError?.errorName,
displayError = ts.translationError?.displayError,
)
) { _ -> }
}
}
// PWA manifest availability events - following Fenix MenuPresenter pattern // PWA manifest availability events - following Fenix MenuPresenter pattern
stateFlow.flowScoped { flow -> stateFlow.flowScoped { flow ->
flow.mapNotNull { state -> state.selectedTab } flow.mapNotNull { state -> state.selectedTab }
@@ -24,6 +24,15 @@ class SaveToPDFMiddleware : Middleware<BrowserState, BrowserAction> {
is EngineAction.SaveToPdfExceptionAction -> { is EngineAction.SaveToPdfExceptionAction -> {
logger.error("Unable to save PDF", action.throwable) logger.error("Unable to save PDF", action.throwable)
} }
is EngineAction.PrintContentAction -> {
next(action)
}
is EngineAction.PrintContentCompletedAction -> {
logger.info("Print completed successfully")
}
is EngineAction.PrintContentExceptionAction -> {
logger.error("Unable to print content", action.throwable)
}
else -> { else -> {
next(action) next(action)
} }
@@ -94,9 +94,15 @@ export 'src/pigeons/gecko.g.dart'
SyncRemoteTab, SyncRemoteTab,
TabContent, TabContent,
TabContentState, TabContentState,
TabTranslationStateData,
TrackingProtectionException, TrackingProtectionException,
TrackingProtectionPolicy, TrackingProtectionPolicy,
TrackingScope, TrackingScope,
TranslationDetectedLanguages,
TranslationEngineStateData,
TranslationLanguage,
TranslationOptions,
TranslationPair,
UnknownHitResult, UnknownHitResult,
VideoHitResult, VideoHitResult,
VisitInfo, VisitInfo,
@@ -21,6 +21,8 @@ typedef FindResultsEvent = ({String tabId, List<FindResultState> results});
typedef LongPressEvent = ({String tabId, HitResult hitResult}); typedef LongPressEvent = ({String tabId, HitResult hitResult});
typedef ScrollEvent = ({String tabId, int scrollY}); typedef ScrollEvent = ({String tabId, int scrollY});
typedef ManifestUpdateEvent = ({String tabId, PwaManifest? manifest}); typedef ManifestUpdateEvent = ({String tabId, PwaManifest? manifest});
typedef TabTranslationEvent = ({String tabId, TabTranslationStateData state});
typedef TranslationEngineEvent = TranslationEngineStateData;
class GeckoEventService extends GeckoStateEvents { class GeckoEventService extends GeckoStateEvents {
// Stream controllers // Stream controllers
@@ -46,6 +48,9 @@ class GeckoEventService extends GeckoStateEvents {
final _tabAddedSubject = PublishSubject<String>(); final _tabAddedSubject = PublishSubject<String>();
final _mlProgressSubject = PublishSubject<MlProgressData>(); final _mlProgressSubject = PublishSubject<MlProgressData>();
final _manifestUpdateSubject = PublishSubject<ManifestUpdateEvent>(); final _manifestUpdateSubject = PublishSubject<ManifestUpdateEvent>();
final _translationEngineSubject =
BehaviorSubject<TranslationEngineEvent>();
final _tabTranslationSubject = ReplaySubject<TabTranslationEvent>();
// Event streams // Event streams
ValueStream<bool> get viewReadyStateEvents => _viewStateSubject.stream; ValueStream<bool> get viewReadyStateEvents => _viewStateSubject.stream;
@@ -72,6 +77,10 @@ class GeckoEventService extends GeckoStateEvents {
Stream<MlProgressData> get mlProgressEvents => _mlProgressSubject.stream; Stream<MlProgressData> get mlProgressEvents => _mlProgressSubject.stream;
Stream<ManifestUpdateEvent> get manifestUpdateEvents => Stream<ManifestUpdateEvent> get manifestUpdateEvents =>
_manifestUpdateSubject.stream; _manifestUpdateSubject.stream;
ValueStream<TranslationEngineEvent> get translationEngineEvents =>
_translationEngineSubject.stream;
Stream<TabTranslationEvent> get tabTranslationEvents =>
_tabTranslationSubject.stream;
@override @override
void onViewReadyStateChange(int sequence, bool state) { void onViewReadyStateChange(int sequence, bool state) {
@@ -210,6 +219,25 @@ class GeckoEventService extends GeckoStateEvents {
)); ));
} }
@override
void onTranslationEngineStateChange(
int sequence,
TranslationEngineStateData state,
) {
_translationEngineSubject.addWhenMoreRecent(sequence, null, state);
}
@override
void onTabTranslationStateChange(
int sequence,
TabTranslationStateData state,
) {
_tabTranslationSubject.addWhenMoreRecent(sequence, state.tabId, (
tabId: state.tabId,
state: state,
));
}
GeckoEventService.setUp({ GeckoEventService.setUp({
BinaryMessenger? binaryMessenger, BinaryMessenger? binaryMessenger,
String messageChannelSuffix = '', String messageChannelSuffix = '',
@@ -241,5 +269,7 @@ class GeckoEventService extends GeckoStateEvents {
await _siteAssignementSubject.close(); await _siteAssignementSubject.close();
await _mlProgressSubject.close(); await _mlProgressSubject.close();
await _manifestUpdateSubject.close(); await _manifestUpdateSubject.close();
await _translationEngineSubject.close();
await _tabTranslationSubject.close();
} }
} }
@@ -29,6 +29,7 @@ class GeckoTabService {
bool onPageExtensionsChange = false, bool onPageExtensionsChange = false,
bool onBrowserExtensionIcons = false, bool onBrowserExtensionIcons = false,
bool onPageExtensionIcons = false, bool onPageExtensionIcons = false,
bool onTranslationStateChange = false,
}) { }) {
return _api.syncEvents( return _api.syncEvents(
onSelectedTabChange: onSelectedTabChange, onSelectedTabChange: onSelectedTabChange,
@@ -44,6 +45,7 @@ class GeckoTabService {
onPageExtensionsChange: onPageExtensionsChange, onPageExtensionsChange: onPageExtensionsChange,
onBrowserExtensionIcons: onBrowserExtensionIcons, onBrowserExtensionIcons: onBrowserExtensionIcons,
onPageExtensionIcons: onPageExtensionIcons, onPageExtensionIcons: onPageExtensionIcons,
onTranslationStateChange: onTranslationStateChange,
); );
} }
File diff suppressed because it is too large Load Diff
@@ -16,6 +16,77 @@ class TranslationOptions {
TranslationOptions({this.downloadModel = true}); TranslationOptions({this.downloadModel = true});
} }
/// A language supported by the translation engine.
class TranslationLanguage {
final String code;
final String localizedDisplayName;
TranslationLanguage({required this.code, required this.localizedDisplayName});
}
/// Detected languages for a page.
class TranslationDetectedLanguages {
final String? documentLangTag;
final bool? supportedDocumentLang;
final String? userPreferredLangTag;
TranslationDetectedLanguages({
this.documentLangTag,
this.supportedDocumentLang,
this.userPreferredLangTag,
});
}
/// A from/to language pair for translation.
class TranslationPair {
final String fromLanguage;
final String toLanguage;
TranslationPair({required this.fromLanguage, required this.toLanguage});
}
/// Browser-level translation engine state (global).
class TranslationEngineStateData {
final bool? isEngineSupported;
final List<TranslationLanguage?>? fromLanguages;
final List<TranslationLanguage?>? toLanguages;
TranslationEngineStateData({
this.isEngineSupported,
this.fromLanguages,
this.toLanguages,
});
}
/// Per-tab translation state.
class TabTranslationStateData {
final String tabId;
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;
TabTranslationStateData({
required this.tabId,
required this.isTranslated,
required this.isTranslateProcessing,
required this.isOfferTranslate,
required this.isExpectedTranslate,
this.detectedLanguageCode,
this.userPreferredLanguageCode,
this.requestedFromLanguage,
this.requestedToLanguage,
this.translationErrorName,
this.displayError,
});
}
/// Value type that represents the state of reader mode/view. /// Value type that represents the state of reader mode/view.
class ReaderState { class ReaderState {
/// Whether or not the current page can be transformed to /// Whether or not the current page can be transformed to
@@ -1256,6 +1327,7 @@ abstract class GeckoTabsApi {
required bool onPageExtensionsChange, required bool onPageExtensionsChange,
required bool onBrowserExtensionIcons, required bool onBrowserExtensionIcons,
required bool onPageExtensionIcons, required bool onPageExtensionIcons,
required bool onTranslationStateChange,
}); });
void selectTab({required String tabId}); void selectTab({required String tabId});
@@ -1562,6 +1634,15 @@ abstract class GeckoStateEvents {
void onMlProgress(int sequence, MlProgressData progress); void onMlProgress(int sequence, MlProgressData progress);
void onManifestUpdate(int sequence, String tabId, PwaManifest? manifest); void onManifestUpdate(int sequence, String tabId, PwaManifest? manifest);
void onTranslationEngineStateChange(
int sequence,
TranslationEngineStateData state,
);
void onTabTranslationStateChange(
int sequence,
TabTranslationStateData state,
);
} }
@FlutterApi() @FlutterApi()
+62 -112
View File
@@ -15,11 +15,7 @@ PlatformException _createConnectionError(String channelName) {
); );
} }
List<Object?> wrapResponse({ List<Object?> wrapResponse({Object? result, PlatformException? error, bool empty = false}) {
Object? result,
PlatformException? error,
bool empty = false,
}) {
if (empty) { if (empty) {
return <Object?>[]; return <Object?>[];
} }
@@ -28,48 +24,37 @@ List<Object?> wrapResponse({
} }
return <Object?>[error.code, error.message, error.details]; return <Object?>[error.code, error.message, error.details];
} }
bool _deepEquals(Object? a, Object? b) { bool _deepEquals(Object? a, Object? b) {
if (a is List && b is List) { if (a is List && b is List) {
return a.length == b.length && return a.length == b.length &&
a.indexed.every( a.indexed
((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]), .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
);
} }
if (a is Map && b is Map) { if (a is Map && b is Map) {
return a.length == b.length && return a.length == b.length && a.entries.every((MapEntry<Object?, Object?> entry) =>
a.entries.every( (b as Map<Object?, Object?>).containsKey(entry.key) &&
(MapEntry<Object?, Object?> entry) => _deepEquals(entry.value, b[entry.key]));
(b as Map<Object?, Object?>).containsKey(entry.key) &&
_deepEquals(entry.value, b[entry.key]),
);
} }
return a == b; return a == b;
} }
/// Transport types for Tor connections /// Transport types for Tor connections
enum TransportType { enum TransportType {
/// Direct Tor connection (no bridges) /// Direct Tor connection (no bridges)
none, none,
/// obfs4 pluggable transport /// obfs4 pluggable transport
obfs4, obfs4,
/// Snowflake pluggable transport (default broker) /// Snowflake pluggable transport (default broker)
snowflake, snowflake,
/// Snowflake via AMP cache /// Snowflake via AMP cache
snowflakeAmp, snowflakeAmp,
/// Meek pluggable transport /// Meek pluggable transport
meek, meek,
/// Meek via Azure CDN /// Meek via Azure CDN
meekAzure, meekAzure,
/// WebTunnel pluggable transport /// WebTunnel pluggable transport
webtunnel, webtunnel,
/// Custom bridge lines (passthrough) /// Custom bridge lines (passthrough)
custom, custom,
} }
@@ -110,8 +95,7 @@ class TorConfiguration {
} }
Object encode() { Object encode() {
return _toList(); return _toList(); }
}
static TorConfiguration decode(Object result) { static TorConfiguration decode(Object result) {
result as List<Object?>; result as List<Object?>;
@@ -138,7 +122,8 @@ class TorConfiguration {
@override @override
// ignore: avoid_equals_and_hash_code_on_mutable_classes // ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => Object.hashAll(_toList()); int get hashCode => Object.hashAll(_toList())
;
} }
/// Current Tor status /// Current Tor status
@@ -177,8 +162,7 @@ class TorStatus {
} }
Object encode() { Object encode() {
return _toList(); return _toList(); }
}
static TorStatus decode(Object result) { static TorStatus decode(Object result) {
result as List<Object?>; result as List<Object?>;
@@ -205,7 +189,8 @@ class TorStatus {
@override @override
// ignore: avoid_equals_and_hash_code_on_mutable_classes // ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => Object.hashAll(_toList()); int get hashCode => Object.hashAll(_toList())
;
} }
/// Log message from Tor /// Log message from Tor
@@ -226,12 +211,15 @@ class TorLogMessage {
int timestamp; int timestamp;
List<Object?> _toList() { List<Object?> _toList() {
return <Object?>[severity, message, timestamp]; return <Object?>[
severity,
message,
timestamp,
];
} }
Object encode() { Object encode() {
return _toList(); return _toList(); }
}
static TorLogMessage decode(Object result) { static TorLogMessage decode(Object result) {
result as List<Object?>; result as List<Object?>;
@@ -256,9 +244,11 @@ class TorLogMessage {
@override @override
// ignore: avoid_equals_and_hash_code_on_mutable_classes // ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => Object.hashAll(_toList()); int get hashCode => Object.hashAll(_toList())
;
} }
class _PigeonCodec extends StandardMessageCodec { class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec(); const _PigeonCodec();
@override @override
@@ -266,16 +256,16 @@ class _PigeonCodec extends StandardMessageCodec {
if (value is int) { if (value is int) {
buffer.putUint8(4); buffer.putUint8(4);
buffer.putInt64(value); buffer.putInt64(value);
} else if (value is TransportType) { } else if (value is TransportType) {
buffer.putUint8(129); buffer.putUint8(129);
writeValue(buffer, value.index); writeValue(buffer, value.index);
} else if (value is TorConfiguration) { } else if (value is TorConfiguration) {
buffer.putUint8(130); buffer.putUint8(130);
writeValue(buffer, value.encode()); writeValue(buffer, value.encode());
} else if (value is TorStatus) { } else if (value is TorStatus) {
buffer.putUint8(131); buffer.putUint8(131);
writeValue(buffer, value.encode()); writeValue(buffer, value.encode());
} else if (value is TorLogMessage) { } else if (value is TorLogMessage) {
buffer.putUint8(132); buffer.putUint8(132);
writeValue(buffer, value.encode()); writeValue(buffer, value.encode());
} else { } else {
@@ -286,14 +276,14 @@ class _PigeonCodec extends StandardMessageCodec {
@override @override
Object? readValueOfType(int type, ReadBuffer buffer) { Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) { switch (type) {
case 129: case 129:
final value = readValue(buffer) as int?; final value = readValue(buffer) as int?;
return value == null ? null : TransportType.values[value]; return value == null ? null : TransportType.values[value];
case 130: case 130:
return TorConfiguration.decode(readValue(buffer)!); return TorConfiguration.decode(readValue(buffer)!);
case 131: case 131:
return TorStatus.decode(readValue(buffer)!); return TorStatus.decode(readValue(buffer)!);
case 132: case 132:
return TorLogMessage.decode(readValue(buffer)!); return TorLogMessage.decode(readValue(buffer)!);
default: default:
return super.readValueOfType(type, buffer); return super.readValueOfType(type, buffer);
@@ -307,10 +297,8 @@ class TorApi {
/// available for dependency injection. If it is left null, the default /// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform. /// BinaryMessenger will be used which routes to the host platform.
TorApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) TorApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
: pigeonVar_binaryMessenger = binaryMessenger, : pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
? '.$messageChannelSuffix'
: '';
final BinaryMessenger? pigeonVar_binaryMessenger; final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec(); static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
@@ -320,16 +308,13 @@ class TorApi {
/// Start Tor with the given configuration /// Start Tor with the given configuration
/// Returns a Future to avoid blocking the main thread /// Returns a Future to avoid blocking the main thread
Future<int> startTor(TorConfiguration config) async { Future<int> startTor(TorConfiguration config) async {
final pigeonVar_channelName = final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.TorApi.startTor$pigeonVar_messageChannelSuffix';
'dev.flutter.pigeon.flutter_tor.TorApi.startTor$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>( final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName, pigeonVar_channelName,
pigeonChannelCodec, pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger, binaryMessenger: pigeonVar_binaryMessenger,
); );
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send( final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[config]);
<Object?>[config],
);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?; final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) { if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName); throw _createConnectionError(pigeonVar_channelName);
@@ -351,8 +336,7 @@ class TorApi {
/// Stop Tor /// Stop Tor
Future<void> stopTor() async { Future<void> stopTor() async {
final pigeonVar_channelName = final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.TorApi.stopTor$pigeonVar_messageChannelSuffix';
'dev.flutter.pigeon.flutter_tor.TorApi.stopTor$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>( final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName, pigeonVar_channelName,
pigeonChannelCodec, pigeonChannelCodec,
@@ -375,8 +359,7 @@ class TorApi {
/// Get current status /// Get current status
Future<TorStatus> getStatus() async { Future<TorStatus> getStatus() async {
final pigeonVar_channelName = final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.TorApi.getStatus$pigeonVar_messageChannelSuffix';
'dev.flutter.pigeon.flutter_tor.TorApi.getStatus$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>( final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName, pigeonVar_channelName,
pigeonChannelCodec, pigeonChannelCodec,
@@ -404,8 +387,7 @@ class TorApi {
/// Request a new Tor identity (new circuit) /// Request a new Tor identity (new circuit)
Future<void> requestNewIdentity() async { Future<void> requestNewIdentity() async {
final pigeonVar_channelName = final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.TorApi.requestNewIdentity$pigeonVar_messageChannelSuffix';
'dev.flutter.pigeon.flutter_tor.TorApi.requestNewIdentity$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>( final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName, pigeonVar_channelName,
pigeonChannelCodec, pigeonChannelCodec,
@@ -437,76 +419,54 @@ abstract class TorLogApi {
/// Called when status changes /// Called when status changes
void onStatusChanged(TorStatus status); void onStatusChanged(TorStatus status);
static void setUp( static void setUp(TorLogApi? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
TorLogApi? api, { messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
BinaryMessenger? binaryMessenger,
String messageChannelSuffix = '',
}) {
messageChannelSuffix = messageChannelSuffix.isNotEmpty
? '.$messageChannelSuffix'
: '';
{ {
final pigeonVar_channel = BasicMessageChannel<Object?>( final pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.flutter_tor.TorLogApi.onLogMessage$messageChannelSuffix', 'dev.flutter.pigeon.flutter_tor.TorLogApi.onLogMessage$messageChannelSuffix', pigeonChannelCodec,
pigeonChannelCodec, binaryMessenger: binaryMessenger);
binaryMessenger: binaryMessenger,
);
if (api == null) { if (api == null) {
pigeonVar_channel.setMessageHandler(null); pigeonVar_channel.setMessageHandler(null);
} else { } else {
pigeonVar_channel.setMessageHandler((Object? message) async { pigeonVar_channel.setMessageHandler((Object? message) async {
assert( assert(message != null,
message != null, 'Argument for dev.flutter.pigeon.flutter_tor.TorLogApi.onLogMessage was null.');
'Argument for dev.flutter.pigeon.flutter_tor.TorLogApi.onLogMessage was null.',
);
final List<Object?> args = (message as List<Object?>?)!; final List<Object?> args = (message as List<Object?>?)!;
final TorLogMessage? arg_log = (args[0] as TorLogMessage?); final TorLogMessage? arg_log = (args[0] as TorLogMessage?);
assert( assert(arg_log != null,
arg_log != null, 'Argument for dev.flutter.pigeon.flutter_tor.TorLogApi.onLogMessage was null, expected non-null TorLogMessage.');
'Argument for dev.flutter.pigeon.flutter_tor.TorLogApi.onLogMessage was null, expected non-null TorLogMessage.',
);
try { try {
api.onLogMessage(arg_log!); api.onLogMessage(arg_log!);
return wrapResponse(empty: true); return wrapResponse(empty: true);
} on PlatformException catch (e) { } on PlatformException catch (e) {
return wrapResponse(error: e); return wrapResponse(error: e);
} catch (e) { } catch (e) {
return wrapResponse( return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
error: PlatformException(code: 'error', message: e.toString()),
);
} }
}); });
} }
} }
{ {
final pigeonVar_channel = BasicMessageChannel<Object?>( final pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.flutter_tor.TorLogApi.onStatusChanged$messageChannelSuffix', 'dev.flutter.pigeon.flutter_tor.TorLogApi.onStatusChanged$messageChannelSuffix', pigeonChannelCodec,
pigeonChannelCodec, binaryMessenger: binaryMessenger);
binaryMessenger: binaryMessenger,
);
if (api == null) { if (api == null) {
pigeonVar_channel.setMessageHandler(null); pigeonVar_channel.setMessageHandler(null);
} else { } else {
pigeonVar_channel.setMessageHandler((Object? message) async { pigeonVar_channel.setMessageHandler((Object? message) async {
assert( assert(message != null,
message != null, 'Argument for dev.flutter.pigeon.flutter_tor.TorLogApi.onStatusChanged was null.');
'Argument for dev.flutter.pigeon.flutter_tor.TorLogApi.onStatusChanged was null.',
);
final List<Object?> args = (message as List<Object?>?)!; final List<Object?> args = (message as List<Object?>?)!;
final TorStatus? arg_status = (args[0] as TorStatus?); final TorStatus? arg_status = (args[0] as TorStatus?);
assert( assert(arg_status != null,
arg_status != null, 'Argument for dev.flutter.pigeon.flutter_tor.TorLogApi.onStatusChanged was null, expected non-null TorStatus.');
'Argument for dev.flutter.pigeon.flutter_tor.TorLogApi.onStatusChanged was null, expected non-null TorStatus.',
);
try { try {
api.onStatusChanged(arg_status!); api.onStatusChanged(arg_status!);
return wrapResponse(empty: true); return wrapResponse(empty: true);
} on PlatformException catch (e) { } on PlatformException catch (e) {
return wrapResponse(error: e); return wrapResponse(error: e);
} catch (e) { } catch (e) {
return wrapResponse( return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
error: PlatformException(code: 'error', message: e.toString()),
);
} }
}); });
} }
@@ -518,13 +478,9 @@ class IPtProxyController {
/// Constructor for [IPtProxyController]. The [binaryMessenger] named argument is /// Constructor for [IPtProxyController]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default /// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform. /// BinaryMessenger will be used which routes to the host platform.
IPtProxyController({ IPtProxyController({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
BinaryMessenger? binaryMessenger, : pigeonVar_binaryMessenger = binaryMessenger,
String messageChannelSuffix = '', pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
}) : pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty
? '.$messageChannelSuffix'
: '';
final BinaryMessenger? pigeonVar_binaryMessenger; final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec(); static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
@@ -532,16 +488,13 @@ class IPtProxyController {
final String pigeonVar_messageChannelSuffix; final String pigeonVar_messageChannelSuffix;
Future<int> start(TransportType proxyType, String proxy) async { Future<int> start(TransportType proxyType, String proxy) async {
final pigeonVar_channelName = final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.IPtProxyController.start$pigeonVar_messageChannelSuffix';
'dev.flutter.pigeon.flutter_tor.IPtProxyController.start$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>( final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName, pigeonVar_channelName,
pigeonChannelCodec, pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger, binaryMessenger: pigeonVar_binaryMessenger,
); );
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send( final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[proxyType, proxy]);
<Object?>[proxyType, proxy],
);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?; final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) { if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName); throw _createConnectionError(pigeonVar_channelName);
@@ -562,16 +515,13 @@ class IPtProxyController {
} }
Future<void> stop(TransportType proxyType) async { Future<void> stop(TransportType proxyType) async {
final pigeonVar_channelName = final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.IPtProxyController.stop$pigeonVar_messageChannelSuffix';
'dev.flutter.pigeon.flutter_tor.IPtProxyController.stop$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>( final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName, pigeonVar_channelName,
pigeonChannelCodec, pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger, binaryMessenger: pigeonVar_binaryMessenger,
); );
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send( final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[proxyType]);
<Object?>[proxyType],
);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?; final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) { if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName); throw _createConnectionError(pigeonVar_channelName);
@@ -14,39 +14,40 @@ PlatformException _createConnectionError(String channelName) {
message: 'Unable to establish connection on channel: "$channelName".', message: 'Unable to establish connection on channel: "$channelName".',
); );
} }
bool _deepEquals(Object? a, Object? b) { bool _deepEquals(Object? a, Object? b) {
if (a is List && b is List) { if (a is List && b is List) {
return a.length == b.length && return a.length == b.length &&
a.indexed.every( a.indexed
((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]), .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
);
} }
if (a is Map && b is Map) { if (a is Map && b is Map) {
return a.length == b.length && return a.length == b.length && a.entries.every((MapEntry<Object?, Object?> entry) =>
a.entries.every( (b as Map<Object?, Object?>).containsKey(entry.key) &&
(MapEntry<Object?, Object?> entry) => _deepEquals(entry.value, b[entry.key]));
(b as Map<Object?, Object?>).containsKey(entry.key) &&
_deepEquals(entry.value, b[entry.key]),
);
} }
return a == b; return a == b;
} }
class LocalizedResult { class LocalizedResult {
LocalizedResult({required this.languageName, this.countryName}); LocalizedResult({
required this.languageName,
this.countryName,
});
String languageName; String languageName;
String? countryName; String? countryName;
List<Object?> _toList() { List<Object?> _toList() {
return <Object?>[languageName, countryName]; return <Object?>[
languageName,
countryName,
];
} }
Object encode() { Object encode() {
return _toList(); return _toList(); }
}
static LocalizedResult decode(Object result) { static LocalizedResult decode(Object result) {
result as List<Object?>; result as List<Object?>;
@@ -70,9 +71,11 @@ class LocalizedResult {
@override @override
// ignore: avoid_equals_and_hash_code_on_mutable_classes // ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => Object.hashAll(_toList()); int get hashCode => Object.hashAll(_toList())
;
} }
class _PigeonCodec extends StandardMessageCodec { class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec(); const _PigeonCodec();
@override @override
@@ -80,7 +83,7 @@ class _PigeonCodec extends StandardMessageCodec {
if (value is int) { if (value is int) {
buffer.putUint8(4); buffer.putUint8(4);
buffer.putInt64(value); buffer.putInt64(value);
} else if (value is LocalizedResult) { } else if (value is LocalizedResult) {
buffer.putUint8(129); buffer.putUint8(129);
writeValue(buffer, value.encode()); writeValue(buffer, value.encode());
} else { } else {
@@ -91,7 +94,7 @@ class _PigeonCodec extends StandardMessageCodec {
@override @override
Object? readValueOfType(int type, ReadBuffer buffer) { Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) { switch (type) {
case 129: case 129:
return LocalizedResult.decode(readValue(buffer)!); return LocalizedResult.decode(readValue(buffer)!);
default: default:
return super.readValueOfType(type, buffer); return super.readValueOfType(type, buffer);
@@ -103,33 +106,23 @@ class LocaleResolver {
/// Constructor for [LocaleResolver]. The [binaryMessenger] named argument is /// Constructor for [LocaleResolver]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default /// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform. /// BinaryMessenger will be used which routes to the host platform.
LocaleResolver({ LocaleResolver({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
BinaryMessenger? binaryMessenger, : pigeonVar_binaryMessenger = binaryMessenger,
String messageChannelSuffix = '', pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
}) : pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty
? '.$messageChannelSuffix'
: '';
final BinaryMessenger? pigeonVar_binaryMessenger; final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec(); static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
final String pigeonVar_messageChannelSuffix; final String pigeonVar_messageChannelSuffix;
Future<LocalizedResult> resolve( Future<LocalizedResult> resolve(String languageTag, String targetLangouageTag) async {
String languageTag, final pigeonVar_channelName = 'dev.flutter.pigeon.locale_resolver.LocaleResolver.resolve$pigeonVar_messageChannelSuffix';
String targetLangouageTag,
) async {
final pigeonVar_channelName =
'dev.flutter.pigeon.locale_resolver.LocaleResolver.resolve$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>( final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName, pigeonVar_channelName,
pigeonChannelCodec, pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger, binaryMessenger: pigeonVar_binaryMessenger,
); );
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send( final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[languageTag, targetLangouageTag]);
<Object?>[languageTag, targetLangouageTag],
);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?; final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) { if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName); throw _createConnectionError(pigeonVar_channelName);
@@ -8,11 +8,7 @@ import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
List<Object?> wrapResponse({ List<Object?> wrapResponse({Object? result, PlatformException? error, bool empty = false}) {
Object? result,
PlatformException? error,
bool empty = false,
}) {
if (empty) { if (empty) {
return <Object?>[]; return <Object?>[];
} }
@@ -21,25 +17,21 @@ List<Object?> wrapResponse({
} }
return <Object?>[error.code, error.message, error.details]; return <Object?>[error.code, error.message, error.details];
} }
bool _deepEquals(Object? a, Object? b) { bool _deepEquals(Object? a, Object? b) {
if (a is List && b is List) { if (a is List && b is List) {
return a.length == b.length && return a.length == b.length &&
a.indexed.every( a.indexed
((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]), .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
);
} }
if (a is Map && b is Map) { if (a is Map && b is Map) {
return a.length == b.length && return a.length == b.length && a.entries.every((MapEntry<Object?, Object?> entry) =>
a.entries.every( (b as Map<Object?, Object?>).containsKey(entry.key) &&
(MapEntry<Object?, Object?> entry) => _deepEquals(entry.value, b[entry.key]));
(b as Map<Object?, Object?>).containsKey(entry.key) &&
_deepEquals(entry.value, b[entry.key]),
);
} }
return a == b; return a == b;
} }
class Intent { class Intent {
Intent({ Intent({
this.fromPackageName, this.fromPackageName,
@@ -74,8 +66,7 @@ class Intent {
} }
Object encode() { Object encode() {
return _toList(); return _toList(); }
}
static Intent decode(Object result) { static Intent decode(Object result) {
result as List<Object?>; result as List<Object?>;
@@ -103,9 +94,11 @@ class Intent {
@override @override
// ignore: avoid_equals_and_hash_code_on_mutable_classes // ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => Object.hashAll(_toList()); int get hashCode => Object.hashAll(_toList())
;
} }
class _PigeonCodec extends StandardMessageCodec { class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec(); const _PigeonCodec();
@override @override
@@ -113,7 +106,7 @@ class _PigeonCodec extends StandardMessageCodec {
if (value is int) { if (value is int) {
buffer.putUint8(4); buffer.putUint8(4);
buffer.putInt64(value); buffer.putInt64(value);
} else if (value is Intent) { } else if (value is Intent) {
buffer.putUint8(129); buffer.putUint8(129);
writeValue(buffer, value.encode()); writeValue(buffer, value.encode());
} else { } else {
@@ -124,7 +117,7 @@ class _PigeonCodec extends StandardMessageCodec {
@override @override
Object? readValueOfType(int type, ReadBuffer buffer) { Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) { switch (type) {
case 129: case 129:
return Intent.decode(readValue(buffer)!); return Intent.decode(readValue(buffer)!);
default: default:
return super.readValueOfType(type, buffer); return super.readValueOfType(type, buffer);
@@ -137,48 +130,32 @@ abstract class IntentEvents {
void onIntentReceived(int sequence, Intent intent); void onIntentReceived(int sequence, Intent intent);
static void setUp( static void setUp(IntentEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
IntentEvents? api, { messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
BinaryMessenger? binaryMessenger,
String messageChannelSuffix = '',
}) {
messageChannelSuffix = messageChannelSuffix.isNotEmpty
? '.$messageChannelSuffix'
: '';
{ {
final pigeonVar_channel = BasicMessageChannel<Object?>( final pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.simple_intent_receiver.IntentEvents.onIntentReceived$messageChannelSuffix', 'dev.flutter.pigeon.simple_intent_receiver.IntentEvents.onIntentReceived$messageChannelSuffix', pigeonChannelCodec,
pigeonChannelCodec, binaryMessenger: binaryMessenger);
binaryMessenger: binaryMessenger,
);
if (api == null) { if (api == null) {
pigeonVar_channel.setMessageHandler(null); pigeonVar_channel.setMessageHandler(null);
} else { } else {
pigeonVar_channel.setMessageHandler((Object? message) async { pigeonVar_channel.setMessageHandler((Object? message) async {
assert( assert(message != null,
message != null, 'Argument for dev.flutter.pigeon.simple_intent_receiver.IntentEvents.onIntentReceived was null.');
'Argument for dev.flutter.pigeon.simple_intent_receiver.IntentEvents.onIntentReceived was null.',
);
final List<Object?> args = (message as List<Object?>?)!; final List<Object?> args = (message as List<Object?>?)!;
final int? arg_sequence = (args[0] as int?); final int? arg_sequence = (args[0] as int?);
assert( assert(arg_sequence != null,
arg_sequence != null, 'Argument for dev.flutter.pigeon.simple_intent_receiver.IntentEvents.onIntentReceived was null, expected non-null int.');
'Argument for dev.flutter.pigeon.simple_intent_receiver.IntentEvents.onIntentReceived was null, expected non-null int.',
);
final Intent? arg_intent = (args[1] as Intent?); final Intent? arg_intent = (args[1] as Intent?);
assert( assert(arg_intent != null,
arg_intent != null, 'Argument for dev.flutter.pigeon.simple_intent_receiver.IntentEvents.onIntentReceived was null, expected non-null Intent.');
'Argument for dev.flutter.pigeon.simple_intent_receiver.IntentEvents.onIntentReceived was null, expected non-null Intent.',
);
try { try {
api.onIntentReceived(arg_sequence!, arg_intent!); api.onIntentReceived(arg_sequence!, arg_intent!);
return wrapResponse(empty: true); return wrapResponse(empty: true);
} on PlatformException catch (e) { } on PlatformException catch (e) {
return wrapResponse(error: e); return wrapResponse(error: e);
} catch (e) { } catch (e) {
return wrapResponse( return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
error: PlatformException(code: 'error', message: e.toString()),
);
} }
}); });
} }
@@ -15,11 +15,7 @@ PlatformException _createConnectionError(String channelName) {
); );
} }
List<Object?> wrapResponse({ List<Object?> wrapResponse({Object? result, PlatformException? error, bool empty = false}) {
Object? result,
PlatformException? error,
bool empty = false,
}) {
if (empty) { if (empty) {
return <Object?>[]; return <Object?>[];
} }
@@ -29,6 +25,7 @@ List<Object?> wrapResponse({
return <Object?>[error.code, error.message, error.details]; return <Object?>[error.code, error.message, error.details];
} }
class _PigeonCodec extends StandardMessageCodec { class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec(); const _PigeonCodec();
@override @override
@@ -55,13 +52,9 @@ class SpeechToTextApi {
/// Constructor for [SpeechToTextApi]. The [binaryMessenger] named argument is /// Constructor for [SpeechToTextApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default /// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform. /// BinaryMessenger will be used which routes to the host platform.
SpeechToTextApi({ SpeechToTextApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
BinaryMessenger? binaryMessenger, : pigeonVar_binaryMessenger = binaryMessenger,
String messageChannelSuffix = '', pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
}) : pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty
? '.$messageChannelSuffix'
: '';
final BinaryMessenger? pigeonVar_binaryMessenger; final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec(); static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
@@ -76,16 +69,13 @@ class SpeechToTextApi {
/// The [locale] parameter specifies the language locale for recognition /// The [locale] parameter specifies the language locale for recognition
/// (e.g., 'en-US', 'de-DE'). If null, uses the device default. /// (e.g., 'en-US', 'de-DE'). If null, uses the device default.
Future<bool> showDialog({String? locale}) async { Future<bool> showDialog({String? locale}) async {
final pigeonVar_channelName = final pigeonVar_channelName = 'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextApi.showDialog$pigeonVar_messageChannelSuffix';
'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextApi.showDialog$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>( final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName, pigeonVar_channelName,
pigeonChannelCodec, pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger, binaryMessenger: pigeonVar_binaryMessenger,
); );
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send( final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[locale]);
<Object?>[locale],
);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?; final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) { if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName); throw _createConnectionError(pigeonVar_channelName);
@@ -116,43 +106,29 @@ abstract class SpeechToTextEvents {
/// recognition failed or was cancelled. /// recognition failed or was cancelled.
void onTextReceived(String text); void onTextReceived(String text);
static void setUp( static void setUp(SpeechToTextEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
SpeechToTextEvents? api, { messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
BinaryMessenger? binaryMessenger,
String messageChannelSuffix = '',
}) {
messageChannelSuffix = messageChannelSuffix.isNotEmpty
? '.$messageChannelSuffix'
: '';
{ {
final pigeonVar_channel = BasicMessageChannel<Object?>( final pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived$messageChannelSuffix', 'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived$messageChannelSuffix', pigeonChannelCodec,
pigeonChannelCodec, binaryMessenger: binaryMessenger);
binaryMessenger: binaryMessenger,
);
if (api == null) { if (api == null) {
pigeonVar_channel.setMessageHandler(null); pigeonVar_channel.setMessageHandler(null);
} else { } else {
pigeonVar_channel.setMessageHandler((Object? message) async { pigeonVar_channel.setMessageHandler((Object? message) async {
assert( assert(message != null,
message != null, 'Argument for dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived was null.');
'Argument for dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived was null.',
);
final List<Object?> args = (message as List<Object?>?)!; final List<Object?> args = (message as List<Object?>?)!;
final String? arg_text = (args[0] as String?); final String? arg_text = (args[0] as String?);
assert( assert(arg_text != null,
arg_text != null, 'Argument for dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived was null, expected non-null String.');
'Argument for dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived was null, expected non-null String.',
);
try { try {
api.onTextReceived(arg_text!); api.onTextReceived(arg_text!);
return wrapResponse(empty: true); return wrapResponse(empty: true);
} on PlatformException catch (e) { } on PlatformException catch (e) {
return wrapResponse(error: e); return wrapResponse(error: e);
} catch (e) { } catch (e) {
return wrapResponse( return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
error: PlatformException(code: 'error', message: e.toString()),
);
} }
}); });
} }