From af2b2ebadb5e62fc661637febe54284dcd7a23bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Sj=C3=B8gren?= Date: Tue, 3 Mar 2026 12:51:46 +0100 Subject: [PATCH] Reenable TTS settings in example app --- flutter_readium/example/lib/main.dart | 15 +- .../example/lib/pages/player.page.dart | 29 +- .../lib/state/player_controls_bloc.dart | 106 ++--- .../example/lib/state/tts_settings_bloc.dart | 254 +++++----- .../lib/widgets/player_controls.widget.dart | 19 +- .../lib/widgets/tts_settings.widget.dart | 443 ++++++++---------- .../lib/method_channel_flutter_readium.dart | 5 +- 7 files changed, 417 insertions(+), 454 deletions(-) diff --git a/flutter_readium/example/lib/main.dart b/flutter_readium/example/lib/main.dart index ca5a90d9..aa0dbd93 100644 --- a/flutter_readium/example/lib/main.dart +++ b/flutter_readium/example/lib/main.dart @@ -16,17 +16,15 @@ Future main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( - storageDirectory: - kIsWeb ? HydratedStorageDirectory.web : HydratedStorageDirectory((await getTemporaryDirectory()).path), + storageDirectory: kIsWeb + ? HydratedStorageDirectory.web + : HydratedStorageDirectory((await getTemporaryDirectory()).path), ); runApp( MultiBlocProvider( providers: [ - BlocProvider( - create: (final _) => PublicationBloc(), - lazy: false, - ), + BlocProvider(create: (final _) => PublicationBloc(), lazy: false), BlocProvider( create: (final _) { final bloc = TextSettingsBloc(); @@ -34,10 +32,7 @@ Future main() async { return bloc; }, ), - // BlocProvider( - // create: (final _) => TtsSettingsBloc(), - // lazy: false, - // ), + BlocProvider(create: (final _) => TtsSettingsBloc(), lazy: false), BlocProvider(create: (final _) => PlayerControlsBloc()), ], child: MyApp(), diff --git a/flutter_readium/example/lib/pages/player.page.dart b/flutter_readium/example/lib/pages/player.page.dart index 9c6e5e2a..a74e03c9 100644 --- a/flutter_readium/example/lib/pages/player.page.dart +++ b/flutter_readium/example/lib/pages/player.page.dart @@ -62,24 +62,21 @@ class _PlayerPageState extends State with RestorationMixin { ); List _buildActionButtons() => [ - // IconButton( - // icon: const Icon(Icons.headphones), - // onPressed: () { - // context.read().add(GetTtsVoicesEvent()); + IconButton( + icon: const Icon(Icons.headphones), + onPressed: () { + context.read().add(GetTtsVoicesEvent()); - // final pubLang = - // context.read().state.publication?.metadata.language ?? ['en']; + final pubLang = context.read().state.publication?.metadata.languages ?? ['en']; - // showModalBottomSheet( - // context: context, - // isScrollControlled: true, - // builder: (final context) => TtsSettingsWidget( - // pubLang: pubLang, - // ), - // ); - // }, - // tooltip: 'Open tts settings', - // ), + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (final context) => TtsSettingsWidget(pubLang: pubLang), + ); + }, + tooltip: 'Open tts settings', + ), IconButton( icon: const Icon(Icons.format_paint), onPressed: () { diff --git a/flutter_readium/example/lib/state/player_controls_bloc.dart b/flutter_readium/example/lib/state/player_controls_bloc.dart index 7d6127da..1dfd4337 100644 --- a/flutter_readium/example/lib/state/player_controls_bloc.dart +++ b/flutter_readium/example/lib/state/player_controls_bloc.dart @@ -8,81 +8,96 @@ import 'package:collection/collection.dart'; import 'package:flutter_readium/flutter_readium.dart'; -abstract class PlayerControlsEvent {} +@immutable +abstract class PlayerControlsEvent { + const PlayerControlsEvent(); +} +@immutable class PlayTTS extends PlayerControlsEvent { - PlayTTS({this.fromLocator}); + const PlayTTS({this.fromLocator, this.ttsPreferences}); - Locator? fromLocator; + final Locator? fromLocator; + final TTSPreferences? ttsPreferences; } +@immutable class Play extends PlayerControlsEvent { - Play({this.fromLocator}); + const Play({this.fromLocator, this.audioPreferences}); - Locator? fromLocator; + final Locator? fromLocator; + final AudioPreferences? audioPreferences; } +@immutable class Pause extends PlayerControlsEvent {} +@immutable class Stop extends PlayerControlsEvent {} +@immutable class TogglePlayingState extends PlayerControlsEvent { - TogglePlayingState({required this.isPlaying}); - bool isPlaying; + const TogglePlayingState({required this.isPlaying}); + final bool isPlaying; } -class SkipToNext extends PlayerControlsEvent {} +@immutable +class SkipToNext extends PlayerControlsEvent { + const SkipToNext(); +} -class SkipToPrevious extends PlayerControlsEvent {} +@immutable +class SkipToPrevious extends PlayerControlsEvent { + const SkipToPrevious(); +} -class SkipToNextChapter extends PlayerControlsEvent {} +@immutable +class SkipToNextChapter extends PlayerControlsEvent { + const SkipToNextChapter(); +} +@immutable class SkipToPreviousChapter extends PlayerControlsEvent {} +@immutable class SkipToNextPage extends PlayerControlsEvent {} +@immutable class SkipToPreviousPage extends PlayerControlsEvent {} +@immutable class GoToLocator extends PlayerControlsEvent { - GoToLocator(this.locator); + const GoToLocator(this.locator); final Locator locator; } -class GetAvailableVoices extends PlayerControlsEvent {} - +@immutable class PlayerControlsState { - PlayerControlsState({required this.playing, required this.ttsEnabled, required this.audioEnabled}); + const PlayerControlsState({required this.playing, required this.ttsEnabled, required this.audioEnabled}); final bool playing; final bool ttsEnabled; final bool audioEnabled; Future togglePlay(final bool playing) async { - final newState = PlayerControlsState(playing: playing, ttsEnabled: ttsEnabled, audioEnabled: audioEnabled); - - return newState; + return copyWith(playing: playing); } Future toggleTTSEnabled(final bool ttsEnabled) async { - final newState = PlayerControlsState( - playing: ttsEnabled && playing, - ttsEnabled: ttsEnabled, - audioEnabled: audioEnabled, - ); - - return newState; + return copyWith(ttsEnabled: ttsEnabled, playing: ttsEnabled && playing); } Future toggleAudioEnabled(final bool audioEnabled) async { - final newState = PlayerControlsState( - playing: audioEnabled && playing, - ttsEnabled: ttsEnabled, - audioEnabled: audioEnabled, - ); - - return newState; + return copyWith(audioEnabled: audioEnabled, playing: audioEnabled && playing); } + + PlayerControlsState copyWith({final bool? playing, final bool? ttsEnabled, final bool? audioEnabled}) => + PlayerControlsState( + playing: playing ?? this.playing, + ttsEnabled: ttsEnabled ?? this.ttsEnabled, + audioEnabled: audioEnabled ?? this.audioEnabled, + ); } class PlayerControlsBloc extends Bloc { @@ -121,7 +136,7 @@ class PlayerControlsBloc extends Bloc on((final event, final emit) async { if (!state.ttsEnabled) { - await instance.ttsEnable(TTSPreferences(speed: 1.2)); + await instance.ttsEnable(event.ttsPreferences ?? TTSPreferences(speed: 1.2)); await instance.play(event.fromLocator); emit(await state.toggleTTSEnabled(true)); } else { @@ -132,7 +147,7 @@ class PlayerControlsBloc extends Bloc on((final event, final emit) async { if (!state.audioEnabled) { await instance.audioEnable( - prefs: AudioPreferences(speed: 1.5, seekInterval: 10), + prefs: event.audioPreferences ?? AudioPreferences(speed: 1.5, seekInterval: 10), fromLocator: event.fromLocator, ); emit(await state.toggleAudioEnabled(true)); @@ -170,31 +185,6 @@ class PlayerControlsBloc extends Bloc on((event, emit) => instance.goToLocator(event.locator)); - on((final event, final emit) async { - final voices = await instance.ttsGetAvailableVoices(); - - // Sort by identifer - voices.sortBy((v) => v.identifier); - - for (final i in voices.groupListsBy((v) => v.language).entries) { - debugPrint('Language: ${i.key}'); - debugPrint(' Available voices:'); - for (final v in i.value) { - debugPrint( - ' - ${v.identifier},name=${v.name},quality=${v.quality?.name},gender=${v.gender.name},active=${v.active},networkRequired=${v.networkRequired}', - ); - } - } - - final dkVoices = voices.where((v) => v.language == "da-DK").toList(); - - // TODO: Demo: change to first voice matching "da-DK" language. - final daVoice = dkVoices.lastOrNull; - if (daVoice != null) { - await instance.ttsSetVoice(daVoice.identifier, daVoice.language); - } - }); - @override // ignore: unused_element Future close() async { diff --git a/flutter_readium/example/lib/state/tts_settings_bloc.dart b/flutter_readium/example/lib/state/tts_settings_bloc.dart index 99d49ed3..fd7a17b9 100644 --- a/flutter_readium/example/lib/state/tts_settings_bloc.dart +++ b/flutter_readium/example/lib/state/tts_settings_bloc.dart @@ -1,123 +1,131 @@ -// import 'package:flutter_bloc/flutter_bloc.dart'; -// import 'package:flutter_readium/flutter_readium.dart'; - -// abstract class TtsSettingsEvent {} - -// class GetTtsVoicesEvent extends TtsSettingsEvent { -// GetTtsVoicesEvent({this.fallbackLang}); -// final List? fallbackLang; -// } - -// class SetTtsVoiceEvent extends TtsSettingsEvent { -// SetTtsVoiceEvent(this.selectedVoice); -// final ReadiumTtsVoice selectedVoice; -// } - -// class SetTtsHighlightModeEvent extends TtsSettingsEvent { -// SetTtsHighlightModeEvent(this.highlightMode); -// final ReadiumHighlightMode highlightMode; -// } - -// class ToggleTtsHighlightModeEvent extends TtsSettingsEvent {} - -// class SetTtsSpeakPhysicalPageIndexEvent extends TtsSettingsEvent { -// SetTtsSpeakPhysicalPageIndexEvent(this.speak); -// final bool speak; -// } - -// class TtsSettingsState { -// TtsSettingsState({ -// this.voices, -// this.loaded, -// this.preferredVoices, -// this.highlightMode, -// this.ttsSpeakPhysicalPageIndex, -// }); -// final List? voices; -// final bool? loaded; -// final List? preferredVoices; -// final ReadiumHighlightMode? highlightMode; -// final bool? ttsSpeakPhysicalPageIndex; - -// TtsSettingsState copyWith({ -// final List? voices, -// final bool? loaded, -// final List? preferredVoices, -// final ReadiumHighlightMode? highlightMode, -// final bool? ttsSpeakPhysicalPageIndex, -// }) => -// TtsSettingsState( -// voices: voices ?? this.voices, -// loaded: loaded ?? this.loaded, -// preferredVoices: preferredVoices ?? this.preferredVoices, -// highlightMode: highlightMode ?? this.highlightMode, -// ttsSpeakPhysicalPageIndex: ttsSpeakPhysicalPageIndex ?? this.ttsSpeakPhysicalPageIndex, -// ); - -// TtsSettingsState updateVoices(final List voices) => copyWith( -// voices: voices, -// loaded: true, -// ); - -// TtsSettingsState updatePreferredVoices(final ReadiumTtsVoice selectedVoice) { -// final preferredVoicesList = preferredVoices ?? []; -// final updatedVoices = preferredVoicesList -// .where((final voice) => voice.langCode != selectedVoice.langCode) -// .toList() -// ..add(selectedVoice); - -// FlutterReadium().updateCurrentTtsVoicesReadium(updatedVoices); - -// return copyWith(preferredVoices: updatedVoices); -// } - -// TtsSettingsState setHighlightMode(final ReadiumHighlightMode highlightMode) { -// FlutterReadium().setHighlightMode(highlightMode); -// return copyWith(highlightMode: highlightMode); -// } - -// TtsSettingsState setTtsSpeakPhysicalPageIndex(final bool speak) { -// FlutterReadium().setTtsSpeakPhysicalPageIndex(speak: speak); -// return copyWith(ttsSpeakPhysicalPageIndex: speak); -// } -// } - -// class TtsSettingsBloc extends Bloc { -// TtsSettingsBloc() -// : super( -// TtsSettingsState( -// voices: [], -// loaded: false, -// preferredVoices: [], -// highlightMode: ReadiumHighlightMode.paragraph, // to reflect default in ReadiumState -// ttsSpeakPhysicalPageIndex: false, -// ), -// ) { -// on((final event, final emit) async { -// final voices = await instance.getTtsVoices(fallbackLang: event.fallbackLang); -// emit(state.updateVoices(voices)); -// }); - -// on((final event, final emit) async { -// await instance.setTtsVoice(event.selectedVoice); -// emit(state.updatePreferredVoices(event.selectedVoice)); -// }); - -// on((final event, final emit) async { -// emit(state.setHighlightMode(event.highlightMode)); -// }); - -// on((final event, final emit) async { -// final newHighlightMode = state.highlightMode == ReadiumHighlightMode.word -// ? ReadiumHighlightMode.paragraph -// : ReadiumHighlightMode.word; -// emit(state.setHighlightMode(newHighlightMode)); -// }); - -// on((final event, final emit) async { -// emit(state.setTtsSpeakPhysicalPageIndex(event.speak)); -// }); -// } - -// final FlutterReadium instance = FlutterReadium(); -// } +import 'package:flutter/cupertino.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_readium/flutter_readium.dart'; + +@immutable +abstract class TtsSettingsEvent { + const TtsSettingsEvent(); +} + +@immutable +class GetTtsVoicesEvent extends TtsSettingsEvent { + const GetTtsVoicesEvent({this.fallbackLang}); + final List? fallbackLang; +} + +@immutable +class SetTtsVoiceEvent extends TtsSettingsEvent { + const SetTtsVoiceEvent(this.selectedVoice); + final ReaderTTSVoice selectedVoice; +} + +/* +@immutable +class SetTtsHighlightModeEvent extends TtsSettingsEvent { + const SetTtsHighlightModeEvent(this.highlightMode); + final ReadiumHighlightMode highlightMode; +} + +@immutable +class ToggleTtsHighlightModeEvent extends TtsSettingsEvent { + const ToggleTtsHighlightModeEvent(); +} + +class SetTtsSpeakPhysicalPageIndexEvent extends TtsSettingsEvent { + const SetTtsSpeakPhysicalPageIndexEvent(this.speak); + final bool speak; +} +*/ + +@immutable +class TtsSettingsState { + const TtsSettingsState({ + this.voices, + this.loaded, + this.preferredVoices, + this.highlightMode, + this.ttsSpeakPhysicalPageIndex, + }); + final List? voices; + final bool? loaded; + final List? preferredVoices; + final ReadiumHighlightMode? highlightMode; + final bool? ttsSpeakPhysicalPageIndex; + + TtsSettingsState copyWith({ + final List? voices, + final bool? loaded, + final List? preferredVoices, + final ReadiumHighlightMode? highlightMode, + final bool? ttsSpeakPhysicalPageIndex, + }) => TtsSettingsState( + voices: voices ?? this.voices, + loaded: loaded ?? this.loaded, + preferredVoices: preferredVoices ?? this.preferredVoices, + highlightMode: highlightMode ?? this.highlightMode, + ttsSpeakPhysicalPageIndex: ttsSpeakPhysicalPageIndex ?? this.ttsSpeakPhysicalPageIndex, + ); + + TtsSettingsState updateVoices(final List voices) => copyWith(voices: voices, loaded: true); + + TtsSettingsState updatePreferredVoices(final ReaderTTSVoice selectedVoice) { + final preferredVoicesList = preferredVoices ?? []; + final updatedVoices = preferredVoicesList.where((final voice) => voice.language != selectedVoice.language).toList() + ..add(selectedVoice); + + FlutterReadium().ttsSetVoice(selectedVoice.identifier, selectedVoice.language); + + return copyWith(preferredVoices: updatedVoices); + } + + /* + TtsSettingsState setHighlightMode(final ReadiumHighlightMode highlightMode) { + FlutterReadium().setHighlightMode(highlightMode); + return copyWith(highlightMode: highlightMode); + } + + TtsSettingsState setTtsSpeakPhysicalPageIndex(final bool speak) { + FlutterReadium().setTtsSpeakPhysicalPageIndex(speak: speak); + return copyWith(ttsSpeakPhysicalPageIndex: speak); + } */ +} + +class TtsSettingsBloc extends Bloc { + TtsSettingsBloc() + : super( + TtsSettingsState( + voices: [], + loaded: false, + preferredVoices: [], + highlightMode: ReadiumHighlightMode.paragraph, // to reflect default in ReadiumState + ttsSpeakPhysicalPageIndex: false, + ), + ) { + on((final event, final emit) async { + final voices = await instance.ttsGetAvailableVoices(); + emit(state.updateVoices(voices)); + }); + + on((final event, final emit) async { + await instance.ttsSetVoice(event.selectedVoice.identifier, event.selectedVoice.language); + emit(state.updatePreferredVoices(event.selectedVoice)); + }); + + /* on((final event, final emit) async { + emit(state.setHighlightMode(event.highlightMode)); + }); + + on((final event, final emit) async { + final newHighlightMode = state.highlightMode == ReadiumHighlightMode.word + ? ReadiumHighlightMode.paragraph + : ReadiumHighlightMode.word; + emit(state.setHighlightMode(newHighlightMode)); + }); + + on((final event, final emit) async { + emit(state.setTtsSpeakPhysicalPageIndex(event.speak)); + }); */ + } + + final FlutterReadium instance = FlutterReadium(); +} diff --git a/flutter_readium/example/lib/widgets/player_controls.widget.dart b/flutter_readium/example/lib/widgets/player_controls.widget.dart index b134e981..91966612 100644 --- a/flutter_readium/example/lib/widgets/player_controls.widget.dart +++ b/flutter_readium/example/lib/widgets/player_controls.widget.dart @@ -1,10 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_readium/flutter_readium.dart' show Locator; +import 'package:flutter_readium/flutter_readium.dart' show Locator, TTSPreferences; import 'package:flutter_readium_example/state/index.dart'; -import '../state/player_controls_bloc.dart'; - class PlayerControls extends StatelessWidget { const PlayerControls({super.key, required this.isAudioBook}); @@ -32,6 +30,12 @@ class PlayerControls extends StatelessWidget { onPressed: state.playing ? () => context.read().add(Pause()) : () { + TtsSettingsState ttsSettingsState = context.read().state; + + TTSPreferences ttsPreferences = TTSPreferences( + voiceIdentifier: ttsSettingsState.preferredVoices?.firstOrNull?.identifier, + ); + Locator? fakeInitialLocator; // DEMO: Start from the 3rd item in readingOrder. // final pub = context.read().state.publication; @@ -39,7 +43,9 @@ class PlayerControls extends StatelessWidget { // fakeInitialLocator = pub?.locatorFromLink(fakeInitialLink!); isAudioBook ? context.read().add(Play(fromLocator: fakeInitialLocator)) - : context.read().add(PlayTTS(fromLocator: fakeInitialLocator)); + : context.read().add( + PlayTTS(fromLocator: fakeInitialLocator, ttsPreferences: ttsPreferences), + ); }, tooltip: state.playing ? 'Pause' : 'Play', ), @@ -60,11 +66,6 @@ class PlayerControls extends StatelessWidget { onPressed: () => context.read().add(SkipToNextChapter()), tooltip: 'Skip to next chapter', ), - IconButton( - icon: const Icon(Icons.settings_voice), - onPressed: () => context.read().add(GetAvailableVoices()), - tooltip: 'Change voice', - ), ], ), ); diff --git a/flutter_readium/example/lib/widgets/tts_settings.widget.dart b/flutter_readium/example/lib/widgets/tts_settings.widget.dart index f23242e2..82acc42f 100644 --- a/flutter_readium/example/lib/widgets/tts_settings.widget.dart +++ b/flutter_readium/example/lib/widgets/tts_settings.widget.dart @@ -1,236 +1,207 @@ -// import 'dart:io'; - -// import 'package:flutter/material.dart'; -// import 'package:flutter_bloc/flutter_bloc.dart'; -// import 'package:flutter_readium/flutter_readium.dart'; - -// import '../state/tts_settings_bloc.dart'; -// import 'index.dart'; - -// class TtsSettingsWidget extends StatefulWidget { -// const TtsSettingsWidget({required this.pubLang, super.key}); - -// final List pubLang; - -// @override -// _TtsSettingsWidgetState createState() => _TtsSettingsWidgetState(); -// } - -// class _TtsSettingsWidgetState extends State { -// String? selectedLocale; -// ReadiumTtsVoice? selectedVoice; - -// @override -// void initState() { -// super.initState(); -// } - -// @override -// Widget build(final BuildContext context) => SafeArea( -// child: Wrap( -// children: [ -// Padding( -// padding: const EdgeInsets.all(20.0), -// child: Semantics( -// header: true, -// child: const Align( -// alignment: Alignment.center, -// child: Text( -// 'TTS settings', -// style: TextStyle(fontSize: 25), -// ), -// ), -// ), -// ), -// const Divider(), -// SingleChildScrollView( -// child: Column( -// children: [ -// ListItemWidget( -// label: 'Voice', -// child: _buildVoiceOptions(context), -// ), -// const Divider(), -// // TODO: Remember that it will only highlight paragraphs if google network voices are used. Implement this in the UI. -// ListItemWidget( -// label: 'Highlight', -// child: BlocBuilder( -// builder: (final context, final state) { -// final chosenVoices = _findVoicesByLangCode(state, widget.pubLang); -// final isGoogleNetworkVoice = -// chosenVoices.any((final voice) => voice.androidIsLocal == false); -// final highlightModes = isGoogleNetworkVoice -// ? [ReadiumHighlightMode.paragraph] -// : ReadiumHighlightMode.values; - -// return SingleChildScrollView( -// scrollDirection: Axis.horizontal, -// child: ToggleButtons( -// isSelected: highlightModes -// .map( -// (final mode) => mode == state.highlightMode, -// ) -// .toList(), -// selectedBorderColor: Colors.blue, -// borderWidth: 4.0, -// borderColor: Colors.transparent, -// onPressed: (final index) { -// context.read().add( -// SetTtsHighlightModeEvent( -// ReadiumHighlightMode.values[index], -// ), -// ); -// }, -// children: highlightModes -// .map( -// (final mode) => SizedBox( -// width: 120, -// child: Padding( -// padding: const EdgeInsets.symmetric(horizontal: 8.0), -// child: Center( -// child: Text( -// mode.toString().split('.').last[0].toUpperCase() + -// mode -// .toString() -// .split('.') -// .last -// .substring(1) -// .toLowerCase(), -// style: TextStyle(fontSize: 16), -// ), -// ), -// ), -// ), -// ) -// .toList(), -// ), -// ); -// }, -// ), -// ), -// const Divider(), -// ListItemWidget( -// label: 'Announce page numbers', -// isVerticalAlignment: true, -// child: BlocSelector( -// selector: (final state) => state.ttsSpeakPhysicalPageIndex ?? false, -// builder: (final context, final ttsSpeakPhysicalPageIndex) => Switch( -// value: ttsSpeakPhysicalPageIndex, -// onChanged: (final value) { -// context.read().add( -// SetTtsSpeakPhysicalPageIndexEvent(value), -// ); -// }, -// ), -// ), -// ), -// ], -// ), -// ), -// ], -// ), -// ); - -// Widget _buildVoiceOptions(final BuildContext context) { -// final ttsSettingsBloc = context.watch(); -// final state = ttsSettingsBloc.state; - -// final voices = state.voices; -// final voicesLocale = voices?.map((final voice) => voice.locale).toSet(); -// final preferredVoices = state.preferredVoices; - -// final voicesLoaded = state.loaded ?? false; - -// final preferredLocale = voicesLocale != null -// ? preferredVoices -// ?.firstWhereOrNull( -// (final preferredVoice) => voicesLocale.contains(preferredVoice.locale), -// ) -// ?.locale -// : null; - -// final showVoiceOptions = voicesLoaded && -// voices != null && -// voices.isNotEmpty && -// (selectedLocale != null || voicesLocale == null || voicesLocale.length == 1); - -// if (!voicesLoaded) return const CircularProgressIndicator(); -// if (voicesLoaded && (voices == null || voices.isEmpty)) { -// return const Text('No voices available'); -// } -// if (voicesLoaded && voices != null && voices.isNotEmpty) { -// return Row( -// children: [ -// if (voicesLocale != null && voicesLocale.length > 1) -// DropdownButton( -// value: selectedLocale ?? preferredLocale, -// onChanged: (final locale) { -// setState(() { -// selectedLocale = locale; -// selectedVoice = null; -// }); -// }, -// items: voicesLocale -// .map( -// (final locale) => DropdownMenuItem( -// value: locale, -// child: Text(locale), -// ), -// ) -// .toList(), -// ), -// if (showVoiceOptions) -// DropdownButton( -// value: selectedVoice ?? -// preferredVoices?.firstWhereOrNull( -// (final preferredVoice) => voices -// .where( -// (final voice) => voice.locale == selectedLocale || selectedLocale == null, -// ) -// .contains(preferredVoice), -// ), -// onChanged: (final voice) { -// ttsSettingsBloc.add(SetTtsVoiceEvent(voice!)); -// setState(() { -// selectedVoice = voice; -// }); -// }, -// items: voices -// .where((final voice) => voice.locale == selectedLocale || selectedLocale == null) -// .map( -// (final voice) => DropdownMenuItem( -// value: voice, -// child: Platform.isAndroid -// ? Text(_getAndroidTtsVoiceName(voice)) -// : Text(voice.name), -// ), -// ) -// .toList(), -// ), -// ], -// ); -// } -// return const Text('Something went wrong. Please try again.'); -// } -// } - -// _getAndroidTtsVoiceName(final ReadiumTtsVoice voice) { -// final name = voice.androidVoiceName ?? voice.name; -// final localOrNetwork = voice.androidIsLocal == true -// ? ' (Local)' -// : voice.androidIsLocal == false -// ? ' (Network)' -// : ''; -// return '$name$localOrNetwork'; -// } - -// List _findVoicesByLangCode( -// final TtsSettingsState state, -// final List pubLang, -// ) => -// state.preferredVoices -// ?.where( -// (final voice) => pubLang.contains(voice.langCode), -// ) -// .toList() ?? -// []; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_readium/flutter_readium.dart'; + +import '../state/tts_settings_bloc.dart'; +import 'index.dart'; + +class TtsSettingsWidget extends StatefulWidget { + const TtsSettingsWidget({required this.pubLang, super.key}); + + final List pubLang; + + @override + State createState() => _TtsSettingsWidgetState(); +} + +class _TtsSettingsWidgetState extends State { + String? selectedLanguage; + ReaderTTSVoice? selectedVoice; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(final BuildContext context) => SafeArea( + child: Wrap( + children: [ + Padding( + padding: const EdgeInsets.all(20.0), + child: Semantics( + header: true, + child: const Align( + alignment: Alignment.center, + child: Text('TTS settings', style: TextStyle(fontSize: 25)), + ), + ), + ), + const Divider(), + SingleChildScrollView( + child: Column( + children: [ + ListItemWidget(label: 'Voice', child: _buildVoiceOptions(context)), + /* + const Divider(), + // TODO: Remember that it will only highlight paragraphs if google network voices are used. Implement this in the UI. + ListItemWidget( + label: 'Highlight', + child: BlocBuilder( + builder: (final context, final state) { + final chosenVoices = _findVoicesByLangCode(state, widget.pubLang); + final isGoogleNetworkVoice = + Platform.isAndroid && chosenVoices.any((final voice) => voice.networkRequired); + final highlightModes = isGoogleNetworkVoice + ? [ReadiumHighlightMode.paragraph] + : ReadiumHighlightMode.values; + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: ToggleButtons( + isSelected: highlightModes.map((final mode) => mode == state.highlightMode).toList(), + selectedBorderColor: Colors.blue, + borderWidth: 4.0, + borderColor: Colors.transparent, + onPressed: (final index) { + context.read().add( + SetTtsHighlightModeEvent(ReadiumHighlightMode.values[index]), + ); + }, + children: highlightModes + .map( + (final mode) => SizedBox( + width: 120, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Center( + child: Text( + mode.toString().split('.').last[0].toUpperCase() + + mode.toString().split('.').last.substring(1).toLowerCase(), + style: TextStyle(fontSize: 16), + ), + ), + ), + ), + ) + .toList(), + ), + ); + }, + ), + ), + const Divider(), + ListItemWidget( + label: 'Announce page numbers', + isVerticalAlignment: true, + child: BlocSelector( + selector: (final state) => state.ttsSpeakPhysicalPageIndex ?? false, + builder: (final context, final ttsSpeakPhysicalPageIndex) => Switch( + value: ttsSpeakPhysicalPageIndex, + onChanged: (final value) { + context.read().add(SetTtsSpeakPhysicalPageIndexEvent(value)); + }, + ), + ), + ), + */ + const Divider(), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + style: ButtonStyle( + padding: WidgetStateProperty.all(const EdgeInsets.symmetric(vertical: 16.0)), + shape: WidgetStateProperty.all( + RoundedRectangleBorder(borderRadius: BorderRadius.circular(0.0)), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 8.0, + children: [ + Icon(Icons.close, size: 20), + // SizedBox(width: 10), + Text('Close', style: TextStyle(fontSize: 20)), + ], + ), + ), + ], + ), + ), + ], + ), + ); + + Widget _buildVoiceOptions(final BuildContext context) { + final ttsSettingsBloc = context.watch(); + final state = ttsSettingsBloc.state; + + final voices = state.voices; + final voiceLanguages = voices?.map((final voice) => voice.language).sortedBy((final lang) => lang).toSet(); + final preferredVoices = state.preferredVoices; + + final voicesLoaded = state.loaded ?? false; + + final preferredLanguage = voiceLanguages != null + ? preferredVoices + ?.firstWhereOrNull((final preferredVoice) => voiceLanguages.contains(preferredVoice.language)) + ?.language + : null; + + final showVoiceOptions = + voicesLoaded && + voices != null && + voices.isNotEmpty && + (selectedLanguage != null || preferredLanguage != null || voiceLanguages == null || voiceLanguages.length == 1); + + if (!voicesLoaded) return const CircularProgressIndicator(); + if (voicesLoaded && (voices == null || voices.isEmpty)) { + return const Text('No voices available'); + } + if (voicesLoaded && voices != null && voices.isNotEmpty) { + return Row( + children: [ + if (voiceLanguages != null && voiceLanguages.length > 1) + DropdownButton( + value: selectedLanguage ?? preferredLanguage, + onChanged: (final language) { + setState(() { + selectedLanguage = language; + selectedVoice = null; + }); + }, + items: voiceLanguages + .map((final language) => DropdownMenuItem(value: language, child: Text(language))) + .toList(), + ), + if (showVoiceOptions) + DropdownButton( + value: + selectedVoice ?? + preferredVoices?.firstWhereOrNull( + (final preferredVoice) => voices + .where((final voice) => voice.language == selectedLanguage || selectedLanguage == null) + .contains(preferredVoice), + ), + onChanged: (final voice) { + ttsSettingsBloc.add(SetTtsVoiceEvent(voice!)); + setState(() { + selectedVoice = voice; + }); + }, + items: voices + .where((final voice) => voice.language == selectedLanguage || selectedLanguage == null) + .map((final voice) => DropdownMenuItem(value: voice, child: Text(voice.name))) + .toList(), + ), + ], + ); + } + return const Text('Something went wrong. Please try again.'); + } +} + +List _findVoicesByLangCode(final TtsSettingsState state, final List pubLang) => + state.preferredVoices?.where((final voice) => pubLang.contains(voice.language)).toList() ?? []; diff --git a/flutter_readium_platform_interface/lib/method_channel_flutter_readium.dart b/flutter_readium_platform_interface/lib/method_channel_flutter_readium.dart index 142748f8..947ca157 100644 --- a/flutter_readium_platform_interface/lib/method_channel_flutter_readium.dart +++ b/flutter_readium_platform_interface/lib/method_channel_flutter_readium.dart @@ -136,8 +136,9 @@ class MethodChannelFlutterReadium extends FlutterReadiumPlatform { await currentReaderWidget?.applyDecorations(id, decorations); @override - Future ttsEnable(TTSPreferences? preferences) async => - await methodChannel.invokeMethod('ttsEnable', preferences?.toJson()); + Future ttsEnable(TTSPreferences? preferences) async { + await methodChannel.invokeMethod('ttsEnable', preferences?.toJson()); + } @override Future play(Locator? fromLocator) async => await methodChannel.invokeMethod('play', [fromLocator?.toJson()]);