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/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';
@@ -75,6 +76,7 @@ class TabState extends WebPageInfo {
final HistoryState historyState;
final ReaderableState readerableState;
final FindResultState findResultState;
final TranslationState translationState;
TabState({
required this.id,
@@ -93,6 +95,7 @@ class TabState extends WebPageInfo {
required this.historyState,
required this.readerableState,
required this.findResultState,
required this.translationState,
}) : super(title: title.trim());
factory TabState.$default(String tabId) => TabState(
@@ -111,6 +114,7 @@ class TabState extends WebPageInfo {
historyState: HistoryState.$default(),
readerableState: ReaderableState.$default(),
findResultState: FindResultState.$default(),
translationState: TranslationState.$default(),
);
@override
@@ -130,5 +134,6 @@ class TabState extends WebPageInfo {
historyState,
readerableState,
findResultState,
translationState,
];
}
@@ -37,6 +37,8 @@ abstract class _$TabStateCWProxy {
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)`.
///
@@ -60,6 +62,7 @@ abstract class _$TabStateCWProxy {
HistoryState historyState,
ReaderableState readerableState,
FindResultState findResultState,
TranslationState translationState,
});
}
@@ -120,6 +123,10 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
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)`.
@@ -144,6 +151,7 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
Object? historyState = const $CopyWithPlaceholder(),
Object? readerableState = const $CopyWithPlaceholder(),
Object? findResultState = const $CopyWithPlaceholder(),
Object? translationState = const $CopyWithPlaceholder(),
}) {
return TabState(
id: _value.id,
@@ -217,6 +225,12 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
? _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,
);
}
}
@@ -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;
}
/// 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 {
@@ -389,6 +389,72 @@ final class MlProgressEventsProvider
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)
@@ -76,6 +76,26 @@ class TabSession extends _$TabSession {
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
void build({required String? tabId}) {
_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
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/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';
@@ -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) {
final FindResultsEvent(:tabId, :results) = event;
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(
@@ -331,6 +354,7 @@ class TabStates extends _$TabStates {
onSecurityInfoStateChange: true,
onHistoryStateChange: 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>> {
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/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/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/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';
@@ -395,6 +396,9 @@ class _PageActionsCard extends HookConsumerWidget {
},
),
// Translate Page
_TranslatePageTile(selectedTabId: selectedTabId),
// Add to Home Screen (conditional)
_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 {
final String selectedTabId;
@@ -1385,6 +1431,24 @@ class _ExportExpansion extends ConsumerWidget {
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 {
const ShareMarkdownActionMenuItemButton({
super.key,
@@ -31,12 +31,14 @@ import 'package:weblibre/core/design/app_colors.dart';
import 'package:weblibre/core/routing/routes.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/providers.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_state.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/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/pwa/domain/providers.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),
PrintMenuItemButton(selectedTabId: selectedTabId),
],
leadingIcon: const Icon(MdiIcons.fileExport),
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)
Consumer(
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)
fragmentEngineView = engineView // Track for lifecycle management
val originalContext = ActivityContextWrapper.getOriginalContext(requireActivity())
?.let { ProfileContext(it, components.profileApplicationContext.relativePath) }
val originalContext =
ActivityContextWrapper.getOriginalContext(requireActivity()) ?: requireActivity()
val engineNativeView = engineView.asView()
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.SecurityInfoState
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.Dispatchers
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.state.action.EngineAction
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.state.BrowserState
import mozilla.components.browser.state.state.LastMediaAccessState
@@ -55,6 +59,7 @@ import org.json.JSONObject
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
import mozilla.components.feature.addons.logger
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.translate.TranslationOperation
class GeckoTabsApiImpl : GeckoTabsApi {
companion object {
@@ -185,6 +190,7 @@ class GeckoTabsApiImpl : GeckoTabsApi {
onPageExtensionsChange: Boolean,
onBrowserExtensionIcons: Boolean,
onPageExtensionIcons: Boolean,
onTranslationStateChange: Boolean,
) {
try {
val tabs = components.core.store.state.tabs.map { it.copy() }
@@ -293,11 +299,85 @@ class GeckoTabsApiImpl : GeckoTabsApi {
onPageExtensionIcons
)
}
// Sync translation events
if (onTranslationStateChange) {
syncTranslationEvents(tabs)
}
} catch (e: Exception) {
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(
onBrowserExtensionsChange: Boolean,
onPageExtensionsChange: Boolean,
@@ -30,6 +30,7 @@ import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.session.storage.SessionStorage
import mozilla.components.browser.state.engine.EngineMiddleware
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.storage.sync.PlacesBookmarksStorage
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.support.base.worker.Frequency
import org.mozilla.geckoview.GeckoRuntime
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.Job
import java.util.concurrent.TimeUnit
@@ -207,6 +209,9 @@ class Core(
SaveToPDFMiddleware(),
FileUploadsDirCleanerMiddleware(fileUploadsDirCleaner),
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(
engine,
// 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.ShareTargetParams
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.flow.Flow
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
stateFlow.flowScoped { flow ->
flow.mapNotNull { state -> state.selectedTab }
@@ -24,6 +24,15 @@ class SaveToPDFMiddleware : Middleware<BrowserState, BrowserAction> {
is EngineAction.SaveToPdfExceptionAction -> {
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 -> {
next(action)
}
@@ -94,9 +94,15 @@ export 'src/pigeons/gecko.g.dart'
SyncRemoteTab,
TabContent,
TabContentState,
TabTranslationStateData,
TrackingProtectionException,
TrackingProtectionPolicy,
TrackingScope,
TranslationDetectedLanguages,
TranslationEngineStateData,
TranslationLanguage,
TranslationOptions,
TranslationPair,
UnknownHitResult,
VideoHitResult,
VisitInfo,
@@ -21,6 +21,8 @@ typedef FindResultsEvent = ({String tabId, List<FindResultState> results});
typedef LongPressEvent = ({String tabId, HitResult hitResult});
typedef ScrollEvent = ({String tabId, int scrollY});
typedef ManifestUpdateEvent = ({String tabId, PwaManifest? manifest});
typedef TabTranslationEvent = ({String tabId, TabTranslationStateData state});
typedef TranslationEngineEvent = TranslationEngineStateData;
class GeckoEventService extends GeckoStateEvents {
// Stream controllers
@@ -46,6 +48,9 @@ class GeckoEventService extends GeckoStateEvents {
final _tabAddedSubject = PublishSubject<String>();
final _mlProgressSubject = PublishSubject<MlProgressData>();
final _manifestUpdateSubject = PublishSubject<ManifestUpdateEvent>();
final _translationEngineSubject =
BehaviorSubject<TranslationEngineEvent>();
final _tabTranslationSubject = ReplaySubject<TabTranslationEvent>();
// Event streams
ValueStream<bool> get viewReadyStateEvents => _viewStateSubject.stream;
@@ -72,6 +77,10 @@ class GeckoEventService extends GeckoStateEvents {
Stream<MlProgressData> get mlProgressEvents => _mlProgressSubject.stream;
Stream<ManifestUpdateEvent> get manifestUpdateEvents =>
_manifestUpdateSubject.stream;
ValueStream<TranslationEngineEvent> get translationEngineEvents =>
_translationEngineSubject.stream;
Stream<TabTranslationEvent> get tabTranslationEvents =>
_tabTranslationSubject.stream;
@override
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({
BinaryMessenger? binaryMessenger,
String messageChannelSuffix = '',
@@ -241,5 +269,7 @@ class GeckoEventService extends GeckoStateEvents {
await _siteAssignementSubject.close();
await _mlProgressSubject.close();
await _manifestUpdateSubject.close();
await _translationEngineSubject.close();
await _tabTranslationSubject.close();
}
}
@@ -29,6 +29,7 @@ class GeckoTabService {
bool onPageExtensionsChange = false,
bool onBrowserExtensionIcons = false,
bool onPageExtensionIcons = false,
bool onTranslationStateChange = false,
}) {
return _api.syncEvents(
onSelectedTabChange: onSelectedTabChange,
@@ -44,6 +45,7 @@ class GeckoTabService {
onPageExtensionsChange: onPageExtensionsChange,
onBrowserExtensionIcons: onBrowserExtensionIcons,
onPageExtensionIcons: onPageExtensionIcons,
onTranslationStateChange: onTranslationStateChange,
);
}
File diff suppressed because it is too large Load Diff
@@ -16,6 +16,77 @@ class TranslationOptions {
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.
class ReaderState {
/// Whether or not the current page can be transformed to
@@ -1256,6 +1327,7 @@ abstract class GeckoTabsApi {
required bool onPageExtensionsChange,
required bool onBrowserExtensionIcons,
required bool onPageExtensionIcons,
required bool onTranslationStateChange,
});
void selectTab({required String tabId});
@@ -1562,6 +1634,15 @@ abstract class GeckoStateEvents {
void onMlProgress(int sequence, MlProgressData progress);
void onManifestUpdate(int sequence, String tabId, PwaManifest? manifest);
void onTranslationEngineStateChange(
int sequence,
TranslationEngineStateData state,
);
void onTabTranslationStateChange(
int sequence,
TabTranslationStateData state,
);
}
@FlutterApi()
+62 -112
View File
@@ -15,11 +15,7 @@ PlatformException _createConnectionError(String channelName) {
);
}
List<Object?> wrapResponse({
Object? result,
PlatformException? error,
bool empty = false,
}) {
List<Object?> wrapResponse({Object? result, PlatformException? error, bool empty = false}) {
if (empty) {
return <Object?>[];
}
@@ -28,48 +24,37 @@ List<Object?> wrapResponse({
}
return <Object?>[error.code, error.message, error.details];
}
bool _deepEquals(Object? a, Object? b) {
if (a is List && b is List) {
return a.length == b.length &&
a.indexed.every(
((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]),
);
a.indexed
.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
}
if (a is Map && b is Map) {
return a.length == b.length &&
a.entries.every(
(MapEntry<Object?, Object?> entry) =>
(b as Map<Object?, Object?>).containsKey(entry.key) &&
_deepEquals(entry.value, b[entry.key]),
);
return a.length == b.length && a.entries.every((MapEntry<Object?, Object?> entry) =>
(b as Map<Object?, Object?>).containsKey(entry.key) &&
_deepEquals(entry.value, b[entry.key]));
}
return a == b;
}
/// Transport types for Tor connections
enum TransportType {
/// Direct Tor connection (no bridges)
none,
/// obfs4 pluggable transport
obfs4,
/// Snowflake pluggable transport (default broker)
snowflake,
/// Snowflake via AMP cache
snowflakeAmp,
/// Meek pluggable transport
meek,
/// Meek via Azure CDN
meekAzure,
/// WebTunnel pluggable transport
webtunnel,
/// Custom bridge lines (passthrough)
custom,
}
@@ -110,8 +95,7 @@ class TorConfiguration {
}
Object encode() {
return _toList();
}
return _toList(); }
static TorConfiguration decode(Object result) {
result as List<Object?>;
@@ -138,7 +122,8 @@ class TorConfiguration {
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => Object.hashAll(_toList());
int get hashCode => Object.hashAll(_toList())
;
}
/// Current Tor status
@@ -177,8 +162,7 @@ class TorStatus {
}
Object encode() {
return _toList();
}
return _toList(); }
static TorStatus decode(Object result) {
result as List<Object?>;
@@ -205,7 +189,8 @@ class TorStatus {
@override
// 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
@@ -226,12 +211,15 @@ class TorLogMessage {
int timestamp;
List<Object?> _toList() {
return <Object?>[severity, message, timestamp];
return <Object?>[
severity,
message,
timestamp,
];
}
Object encode() {
return _toList();
}
return _toList(); }
static TorLogMessage decode(Object result) {
result as List<Object?>;
@@ -256,9 +244,11 @@ class TorLogMessage {
@override
// 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 {
const _PigeonCodec();
@override
@@ -266,16 +256,16 @@ class _PigeonCodec extends StandardMessageCodec {
if (value is int) {
buffer.putUint8(4);
buffer.putInt64(value);
} else if (value is TransportType) {
} else if (value is TransportType) {
buffer.putUint8(129);
writeValue(buffer, value.index);
} else if (value is TorConfiguration) {
} else if (value is TorConfiguration) {
buffer.putUint8(130);
writeValue(buffer, value.encode());
} else if (value is TorStatus) {
} else if (value is TorStatus) {
buffer.putUint8(131);
writeValue(buffer, value.encode());
} else if (value is TorLogMessage) {
} else if (value is TorLogMessage) {
buffer.putUint8(132);
writeValue(buffer, value.encode());
} else {
@@ -286,14 +276,14 @@ class _PigeonCodec extends StandardMessageCodec {
@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case 129:
case 129:
final value = readValue(buffer) as int?;
return value == null ? null : TransportType.values[value];
case 130:
case 130:
return TorConfiguration.decode(readValue(buffer)!);
case 131:
case 131:
return TorStatus.decode(readValue(buffer)!);
case 132:
case 132:
return TorLogMessage.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
@@ -307,10 +297,8 @@ class TorApi {
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
TorApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
: pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty
? '.$messageChannelSuffix'
: '';
: pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
@@ -320,16 +308,13 @@ class TorApi {
/// Start Tor with the given configuration
/// Returns a Future to avoid blocking the main thread
Future<int> startTor(TorConfiguration config) async {
final pigeonVar_channelName =
'dev.flutter.pigeon.flutter_tor.TorApi.startTor$pigeonVar_messageChannelSuffix';
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.TorApi.startTor$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
<Object?>[config],
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[config]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
@@ -351,8 +336,7 @@ class TorApi {
/// Stop Tor
Future<void> stopTor() async {
final pigeonVar_channelName =
'dev.flutter.pigeon.flutter_tor.TorApi.stopTor$pigeonVar_messageChannelSuffix';
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.TorApi.stopTor$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
@@ -375,8 +359,7 @@ class TorApi {
/// Get current status
Future<TorStatus> getStatus() async {
final pigeonVar_channelName =
'dev.flutter.pigeon.flutter_tor.TorApi.getStatus$pigeonVar_messageChannelSuffix';
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.TorApi.getStatus$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
@@ -404,8 +387,7 @@ class TorApi {
/// Request a new Tor identity (new circuit)
Future<void> requestNewIdentity() async {
final pigeonVar_channelName =
'dev.flutter.pigeon.flutter_tor.TorApi.requestNewIdentity$pigeonVar_messageChannelSuffix';
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.TorApi.requestNewIdentity$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
@@ -437,76 +419,54 @@ abstract class TorLogApi {
/// Called when status changes
void onStatusChanged(TorStatus status);
static void setUp(
TorLogApi? api, {
BinaryMessenger? binaryMessenger,
String messageChannelSuffix = '',
}) {
messageChannelSuffix = messageChannelSuffix.isNotEmpty
? '.$messageChannelSuffix'
: '';
static void setUp(TorLogApi? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
{
final pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.flutter_tor.TorLogApi.onLogMessage$messageChannelSuffix',
pigeonChannelCodec,
binaryMessenger: binaryMessenger,
);
'dev.flutter.pigeon.flutter_tor.TorLogApi.onLogMessage$messageChannelSuffix', pigeonChannelCodec,
binaryMessenger: binaryMessenger);
if (api == null) {
pigeonVar_channel.setMessageHandler(null);
} else {
pigeonVar_channel.setMessageHandler((Object? message) async {
assert(
message != null,
'Argument for dev.flutter.pigeon.flutter_tor.TorLogApi.onLogMessage was null.',
);
assert(message != null,
'Argument for dev.flutter.pigeon.flutter_tor.TorLogApi.onLogMessage was null.');
final List<Object?> args = (message as List<Object?>?)!;
final TorLogMessage? arg_log = (args[0] as TorLogMessage?);
assert(
arg_log != null,
'Argument for dev.flutter.pigeon.flutter_tor.TorLogApi.onLogMessage was null, expected non-null TorLogMessage.',
);
assert(arg_log != null,
'Argument for dev.flutter.pigeon.flutter_tor.TorLogApi.onLogMessage was null, expected non-null TorLogMessage.');
try {
api.onLogMessage(arg_log!);
return wrapResponse(empty: true);
} on PlatformException catch (e) {
return wrapResponse(error: e);
} catch (e) {
return wrapResponse(
error: PlatformException(code: 'error', message: e.toString()),
);
} catch (e) {
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
}
});
}
}
{
final pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.flutter_tor.TorLogApi.onStatusChanged$messageChannelSuffix',
pigeonChannelCodec,
binaryMessenger: binaryMessenger,
);
'dev.flutter.pigeon.flutter_tor.TorLogApi.onStatusChanged$messageChannelSuffix', pigeonChannelCodec,
binaryMessenger: binaryMessenger);
if (api == null) {
pigeonVar_channel.setMessageHandler(null);
} else {
pigeonVar_channel.setMessageHandler((Object? message) async {
assert(
message != null,
'Argument for dev.flutter.pigeon.flutter_tor.TorLogApi.onStatusChanged was null.',
);
assert(message != null,
'Argument for dev.flutter.pigeon.flutter_tor.TorLogApi.onStatusChanged was null.');
final List<Object?> args = (message as List<Object?>?)!;
final TorStatus? arg_status = (args[0] as TorStatus?);
assert(
arg_status != null,
'Argument for dev.flutter.pigeon.flutter_tor.TorLogApi.onStatusChanged was null, expected non-null TorStatus.',
);
assert(arg_status != null,
'Argument for dev.flutter.pigeon.flutter_tor.TorLogApi.onStatusChanged was null, expected non-null TorStatus.');
try {
api.onStatusChanged(arg_status!);
return wrapResponse(empty: true);
} on PlatformException catch (e) {
return wrapResponse(error: e);
} catch (e) {
return wrapResponse(
error: PlatformException(code: 'error', message: e.toString()),
);
} catch (e) {
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
}
});
}
@@ -518,13 +478,9 @@ class IPtProxyController {
/// Constructor for [IPtProxyController]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
IPtProxyController({
BinaryMessenger? binaryMessenger,
String messageChannelSuffix = '',
}) : pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty
? '.$messageChannelSuffix'
: '';
IPtProxyController({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
: pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
@@ -532,16 +488,13 @@ class IPtProxyController {
final String pigeonVar_messageChannelSuffix;
Future<int> start(TransportType proxyType, String proxy) async {
final pigeonVar_channelName =
'dev.flutter.pigeon.flutter_tor.IPtProxyController.start$pigeonVar_messageChannelSuffix';
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.IPtProxyController.start$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
<Object?>[proxyType, proxy],
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[proxyType, proxy]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
@@ -562,16 +515,13 @@ class IPtProxyController {
}
Future<void> stop(TransportType proxyType) async {
final pigeonVar_channelName =
'dev.flutter.pigeon.flutter_tor.IPtProxyController.stop$pigeonVar_messageChannelSuffix';
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.IPtProxyController.stop$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
<Object?>[proxyType],
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[proxyType]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
@@ -14,39 +14,40 @@ PlatformException _createConnectionError(String channelName) {
message: 'Unable to establish connection on channel: "$channelName".',
);
}
bool _deepEquals(Object? a, Object? b) {
if (a is List && b is List) {
return a.length == b.length &&
a.indexed.every(
((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]),
);
a.indexed
.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
}
if (a is Map && b is Map) {
return a.length == b.length &&
a.entries.every(
(MapEntry<Object?, Object?> entry) =>
(b as Map<Object?, Object?>).containsKey(entry.key) &&
_deepEquals(entry.value, b[entry.key]),
);
return a.length == b.length && a.entries.every((MapEntry<Object?, Object?> entry) =>
(b as Map<Object?, Object?>).containsKey(entry.key) &&
_deepEquals(entry.value, b[entry.key]));
}
return a == b;
}
class LocalizedResult {
LocalizedResult({required this.languageName, this.countryName});
LocalizedResult({
required this.languageName,
this.countryName,
});
String languageName;
String? countryName;
List<Object?> _toList() {
return <Object?>[languageName, countryName];
return <Object?>[
languageName,
countryName,
];
}
Object encode() {
return _toList();
}
return _toList(); }
static LocalizedResult decode(Object result) {
result as List<Object?>;
@@ -70,9 +71,11 @@ class LocalizedResult {
@override
// 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 {
const _PigeonCodec();
@override
@@ -80,7 +83,7 @@ class _PigeonCodec extends StandardMessageCodec {
if (value is int) {
buffer.putUint8(4);
buffer.putInt64(value);
} else if (value is LocalizedResult) {
} else if (value is LocalizedResult) {
buffer.putUint8(129);
writeValue(buffer, value.encode());
} else {
@@ -91,7 +94,7 @@ class _PigeonCodec extends StandardMessageCodec {
@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case 129:
case 129:
return LocalizedResult.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
@@ -103,33 +106,23 @@ class LocaleResolver {
/// Constructor for [LocaleResolver]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
LocaleResolver({
BinaryMessenger? binaryMessenger,
String messageChannelSuffix = '',
}) : pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty
? '.$messageChannelSuffix'
: '';
LocaleResolver({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
: pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
final String pigeonVar_messageChannelSuffix;
Future<LocalizedResult> resolve(
String languageTag,
String targetLangouageTag,
) async {
final pigeonVar_channelName =
'dev.flutter.pigeon.locale_resolver.LocaleResolver.resolve$pigeonVar_messageChannelSuffix';
Future<LocalizedResult> resolve(String languageTag, String targetLangouageTag) async {
final pigeonVar_channelName = 'dev.flutter.pigeon.locale_resolver.LocaleResolver.resolve$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
<Object?>[languageTag, targetLangouageTag],
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[languageTag, targetLangouageTag]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
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/services.dart';
List<Object?> wrapResponse({
Object? result,
PlatformException? error,
bool empty = false,
}) {
List<Object?> wrapResponse({Object? result, PlatformException? error, bool empty = false}) {
if (empty) {
return <Object?>[];
}
@@ -21,25 +17,21 @@ List<Object?> wrapResponse({
}
return <Object?>[error.code, error.message, error.details];
}
bool _deepEquals(Object? a, Object? b) {
if (a is List && b is List) {
return a.length == b.length &&
a.indexed.every(
((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]),
);
a.indexed
.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
}
if (a is Map && b is Map) {
return a.length == b.length &&
a.entries.every(
(MapEntry<Object?, Object?> entry) =>
(b as Map<Object?, Object?>).containsKey(entry.key) &&
_deepEquals(entry.value, b[entry.key]),
);
return a.length == b.length && a.entries.every((MapEntry<Object?, Object?> entry) =>
(b as Map<Object?, Object?>).containsKey(entry.key) &&
_deepEquals(entry.value, b[entry.key]));
}
return a == b;
}
class Intent {
Intent({
this.fromPackageName,
@@ -74,8 +66,7 @@ class Intent {
}
Object encode() {
return _toList();
}
return _toList(); }
static Intent decode(Object result) {
result as List<Object?>;
@@ -103,9 +94,11 @@ class Intent {
@override
// 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 {
const _PigeonCodec();
@override
@@ -113,7 +106,7 @@ class _PigeonCodec extends StandardMessageCodec {
if (value is int) {
buffer.putUint8(4);
buffer.putInt64(value);
} else if (value is Intent) {
} else if (value is Intent) {
buffer.putUint8(129);
writeValue(buffer, value.encode());
} else {
@@ -124,7 +117,7 @@ class _PigeonCodec extends StandardMessageCodec {
@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case 129:
case 129:
return Intent.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
@@ -137,48 +130,32 @@ abstract class IntentEvents {
void onIntentReceived(int sequence, Intent intent);
static void setUp(
IntentEvents? api, {
BinaryMessenger? binaryMessenger,
String messageChannelSuffix = '',
}) {
messageChannelSuffix = messageChannelSuffix.isNotEmpty
? '.$messageChannelSuffix'
: '';
static void setUp(IntentEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
{
final pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.simple_intent_receiver.IntentEvents.onIntentReceived$messageChannelSuffix',
pigeonChannelCodec,
binaryMessenger: binaryMessenger,
);
'dev.flutter.pigeon.simple_intent_receiver.IntentEvents.onIntentReceived$messageChannelSuffix', pigeonChannelCodec,
binaryMessenger: binaryMessenger);
if (api == null) {
pigeonVar_channel.setMessageHandler(null);
} else {
pigeonVar_channel.setMessageHandler((Object? message) async {
assert(
message != null,
'Argument for dev.flutter.pigeon.simple_intent_receiver.IntentEvents.onIntentReceived was null.',
);
assert(message != null,
'Argument for dev.flutter.pigeon.simple_intent_receiver.IntentEvents.onIntentReceived was null.');
final List<Object?> args = (message as List<Object?>?)!;
final int? arg_sequence = (args[0] as int?);
assert(
arg_sequence != null,
'Argument for dev.flutter.pigeon.simple_intent_receiver.IntentEvents.onIntentReceived was null, expected non-null int.',
);
assert(arg_sequence != null,
'Argument for dev.flutter.pigeon.simple_intent_receiver.IntentEvents.onIntentReceived was null, expected non-null int.');
final Intent? arg_intent = (args[1] as Intent?);
assert(
arg_intent != null,
'Argument for dev.flutter.pigeon.simple_intent_receiver.IntentEvents.onIntentReceived was null, expected non-null Intent.',
);
assert(arg_intent != null,
'Argument for dev.flutter.pigeon.simple_intent_receiver.IntentEvents.onIntentReceived was null, expected non-null Intent.');
try {
api.onIntentReceived(arg_sequence!, arg_intent!);
return wrapResponse(empty: true);
} on PlatformException catch (e) {
return wrapResponse(error: e);
} catch (e) {
return wrapResponse(
error: PlatformException(code: 'error', message: e.toString()),
);
} catch (e) {
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
}
});
}
@@ -15,11 +15,7 @@ PlatformException _createConnectionError(String channelName) {
);
}
List<Object?> wrapResponse({
Object? result,
PlatformException? error,
bool empty = false,
}) {
List<Object?> wrapResponse({Object? result, PlatformException? error, bool empty = false}) {
if (empty) {
return <Object?>[];
}
@@ -29,6 +25,7 @@ List<Object?> wrapResponse({
return <Object?>[error.code, error.message, error.details];
}
class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec();
@override
@@ -55,13 +52,9 @@ class SpeechToTextApi {
/// Constructor for [SpeechToTextApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
SpeechToTextApi({
BinaryMessenger? binaryMessenger,
String messageChannelSuffix = '',
}) : pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty
? '.$messageChannelSuffix'
: '';
SpeechToTextApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
: pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
@@ -76,16 +69,13 @@ class SpeechToTextApi {
/// The [locale] parameter specifies the language locale for recognition
/// (e.g., 'en-US', 'de-DE'). If null, uses the device default.
Future<bool> showDialog({String? locale}) async {
final pigeonVar_channelName =
'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextApi.showDialog$pigeonVar_messageChannelSuffix';
final pigeonVar_channelName = 'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextApi.showDialog$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
<Object?>[locale],
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[locale]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
@@ -116,43 +106,29 @@ abstract class SpeechToTextEvents {
/// recognition failed or was cancelled.
void onTextReceived(String text);
static void setUp(
SpeechToTextEvents? api, {
BinaryMessenger? binaryMessenger,
String messageChannelSuffix = '',
}) {
messageChannelSuffix = messageChannelSuffix.isNotEmpty
? '.$messageChannelSuffix'
: '';
static void setUp(SpeechToTextEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
{
final pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived$messageChannelSuffix',
pigeonChannelCodec,
binaryMessenger: binaryMessenger,
);
'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived$messageChannelSuffix', pigeonChannelCodec,
binaryMessenger: binaryMessenger);
if (api == null) {
pigeonVar_channel.setMessageHandler(null);
} else {
pigeonVar_channel.setMessageHandler((Object? message) async {
assert(
message != null,
'Argument for dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived was null.',
);
assert(message != null,
'Argument for dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived was null.');
final List<Object?> args = (message as List<Object?>?)!;
final String? arg_text = (args[0] as String?);
assert(
arg_text != null,
'Argument for dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived was null, expected non-null String.',
);
assert(arg_text != null,
'Argument for dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived was null, expected non-null String.');
try {
api.onTextReceived(arg_text!);
return wrapResponse(empty: true);
} on PlatformException catch (e) {
return wrapResponse(error: e);
} catch (e) {
return wrapResponse(
error: PlatformException(code: 'error', message: e.toString()),
);
} catch (e) {
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
}
});
}