From e06c834d0ede6eb9985fe2e17907a82fb1ea52b1 Mon Sep 17 00:00:00 2001 From: rang <378694192@qq.com> Date: Wed, 22 Oct 2025 17:58:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=AF=AD=E8=A8=80=E5=88=87?= =?UTF-8?q?=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../view/library_item_actions.dart | 65 +- .../view/library_item_metadata.dart | 33 +- .../item_viewer/view/library_item_page.dart | 16 +- .../player/view/player_when_minimized.dart | 4 +- lib/generated/intl/messages_all.dart | 72 +++ lib/generated/intl/messages_en.dart | 150 +++++ lib/generated/intl/messages_zh.dart | 104 +++ lib/generated/l10n.dart | 598 ++++++++++++++++++ lib/l10n/intl_en.arb | 87 +++ lib/l10n/intl_zh.arb | 84 +++ lib/main.dart | 11 + lib/router/router.dart | 15 +- lib/router/scaffold_with_nav_bar.dart | 214 ++++--- lib/settings/models/app_settings.dart | 28 +- lib/settings/models/app_settings.freezed.dart | 33 +- lib/settings/models/app_settings.g.dart | 2 + lib/settings/view/app_settings_page.dart | 123 ++-- lib/settings/view/buttons.dart | 5 +- lib/shared/widgets/shelves/book_shelf.dart | 44 +- pubspec.lock | 5 + pubspec.yaml | 4 + 21 files changed, 1416 insertions(+), 281 deletions(-) create mode 100644 lib/generated/intl/messages_all.dart create mode 100644 lib/generated/intl/messages_en.dart create mode 100644 lib/generated/intl/messages_zh.dart create mode 100644 lib/generated/l10n.dart create mode 100644 lib/l10n/intl_en.arb create mode 100644 lib/l10n/intl_zh.arb diff --git a/lib/features/item_viewer/view/library_item_actions.dart b/lib/features/item_viewer/view/library_item_actions.dart index fe5525f..1cf596c 100644 --- a/lib/features/item_viewer/view/library_item_actions.dart +++ b/lib/features/item_viewer/view/library_item_actions.dart @@ -18,6 +18,7 @@ import 'package:vaani/features/item_viewer/view/library_item_page.dart'; import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/features/player/providers/player_form.dart'; +import 'package:vaani/generated/l10n.dart'; import 'package:vaani/main.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/settings/api_settings_provider.dart'; @@ -76,11 +77,9 @@ class LibraryItemActions extends HookConsumerWidget { IconButton( onPressed: () { appLogger.fine('Sharing'); - var currentServerUrl = - apiSettings.activeServer!.serverUrl; + var currentServerUrl = apiSettings.activeServer!.serverUrl; if (!currentServerUrl.hasScheme) { - currentServerUrl = - Uri.https(currentServerUrl.toString()); + currentServerUrl = Uri.https(currentServerUrl.toString()); } handleLaunchUrl( Uri.parse( @@ -139,8 +138,7 @@ class LibraryItemActions extends HookConsumerWidget { FileDownloader() .database .deleteRecordWithId( - record - .task.taskId, + record.task.taskId, ); Navigator.pop(context); }, @@ -159,8 +157,7 @@ class LibraryItemActions extends HookConsumerWidget { }, onTap: () async { // open the file location - final didOpen = - await FileDownloader().openFile( + final didOpen = await FileDownloader().openFile( task: record.task, ); @@ -229,9 +226,7 @@ class LibItemDownloadButton extends HookConsumerWidget { onPressed: () { appLogger.fine('Pressed download button'); - ref - .read(downloadManagerProvider.notifier) - .queueAudioBookDownload(item); + ref.read(downloadManagerProvider.notifier).queueAudioBookDownload(item); }, icon: const Icon( Icons.download_rounded, @@ -250,10 +245,7 @@ class ItemCurrentlyInDownloadQueue extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final progress = ref - .watch(itemDownloadProgressProvider(item.id)) - .valueOrNull - ?.clamp(0.05, 1.0); + final progress = ref.watch(itemDownloadProgressProvider(item.id)).valueOrNull?.clamp(0.05, 1.0); if (progress == 1) { return AlreadyItemDownloadedButton(item: item); @@ -366,7 +358,7 @@ class DownloadSheet extends HookConsumerWidget { // }, // ), ListTile( - title: const Text('Delete'), + title: Text(S.of(context).delete), leading: const Icon( Icons.delete_rounded, ), @@ -377,28 +369,26 @@ class DownloadSheet extends HookConsumerWidget { context: context, builder: (context) { return AlertDialog( - title: const Text('Delete'), + title: Text(S.of(context).delete), content: Text( - 'Are you sure you want to delete ${item.media.metadata.title}?', + S.of(context).deleteDialog(item.media.metadata.title ?? ''), ), actions: [ TextButton( onPressed: () { // delete the file - ref - .read(downloadManagerProvider.notifier) - .deleteDownloadedItem( + ref.read(downloadManagerProvider.notifier).deleteDownloadedItem( item, ); GoRouter.of(context).pop(true); }, - child: const Text('Yes'), + child: Text(S.of(context).yes), ), TextButton( onPressed: () { GoRouter.of(context).pop(false); }, - child: const Text('No'), + child: Text(S.of(context).no), ), ], ); @@ -406,12 +396,12 @@ class DownloadSheet extends HookConsumerWidget { ); if (wasDeleted ?? false) { - appLogger.fine('Deleted ${item.media.metadata.title}'); + appLogger.fine(S.of(context).deleted(item.media.metadata.title ?? '')); GoRouter.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - 'Deleted ${item.media.metadata.title}', + S.of(context).deleted(item.media.metadata.title ?? ''), ), ), ); @@ -445,19 +435,19 @@ class _LibraryItemPlayButton extends HookConsumerWidget { if (!isCurrentBookSetInPlayer) { // either play or resume or listen again based on the progress if (isBookCompleted) { - return 'Listen Again'; + return S.of(context).homeListenAgain; } // if some progress is made, then 'continue listening' if (userMediaProgress?.progress != null) { - return 'Continue Listening'; + return S.of(context).homeContinueListening; } - return 'Start Listening'; + return S.of(context).homeStartListening; } else { // if book is set to player if (isPlayingThisBook) { - return 'Pause'; + return S.of(context).pause; } - return 'Resume'; + return S.of(context).resume; } } @@ -529,8 +519,7 @@ Future libraryItemPlayButtonOnPressed({ appLogger.info('Setting the book ${book.libraryItemId}'); appLogger.info('Initial position: ${userMediaProgress?.currentTime}'); final downloadManager = ref.watch(simpleDownloadManagerProvider); - final libItem = - await ref.read(libraryItemProvider(book.libraryItemId).future); + final libItem = await ref.read(libraryItemProvider(book.libraryItemId).future); final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem); setSourceFuture = player.setSourceAudiobook( book, @@ -546,27 +535,23 @@ Future libraryItemPlayButtonOnPressed({ } } // set the volume as this is the first time playing and dismissing causes the volume to go to 0 - var bookPlayerSettings = - ref.read(bookSettingsProvider(book.libraryItemId)).playerSettings; + var bookPlayerSettings = ref.read(bookSettingsProvider(book.libraryItemId)).playerSettings; var appPlayerSettings = ref.read(appSettingsProvider).playerSettings; - var configurePlayerForEveryBook = - appPlayerSettings.configurePlayerForEveryBook; + var configurePlayerForEveryBook = appPlayerSettings.configurePlayerForEveryBook; await Future.wait([ setSourceFuture ?? Future.value(), // set the volume player.setVolume( configurePlayerForEveryBook - ? bookPlayerSettings.preferredDefaultVolume ?? - appPlayerSettings.preferredDefaultVolume + ? bookPlayerSettings.preferredDefaultVolume ?? appPlayerSettings.preferredDefaultVolume : appPlayerSettings.preferredDefaultVolume, ), // set the speed player.setSpeed( configurePlayerForEveryBook - ? bookPlayerSettings.preferredDefaultSpeed ?? - appPlayerSettings.preferredDefaultSpeed + ? bookPlayerSettings.preferredDefaultSpeed ?? appPlayerSettings.preferredDefaultSpeed : appPlayerSettings.preferredDefaultSpeed, ), ]); diff --git a/lib/features/item_viewer/view/library_item_metadata.dart b/lib/features/item_viewer/view/library_item_metadata.dart index 664daac..0c9b9ec 100644 --- a/lib/features/item_viewer/view/library_item_metadata.dart +++ b/lib/features/item_viewer/view/library_item_metadata.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/api/library_item_provider.dart'; +import 'package:vaani/generated/l10n.dart'; import 'package:vaani/shared/extensions/model_conversions.dart'; class LibraryItemMetadata extends HookConsumerWidget { @@ -24,9 +25,8 @@ class LibraryItemMetadata extends HookConsumerWidget { if (book == null) { return null; } - final duration = book.audioFiles - .map((e) => e.duration) - .reduce((value, element) => value + element); + final duration = + book.audioFiles.map((e) => e.duration).reduce((value, element) => value + element); final hours = duration.inHours; final minutes = duration.inMinutes.remainder(60); return '${hours}h ${minutes}m'; @@ -41,10 +41,12 @@ class LibraryItemMetadata extends HookConsumerWidget { if (book == null) { return null; } - final size = book.audioFiles - .map((e) => e.metadata.size) - .reduce((value, element) => value + element); - return '${size / 1024 ~/ 1024} MB'; + final size = + book.audioFiles.map((e) => e.metadata.size).reduce((value, element) => value + element); + if (size / 1024 / 1024 < 1024) { + return '${(size / 1024 / 1024).toStringAsFixed(2)} MB'; + } + return '${(size / 1024 / 1024 / 1024).toStringAsFixed(2)} GB'; } /// will return the codec and bitrate of the book @@ -64,21 +66,21 @@ class LibraryItemMetadata extends HookConsumerWidget { // duration of the book _MetadataItem( title: switch (itemBookMetadata?.abridged) { - true => 'Abridged', - false => 'Unabridged', - _ => 'Length', + true => S.of(context).bookMetadataAbridged, + false => S.of(context).bookMetadataUnabridged, + _ => S.of(context).bookMetadataLength, }, value: getDurationFormatted() ?? 'time is just a concept', ), _MetadataItem( - title: 'Published', + title: S.of(context).bookMetadataPublished, value: itemBookMetadata?.publishedDate ?? itemBookMetadata?.publishedYear ?? - 'Unknown', + S.of(context).unknown, ), _MetadataItem( title: getCodecAndBitrate() ?? 'Codec & Bitrate', - value: getSizeFormatted() ?? 'Unknown', + value: getSizeFormatted() ?? S.of(context).unknown, ), ]; return Padding( @@ -96,10 +98,7 @@ class LibraryItemMetadata extends HookConsumerWidget { return VerticalDivider( indent: 6, endIndent: 6, - color: Theme.of(context) - .colorScheme - .onSurface - .withValues(alpha: 0.6), + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), ); }, ), diff --git a/lib/features/item_viewer/view/library_item_page.dart b/lib/features/item_viewer/view/library_item_page.dart index e7b9310..f96c9eb 100644 --- a/lib/features/item_viewer/view/library_item_page.dart +++ b/lib/features/item_viewer/view/library_item_page.dart @@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/api/library_item_provider.dart'; import 'package:vaani/features/item_viewer/view/library_item_sliver_app_bar.dart'; import 'package:vaani/features/player/view/mini_player_bottom_padding.dart'; +import 'package:vaani/generated/l10n.dart'; import 'package:vaani/router/models/library_item_extras.dart'; import 'package:vaani/shared/widgets/expandable_description.dart'; @@ -27,8 +28,7 @@ class LibraryItemPage extends HookConsumerWidget { static const double _showFabThreshold = 300.0; @override Widget build(BuildContext context, WidgetRef ref) { - final additionalItemData = - extra is LibraryItemExtras ? extra as LibraryItemExtras : null; + final additionalItemData = extra is LibraryItemExtras ? extra as LibraryItemExtras : null; final scrollController = useScrollController(); final showFab = useState(false); @@ -150,8 +150,10 @@ class LibraryItemDescription extends HookConsumerWidget { return const SizedBox(); } return ExpandableDescription( - title: 'About the Book', - content: item.media.metadata.description ?? 'Sorry, no description found', + title: S.of(context).bookAbout, + content: item.media.metadata.description ?? S.of(context).bookAboutDefault, + readMoreText: S.of(context).readMore, + readLessText: S.of(context).readLess, ); } } @@ -166,10 +168,8 @@ double calculateWidth( /// height ratio of the cover image to the available height double maxHeightToUse = 0.25, }) { - final availHeight = - min(constraints.maxHeight, MediaQuery.of(context).size.height); - final availWidth = - min(constraints.maxWidth, MediaQuery.of(context).size.width); + final availHeight = min(constraints.maxHeight, MediaQuery.of(context).size.height); + final availWidth = min(constraints.maxWidth, MediaQuery.of(context).size.width); // make the width widthRatio of the available width var width = availWidth * widthRatio; diff --git a/lib/features/player/view/player_when_minimized.dart b/lib/features/player/view/player_when_minimized.dart index 858aec1..f9c62c1 100644 --- a/lib/features/player/view/player_when_minimized.dart +++ b/lib/features/player/view/player_when_minimized.dart @@ -31,6 +31,8 @@ class PlayerWhenMinimized extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final player = ref.watch(audiobookPlayerProvider); + final currentChapter = ref.watch(currentPlayingChapterProvider); + final vanishingPercentage = 1 - percentageMiniplayer; final progress = useStream(player.slowPositionStream, initialData: Duration.zero); @@ -77,7 +79,7 @@ class PlayerWhenMinimized extends HookConsumerWidget { children: [ // AutoScrollText( Text( - bookMetaExpanded?.title ?? '', + '${bookMetaExpanded?.title ?? ''} - ${currentChapter?.title ?? ''}', maxLines: 1, overflow: TextOverflow.ellipsis, // velocity: // const Velocity(pixelsPerSecond: Offset(16, 0)), diff --git a/lib/generated/intl/messages_all.dart b/lib/generated/intl/messages_all.dart new file mode 100644 index 0000000..e1cd2d5 --- /dev/null +++ b/lib/generated/intl/messages_all.dart @@ -0,0 +1,72 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that looks up messages for specific locales by +// delegating to the appropriate library. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:implementation_imports, file_names, unnecessary_new +// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering +// ignore_for_file:argument_type_not_assignable, invalid_assignment +// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases +// ignore_for_file:comment_references + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; +import 'package:intl/src/intl_helpers.dart'; + +import 'messages_en.dart' as messages_en; +import 'messages_zh.dart' as messages_zh; + +typedef Future LibraryLoader(); +Map _deferredLibraries = { + 'en': () => new SynchronousFuture(null), + 'zh': () => new SynchronousFuture(null), +}; + +MessageLookupByLibrary? _findExact(String localeName) { + switch (localeName) { + case 'en': + return messages_en.messages; + case 'zh': + return messages_zh.messages; + default: + return null; + } +} + +/// User programs should call this before using [localeName] for messages. +Future initializeMessages(String localeName) { + var availableLocale = Intl.verifiedLocale( + localeName, + (locale) => _deferredLibraries[locale] != null, + onFailure: (_) => null, + ); + if (availableLocale == null) { + return new SynchronousFuture(false); + } + var lib = _deferredLibraries[availableLocale]; + lib == null ? new SynchronousFuture(false) : lib(); + initializeInternalMessageLookup(() => new CompositeMessageLookup()); + messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); + return new SynchronousFuture(true); +} + +bool _messagesExistFor(String locale) { + try { + return _findExact(locale) != null; + } catch (e) { + return false; + } +} + +MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { + var actualLocale = Intl.verifiedLocale( + locale, + _messagesExistFor, + onFailure: (_) => null, + ); + if (actualLocale == null) return null; + return _findExact(actualLocale); +} diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart new file mode 100644 index 0000000..45d33d9 --- /dev/null +++ b/lib/generated/intl/messages_en.dart @@ -0,0 +1,150 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that provides messages for a en locale. All the +// messages from the main program should be duplicated here with the same +// function name. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new +// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering +// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases +// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes +// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; + +final messages = new MessageLookup(); + +typedef String MessageIfAbsent(String messageStr, List args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'en'; + + static String m0(item) => "Are you sure you want to delete ${item}?"; + + static String m1(item) => "Deleted ${item}"; + + final messages = _notInlinedMessages(_notInlinedMessages); + static Map _notInlinedMessages(_) => { + "appSettings": MessageLookupByLibrary.simpleMessage("App Settings"), + "appearance": MessageLookupByLibrary.simpleMessage("Appearance"), + "autoTurnOnSleepTimer": MessageLookupByLibrary.simpleMessage( + "Auto Turn On Sleep Timer", + ), + "automaticallyDescription": MessageLookupByLibrary.simpleMessage( + "Automatically turn on the sleep timer based on the time of day", + ), + "backup": MessageLookupByLibrary.simpleMessage("Backup"), + "backupAndRestore": MessageLookupByLibrary.simpleMessage( + "Backup and Restore", + ), + "bookAbout": MessageLookupByLibrary.simpleMessage("About the Book"), + "bookAboutDefault": MessageLookupByLibrary.simpleMessage( + "Sorry, no description found", + ), + "bookMetadataAbridged": MessageLookupByLibrary.simpleMessage("Abridged"), + "bookMetadataLength": MessageLookupByLibrary.simpleMessage("Length"), + "bookMetadataPublished": MessageLookupByLibrary.simpleMessage("Published"), + "bookMetadataUnabridged": MessageLookupByLibrary.simpleMessage( + "Unabridged", + ), + "cancel": MessageLookupByLibrary.simpleMessage("Cancel"), + "copyToClipboard": MessageLookupByLibrary.simpleMessage( + "Copy to Clipboard", + ), + "copyToClipboardDescription": MessageLookupByLibrary.simpleMessage( + "Copy the app settings to the clipboard", + ), + "copyToClipboardToast": MessageLookupByLibrary.simpleMessage( + "Settings copied to clipboard", + ), + "delete": MessageLookupByLibrary.simpleMessage("Delete"), + "deleteDialog": m0, + "deleted": m1, + "explore": MessageLookupByLibrary.simpleMessage("explore"), + "exploreTooltip": MessageLookupByLibrary.simpleMessage( + "Search and Explore", + ), + "general": MessageLookupByLibrary.simpleMessage("General"), + "home": MessageLookupByLibrary.simpleMessage("Home"), + "homeContinueListening": MessageLookupByLibrary.simpleMessage( + "Continue Listening", + ), + "homeListenAgain": MessageLookupByLibrary.simpleMessage("Listen Again"), + "homePageSettings": MessageLookupByLibrary.simpleMessage( + "Home Page Settings", + ), + "homePageSettingsDescription": MessageLookupByLibrary.simpleMessage( + "Customize the home page", + ), + "homeStartListening": MessageLookupByLibrary.simpleMessage( + "Start Listening", + ), + "language": MessageLookupByLibrary.simpleMessage("Language"), + "languageDescription": MessageLookupByLibrary.simpleMessage( + "Language switch", + ), + "library": MessageLookupByLibrary.simpleMessage("Library"), + "libraryTooltip": MessageLookupByLibrary.simpleMessage( + "Browse your library", + ), + "no": MessageLookupByLibrary.simpleMessage("No"), + "notificationMediaPlayer": MessageLookupByLibrary.simpleMessage( + "Notification Media Player", + ), + "notificationMediaPlayerDescription": MessageLookupByLibrary.simpleMessage( + "Customize the media player in notifications", + ), + "ok": MessageLookupByLibrary.simpleMessage("OK"), + "pause": MessageLookupByLibrary.simpleMessage("Pause"), + "play": MessageLookupByLibrary.simpleMessage("Play"), + "playerSettings": MessageLookupByLibrary.simpleMessage("Player Settings"), + "playerSettingsDescription": MessageLookupByLibrary.simpleMessage( + "Customize the player settings", + ), + "readLess": MessageLookupByLibrary.simpleMessage("Read Less"), + "readMore": MessageLookupByLibrary.simpleMessage("Read More"), + "reset": MessageLookupByLibrary.simpleMessage("Reset"), + "resetAppSettings": MessageLookupByLibrary.simpleMessage( + "Reset App Settings", + ), + "resetAppSettingsDescription": MessageLookupByLibrary.simpleMessage( + "Reset the app settings to the default values", + ), + "resetAppSettingsDialog": MessageLookupByLibrary.simpleMessage( + "Are you sure you want to reset the app settings?", + ), + "restore": MessageLookupByLibrary.simpleMessage("Restore"), + "restoreBackup": MessageLookupByLibrary.simpleMessage("Restore Backup"), + "restoreBackupHint": MessageLookupByLibrary.simpleMessage( + "Paste the backup here", + ), + "restoreBackupInvalid": MessageLookupByLibrary.simpleMessage( + "Invalid backup", + ), + "restoreBackupSuccess": MessageLookupByLibrary.simpleMessage( + "Settings restored", + ), + "restoreBackupValidator": MessageLookupByLibrary.simpleMessage( + "Please paste the backup here", + ), + "restoreDescription": MessageLookupByLibrary.simpleMessage( + "Restore the app settings from the backup", + ), + "resume": MessageLookupByLibrary.simpleMessage("Resume"), + "shakeDetector": MessageLookupByLibrary.simpleMessage("Shake Detector"), + "shakeDetectorDescription": MessageLookupByLibrary.simpleMessage( + "Customize the shake detector settings", + ), + "themeSettings": MessageLookupByLibrary.simpleMessage("Theme Settings"), + "themeSettingsDescription": MessageLookupByLibrary.simpleMessage( + "Customize the app theme", + ), + "unknown": MessageLookupByLibrary.simpleMessage("Unknown"), + "yes": MessageLookupByLibrary.simpleMessage("Yes"), + "you": MessageLookupByLibrary.simpleMessage("You"), + "youTooltip": MessageLookupByLibrary.simpleMessage( + "Your Profile and Settings", + ), + }; +} diff --git a/lib/generated/intl/messages_zh.dart b/lib/generated/intl/messages_zh.dart new file mode 100644 index 0000000..5423b62 --- /dev/null +++ b/lib/generated/intl/messages_zh.dart @@ -0,0 +1,104 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that provides messages for a zh locale. All the +// messages from the main program should be duplicated here with the same +// function name. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new +// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering +// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases +// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes +// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; + +final messages = new MessageLookup(); + +typedef String MessageIfAbsent(String messageStr, List args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'zh'; + + static String m0(item) => "确定要删除 ${item} 吗?"; + + static String m1(item) => "已删除 ${item}"; + + final messages = _notInlinedMessages(_notInlinedMessages); + static Map _notInlinedMessages(_) => { + "appSettings": MessageLookupByLibrary.simpleMessage("应用设置"), + "appearance": MessageLookupByLibrary.simpleMessage("外观"), + "autoTurnOnSleepTimer": MessageLookupByLibrary.simpleMessage("自动开启睡眠定时器"), + "automaticallyDescription": MessageLookupByLibrary.simpleMessage( + "根据一天中的时间自动打开睡眠定时器", + ), + "backup": MessageLookupByLibrary.simpleMessage("备份"), + "backupAndRestore": MessageLookupByLibrary.simpleMessage("备份与恢复"), + "bookAbout": MessageLookupByLibrary.simpleMessage("关于本书"), + "bookAboutDefault": MessageLookupByLibrary.simpleMessage("抱歉,找不到描述"), + "cancel": MessageLookupByLibrary.simpleMessage("取消"), + "copyToClipboard": MessageLookupByLibrary.simpleMessage("复制到剪贴板"), + "copyToClipboardDescription": MessageLookupByLibrary.simpleMessage( + "将应用程序设置复制到剪贴板", + ), + "copyToClipboardToast": MessageLookupByLibrary.simpleMessage("设置已复制到剪贴板"), + "delete": MessageLookupByLibrary.simpleMessage("Delete"), + "deleteDialog": m0, + "deleted": m1, + "explore": MessageLookupByLibrary.simpleMessage("探索"), + "exploreTooltip": MessageLookupByLibrary.simpleMessage("搜索和探索"), + "general": MessageLookupByLibrary.simpleMessage("通用"), + "home": MessageLookupByLibrary.simpleMessage("首页"), + "homeContinueListening": MessageLookupByLibrary.simpleMessage("继续收听"), + "homeListenAgain": MessageLookupByLibrary.simpleMessage("再听一遍"), + "homePageSettings": MessageLookupByLibrary.simpleMessage("主页设置"), + "homePageSettingsDescription": MessageLookupByLibrary.simpleMessage( + "自定义主页", + ), + "homeStartListening": MessageLookupByLibrary.simpleMessage("开始收听"), + "language": MessageLookupByLibrary.simpleMessage("语言"), + "languageDescription": MessageLookupByLibrary.simpleMessage("语言切换"), + "library": MessageLookupByLibrary.simpleMessage("媒体库"), + "libraryTooltip": MessageLookupByLibrary.simpleMessage("浏览您的媒体库"), + "no": MessageLookupByLibrary.simpleMessage("否"), + "notificationMediaPlayer": MessageLookupByLibrary.simpleMessage("通知媒体播放器"), + "notificationMediaPlayerDescription": MessageLookupByLibrary.simpleMessage( + "在通知中自定义媒体播放器", + ), + "ok": MessageLookupByLibrary.simpleMessage("确定"), + "pause": MessageLookupByLibrary.simpleMessage("暂停"), + "play": MessageLookupByLibrary.simpleMessage("播放"), + "playerSettings": MessageLookupByLibrary.simpleMessage("播放器设置"), + "playerSettingsDescription": MessageLookupByLibrary.simpleMessage( + "自定义播放器设置", + ), + "readLess": MessageLookupByLibrary.simpleMessage("折叠"), + "readMore": MessageLookupByLibrary.simpleMessage("展开"), + "reset": MessageLookupByLibrary.simpleMessage("重置"), + "resetAppSettings": MessageLookupByLibrary.simpleMessage("重置应用程序设置"), + "resetAppSettingsDescription": MessageLookupByLibrary.simpleMessage( + "将应用程序设置重置为默认值", + ), + "resetAppSettingsDialog": MessageLookupByLibrary.simpleMessage( + "您确定要重置应用程序设置吗?", + ), + "restore": MessageLookupByLibrary.simpleMessage("恢复"), + "restoreBackup": MessageLookupByLibrary.simpleMessage("恢复备份"), + "restoreBackupHint": MessageLookupByLibrary.simpleMessage("将备份粘贴到此处"), + "restoreBackupInvalid": MessageLookupByLibrary.simpleMessage("无效备份"), + "restoreBackupSuccess": MessageLookupByLibrary.simpleMessage("设置已恢复"), + "restoreBackupValidator": MessageLookupByLibrary.simpleMessage("请将备份粘贴到此处"), + "restoreDescription": MessageLookupByLibrary.simpleMessage("从备份中还原应用程序设置"), + "resume": MessageLookupByLibrary.simpleMessage("继续"), + "shakeDetector": MessageLookupByLibrary.simpleMessage("抖动检测器"), + "shakeDetectorDescription": MessageLookupByLibrary.simpleMessage( + "自定义抖动检测器设置", + ), + "themeSettings": MessageLookupByLibrary.simpleMessage("主题设置"), + "themeSettingsDescription": MessageLookupByLibrary.simpleMessage("自定义应用主题"), + "unknown": MessageLookupByLibrary.simpleMessage("未知"), + "yes": MessageLookupByLibrary.simpleMessage("是"), + "you": MessageLookupByLibrary.simpleMessage("我的"), + "youTooltip": MessageLookupByLibrary.simpleMessage("您的个人资料和设置"), + }; +} diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart new file mode 100644 index 0000000..f2952a1 --- /dev/null +++ b/lib/generated/l10n.dart @@ -0,0 +1,598 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'intl/messages_all.dart'; + +// ************************************************************************** +// Generator: Flutter Intl IDE plugin +// Made by Localizely +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars +// ignore_for_file: join_return_with_assignment, prefer_final_in_for_each +// ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes + +class S { + S(); + + static S? _current; + + static S get current { + assert( + _current != null, + 'No instance of S was loaded. Try to initialize the S delegate before accessing S.current.', + ); + return _current!; + } + + static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); + + static Future load(Locale locale) { + final name = (locale.countryCode?.isEmpty ?? false) + ? locale.languageCode + : locale.toString(); + final localeName = Intl.canonicalizedLocale(name); + return initializeMessages(localeName).then((_) { + Intl.defaultLocale = localeName; + final instance = S(); + S._current = instance; + + return instance; + }); + } + + static S of(BuildContext context) { + final instance = S.maybeOf(context); + assert( + instance != null, + 'No instance of S present in the widget tree. Did you add S.delegate in localizationsDelegates?', + ); + return instance!; + } + + static S? maybeOf(BuildContext context) { + return Localizations.of(context, S); + } + + /// `Yes` + String get yes { + return Intl.message('Yes', name: 'yes', desc: '', args: []); + } + + /// `No` + String get no { + return Intl.message('No', name: 'no', desc: '', args: []); + } + + /// `OK` + String get ok { + return Intl.message('OK', name: 'ok', desc: '', args: []); + } + + /// `Cancel` + String get cancel { + return Intl.message('Cancel', name: 'cancel', desc: '', args: []); + } + + /// `Reset` + String get reset { + return Intl.message('Reset', name: 'reset', desc: '', args: []); + } + + /// `Delete` + String get delete { + return Intl.message('Delete', name: 'delete', desc: '', args: []); + } + + /// `Are you sure you want to delete {item}?` + String deleteDialog(String item) { + return Intl.message( + 'Are you sure you want to delete $item?', + name: 'deleteDialog', + desc: '', + args: [item], + ); + } + + /// `Deleted {item}` + String deleted(String item) { + return Intl.message( + 'Deleted $item', + name: 'deleted', + desc: '已删除 {}', + args: [item], + ); + } + + /// `Pause` + String get pause { + return Intl.message('Pause', name: 'pause', desc: '', args: []); + } + + /// `Play` + String get play { + return Intl.message('Play', name: 'play', desc: '', args: []); + } + + /// `Resume` + String get resume { + return Intl.message('Resume', name: 'resume', desc: '', args: []); + } + + /// `Unknown` + String get unknown { + return Intl.message('Unknown', name: 'unknown', desc: '', args: []); + } + + /// `Read More` + String get readMore { + return Intl.message('Read More', name: 'readMore', desc: '', args: []); + } + + /// `Read Less` + String get readLess { + return Intl.message('Read Less', name: 'readLess', desc: '', args: []); + } + + /// `Home` + String get home { + return Intl.message('Home', name: 'home', desc: '', args: []); + } + + /// `Listen Again` + String get homeListenAgain { + return Intl.message( + 'Listen Again', + name: 'homeListenAgain', + desc: '', + args: [], + ); + } + + /// `Continue Listening` + String get homeContinueListening { + return Intl.message( + 'Continue Listening', + name: 'homeContinueListening', + desc: '', + args: [], + ); + } + + /// `Start Listening` + String get homeStartListening { + return Intl.message( + 'Start Listening', + name: 'homeStartListening', + desc: '', + args: [], + ); + } + + /// `About the Book` + String get bookAbout { + return Intl.message( + 'About the Book', + name: 'bookAbout', + desc: '', + args: [], + ); + } + + /// `Sorry, no description found` + String get bookAboutDefault { + return Intl.message( + 'Sorry, no description found', + name: 'bookAboutDefault', + desc: '', + args: [], + ); + } + + /// `Abridged` + String get bookMetadataAbridged { + return Intl.message( + 'Abridged', + name: 'bookMetadataAbridged', + desc: '', + args: [], + ); + } + + /// `Unabridged` + String get bookMetadataUnabridged { + return Intl.message( + 'Unabridged', + name: 'bookMetadataUnabridged', + desc: '', + args: [], + ); + } + + /// `Length` + String get bookMetadataLength { + return Intl.message( + 'Length', + name: 'bookMetadataLength', + desc: '', + args: [], + ); + } + + /// `Published` + String get bookMetadataPublished { + return Intl.message( + 'Published', + name: 'bookMetadataPublished', + desc: '', + args: [], + ); + } + + /// `Library` + String get library { + return Intl.message('Library', name: 'library', desc: '', args: []); + } + + /// `Browse your library` + String get libraryTooltip { + return Intl.message( + 'Browse your library', + name: 'libraryTooltip', + desc: '', + args: [], + ); + } + + /// `explore` + String get explore { + return Intl.message('explore', name: 'explore', desc: '', args: []); + } + + /// `Search and Explore` + String get exploreTooltip { + return Intl.message( + 'Search and Explore', + name: 'exploreTooltip', + desc: '', + args: [], + ); + } + + /// `You` + String get you { + return Intl.message('You', name: 'you', desc: '', args: []); + } + + /// `Your Profile and Settings` + String get youTooltip { + return Intl.message( + 'Your Profile and Settings', + name: 'youTooltip', + desc: '', + args: [], + ); + } + + /// `App Settings` + String get appSettings { + return Intl.message( + 'App Settings', + name: 'appSettings', + desc: '', + args: [], + ); + } + + /// `General` + String get general { + return Intl.message('General', name: 'general', desc: '', args: []); + } + + /// `Language` + String get language { + return Intl.message('Language', name: 'language', desc: '', args: []); + } + + /// `Language switch` + String get languageDescription { + return Intl.message( + 'Language switch', + name: 'languageDescription', + desc: '', + args: [], + ); + } + + /// `Player Settings` + String get playerSettings { + return Intl.message( + 'Player Settings', + name: 'playerSettings', + desc: '', + args: [], + ); + } + + /// `Customize the player settings` + String get playerSettingsDescription { + return Intl.message( + 'Customize the player settings', + name: 'playerSettingsDescription', + desc: '', + args: [], + ); + } + + /// `Auto Turn On Sleep Timer` + String get autoTurnOnSleepTimer { + return Intl.message( + 'Auto Turn On Sleep Timer', + name: 'autoTurnOnSleepTimer', + desc: '', + args: [], + ); + } + + /// `Automatically turn on the sleep timer based on the time of day` + String get automaticallyDescription { + return Intl.message( + 'Automatically turn on the sleep timer based on the time of day', + name: 'automaticallyDescription', + desc: '', + args: [], + ); + } + + /// `Shake Detector` + String get shakeDetector { + return Intl.message( + 'Shake Detector', + name: 'shakeDetector', + desc: '', + args: [], + ); + } + + /// `Customize the shake detector settings` + String get shakeDetectorDescription { + return Intl.message( + 'Customize the shake detector settings', + name: 'shakeDetectorDescription', + desc: '', + args: [], + ); + } + + /// `Appearance` + String get appearance { + return Intl.message('Appearance', name: 'appearance', desc: '', args: []); + } + + /// `Theme Settings` + String get themeSettings { + return Intl.message( + 'Theme Settings', + name: 'themeSettings', + desc: '', + args: [], + ); + } + + /// `Customize the app theme` + String get themeSettingsDescription { + return Intl.message( + 'Customize the app theme', + name: 'themeSettingsDescription', + desc: '', + args: [], + ); + } + + /// `Notification Media Player` + String get notificationMediaPlayer { + return Intl.message( + 'Notification Media Player', + name: 'notificationMediaPlayer', + desc: '', + args: [], + ); + } + + /// `Customize the media player in notifications` + String get notificationMediaPlayerDescription { + return Intl.message( + 'Customize the media player in notifications', + name: 'notificationMediaPlayerDescription', + desc: '', + args: [], + ); + } + + /// `Home Page Settings` + String get homePageSettings { + return Intl.message( + 'Home Page Settings', + name: 'homePageSettings', + desc: '', + args: [], + ); + } + + /// `Customize the home page` + String get homePageSettingsDescription { + return Intl.message( + 'Customize the home page', + name: 'homePageSettingsDescription', + desc: '', + args: [], + ); + } + + /// `Backup and Restore` + String get backupAndRestore { + return Intl.message( + 'Backup and Restore', + name: 'backupAndRestore', + desc: '', + args: [], + ); + } + + /// `Copy to Clipboard` + String get copyToClipboard { + return Intl.message( + 'Copy to Clipboard', + name: 'copyToClipboard', + desc: '', + args: [], + ); + } + + /// `Copy the app settings to the clipboard` + String get copyToClipboardDescription { + return Intl.message( + 'Copy the app settings to the clipboard', + name: 'copyToClipboardDescription', + desc: '', + args: [], + ); + } + + /// `Settings copied to clipboard` + String get copyToClipboardToast { + return Intl.message( + 'Settings copied to clipboard', + name: 'copyToClipboardToast', + desc: '', + args: [], + ); + } + + /// `Restore` + String get restore { + return Intl.message('Restore', name: 'restore', desc: '', args: []); + } + + /// `Restore the app settings from the backup` + String get restoreDescription { + return Intl.message( + 'Restore the app settings from the backup', + name: 'restoreDescription', + desc: '', + args: [], + ); + } + + /// `Restore Backup` + String get restoreBackup { + return Intl.message( + 'Restore Backup', + name: 'restoreBackup', + desc: '', + args: [], + ); + } + + /// `Backup` + String get backup { + return Intl.message('Backup', name: 'backup', desc: '', args: []); + } + + /// `Paste the backup here` + String get restoreBackupHint { + return Intl.message( + 'Paste the backup here', + name: 'restoreBackupHint', + desc: '', + args: [], + ); + } + + /// `Please paste the backup here` + String get restoreBackupValidator { + return Intl.message( + 'Please paste the backup here', + name: 'restoreBackupValidator', + desc: '', + args: [], + ); + } + + /// `Invalid backup` + String get restoreBackupInvalid { + return Intl.message( + 'Invalid backup', + name: 'restoreBackupInvalid', + desc: '', + args: [], + ); + } + + /// `Settings restored` + String get restoreBackupSuccess { + return Intl.message( + 'Settings restored', + name: 'restoreBackupSuccess', + desc: '', + args: [], + ); + } + + /// `Reset App Settings` + String get resetAppSettings { + return Intl.message( + 'Reset App Settings', + name: 'resetAppSettings', + desc: '', + args: [], + ); + } + + /// `Reset the app settings to the default values` + String get resetAppSettingsDescription { + return Intl.message( + 'Reset the app settings to the default values', + name: 'resetAppSettingsDescription', + desc: '', + args: [], + ); + } + + /// `Are you sure you want to reset the app settings?` + String get resetAppSettingsDialog { + return Intl.message( + 'Are you sure you want to reset the app settings?', + name: 'resetAppSettingsDialog', + desc: '', + args: [], + ); + } +} + +class AppLocalizationDelegate extends LocalizationsDelegate { + const AppLocalizationDelegate(); + + List get supportedLocales { + return const [ + Locale.fromSubtags(languageCode: 'en'), + Locale.fromSubtags(languageCode: 'zh'), + ]; + } + + @override + bool isSupported(Locale locale) => _isSupported(locale); + @override + Future load(Locale locale) => S.load(locale); + @override + bool shouldReload(AppLocalizationDelegate old) => false; + + bool _isSupported(Locale locale) { + for (var supportedLocale in supportedLocales) { + if (supportedLocale.languageCode == locale.languageCode) { + return true; + } + } + return false; + } +} diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb new file mode 100644 index 0000000..f5c5ca8 --- /dev/null +++ b/lib/l10n/intl_en.arb @@ -0,0 +1,87 @@ +{ + "@@locale": "en", + + "yes": "Yes", + "no": "No", + "ok": "OK", + "cancel": "Cancel", + "reset": "Reset", + "delete": "Delete", + "deleteDialog": "Are you sure you want to delete {item}?", + "@deleteDialog": { + "placeholders": { + "item": { + "type": "String" + } + } + }, + "deleted": "Deleted {item}", + "@deleted": { + "description": "已删除 {}", + "placeholders": { + "item": { + "type": "String" + } + } + }, + + "pause": "Pause", + "play": "Play", + "resume": "Resume", + "unknown": "Unknown", + "readMore": "Read More", + "readLess": "Read Less", + + "home": "Home", + "homeListenAgain": "Listen Again", + "homeContinueListening": "Continue Listening", + "homeStartListening": "Start Listening", + "bookAbout": "About the Book", + "bookAboutDefault": "Sorry, no description found", + "bookMetadataAbridged": "Abridged", + "bookMetadataUnabridged": "Unabridged", + "bookMetadataLength": "Length", + "bookMetadataPublished": "Published", + + "library": "Library", + "libraryTooltip": "Browse your library", + "explore": "explore", + "exploreTooltip": "Search and Explore", + "you": "You", + "youTooltip": "Your Profile and Settings", + + "appSettings": "App Settings", + "general": "General", + "language": "Language", + "languageDescription": "Language switch", + "playerSettings": "Player Settings", + "playerSettingsDescription": "Customize the player settings", + "autoTurnOnSleepTimer": "Auto Turn On Sleep Timer", + "automaticallyDescription": "Automatically turn on the sleep timer based on the time of day", + "shakeDetector": "Shake Detector", + "shakeDetectorDescription": "Customize the shake detector settings", + "appearance": "Appearance", + "themeSettings": "Theme Settings", + "themeSettingsDescription": "Customize the app theme", + "notificationMediaPlayer": "Notification Media Player", + "notificationMediaPlayerDescription": "Customize the media player in notifications", + "homePageSettings": "Home Page Settings", + "homePageSettingsDescription": "Customize the home page", + "backupAndRestore": "Backup and Restore", + "copyToClipboard": "Copy to Clipboard", + "copyToClipboardDescription": "Copy the app settings to the clipboard", + "copyToClipboardToast": "Settings copied to clipboard", + "restore": "Restore", + "restoreDescription": "Restore the app settings from the backup", + "restoreBackup": "Restore Backup", + "backup": "Backup", + "restoreBackupHint": "Paste the backup here", + "restoreBackupValidator": "Please paste the backup here", + "restoreBackupInvalid": "Invalid backup", + "restoreBackupSuccess": "Settings restored", + + "resetAppSettings": "Reset App Settings", + "resetAppSettingsDescription": "Reset the app settings to the default values", + "resetAppSettingsDialog": "Are you sure you want to reset the app settings?" + +} \ No newline at end of file diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb new file mode 100644 index 0000000..6d404cb --- /dev/null +++ b/lib/l10n/intl_zh.arb @@ -0,0 +1,84 @@ +{ + "@@locale": "zh", + + "yes": "是", + "no": "否", + "ok": "确定", + "cancel": "取消", + "reset": "重置", + "delete": "Delete", + "deleteDialog": "确定要删除 {item} 吗?", + "@deleteDialog": { + "placeholders": { + "item": { + "type": "String" + } + } + }, + "deleted": "已删除 {item}", + "@deleted": { + "description": "已删除 {}", + "placeholders": { + "item": { + "type": "String" + } + } + }, + + "pause": "暂停", + "play": "播放", + "resume": "继续", + "unknown": "未知", + "readMore": "展开", + "readLess": "折叠", + + "home": "首页", + "homeListenAgain": "再听一遍", + "homeContinueListening": "继续收听", + "homeStartListening": "开始收听", + "bookAbout": "关于本书", + "bookAboutDefault": "抱歉,找不到描述", + + "library": "媒体库", + "libraryTooltip": "浏览您的媒体库", + "explore": "探索", + "exploreTooltip": "搜索和探索", + "you": "我的", + "youTooltip": "您的个人资料和设置", + + "appSettings": "应用设置", + "general": "通用", + "language": "语言", + "languageDescription": "语言切换", + "playerSettings": "播放器设置", + "playerSettingsDescription": "自定义播放器设置", + "autoTurnOnSleepTimer": "自动开启睡眠定时器", + "automaticallyDescription": "根据一天中的时间自动打开睡眠定时器", + + "shakeDetector": "抖动检测器", + "shakeDetectorDescription": "自定义抖动检测器设置", + "appearance": "外观", + "themeSettings": "主题设置", + "themeSettingsDescription": "自定义应用主题", + "notificationMediaPlayer": "通知媒体播放器", + "notificationMediaPlayerDescription": "在通知中自定义媒体播放器", + "homePageSettings": "主页设置", + "homePageSettingsDescription": "自定义主页", + "backupAndRestore": "备份与恢复", + "copyToClipboard": "复制到剪贴板", + "copyToClipboardDescription": "将应用程序设置复制到剪贴板", + "copyToClipboardToast": "设置已复制到剪贴板", + "restore": "恢复", + "restoreDescription": "从备份中还原应用程序设置", + "restoreBackup": "恢复备份", + "backup": "备份", + "restoreBackupHint": "将备份粘贴到此处", + "restoreBackupValidator": "请将备份粘贴到此处", + "restoreBackupInvalid": "无效备份", + "restoreBackupSuccess": "设置已恢复", + + "resetAppSettings": "重置应用程序设置", + "resetAppSettingsDescription": "将应用程序设置重置为默认值", + "resetAppSettingsDialog": "您确定要重置应用程序设置吗?" + +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 6c4de6f..19acd7a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:logging/logging.dart'; import 'package:vaani/api/server_provider.dart'; @@ -12,6 +13,7 @@ import 'package:vaani/features/player/providers/audiobook_player.dart' show audiobookPlayerProvider, simpleAudiobookPlayerProvider; import 'package:vaani/features/shake_detection/providers/shake_detector.dart'; import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart'; +import 'package:vaani/generated/l10n.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/settings/app_settings_provider.dart'; @@ -126,6 +128,15 @@ class MyApp extends ConsumerWidget { try { return MaterialApp.router( // debugShowCheckedModeBanner: false, + locale: Locale(appSettings.language), + localizationsDelegates: [ + // 以下是其他代理 + S.delegate, //String 资源的 本地化 + GlobalMaterialLocalizations.delegate, //Material Widgets 的本地化 + GlobalWidgetsLocalizations.delegate, //基础 Widgets 的本地化 + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: S.delegate.supportedLocales, theme: appThemeLight, darkTheme: appThemeDark, themeMode: themeSettings.themeMode, diff --git a/lib/router/router.dart b/lib/router/router.dart index eda348e..9f48250 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -25,8 +25,7 @@ import 'transitions/slide.dart'; part 'constants.dart'; -final GlobalKey rootNavigatorKey = - GlobalKey(debugLabel: 'root'); +final GlobalKey rootNavigatorKey = GlobalKey(debugLabel: 'root'); final GlobalKey sectionHomeNavigatorKey = GlobalKey(debugLabel: 'HomeNavigator'); @@ -94,10 +93,8 @@ class MyAppRouter { // itemId: itemId, extra: state.extra); // }, pageBuilder: (context, state) { - final itemId = state - .pathParameters[Routes.libraryItem.pathParamName]!; - final child = - LibraryItemPage(itemId: itemId, extra: state.extra); + final itemId = state.pathParameters[Routes.libraryItem.pathParamName]!; + final child = LibraryItemPage(itemId: itemId, extra: state.extra); return buildPageWithDefaultTransition( context: context, state: state, @@ -204,8 +201,7 @@ class MyAppRouter { GoRoute( path: Routes.playerSettings.pathName, name: Routes.playerSettings.name, - pageBuilder: - defaultPageBuilder(const PlayerSettingsPage()), + pageBuilder: defaultPageBuilder(const PlayerSettingsPage()), ), GoRoute( path: Routes.shakeDetectorSettings.pathName, @@ -253,8 +249,7 @@ class MyAppRouter { final stateParam = state.uri.queryParameters['state']; appLogger.fine('deep linking callback: code: $code, state: $stateParam'); - var callbackPage = - CallbackPage(code: code, state: stateParam, key: ValueKey(stateParam)); + var callbackPage = CallbackPage(code: code, state: stateParam, key: ValueKey(stateParam)); return buildPageWithDefaultTransition( context: context, state: state, diff --git a/lib/router/scaffold_with_nav_bar.dart b/lib/router/scaffold_with_nav_bar.dart index 510c5ac..43511d6 100644 --- a/lib/router/scaffold_with_nav_bar.dart +++ b/lib/router/scaffold_with_nav_bar.dart @@ -10,6 +10,7 @@ import 'package:vaani/features/player/providers/player_form.dart'; import 'package:vaani/features/player/view/audiobook_player.dart'; import 'package:vaani/features/player/view/player_when_expanded.dart'; import 'package:vaani/features/you/view/widgets/library_switch_chip.dart'; +import 'package:vaani/generated/l10n.dart'; import 'package:vaani/main.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/shared/icons/abs_icons.dart' show AbsIcons; @@ -36,7 +37,7 @@ class ScaffoldWithNavBar extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final playerProgress = ref.watch(playerHeightProvider); - final isMobile = Platform.isAndroid || Platform.isIOS; + final isMobile = Platform.isFuchsia || Platform.isAndroid || Platform.isIOS; onBackButtonPressed() async { final isPlayerExpanded = playerProgress != playerMinHeight; @@ -90,75 +91,72 @@ class ScaffoldWithNavBar extends HookConsumerWidget { return BackButtonListener( onBackButtonPressed: onBackButtonPressed, child: Scaffold( - body: isMobile - ? Stack( - children: [ - navigationShell, - const AudiobookPlayer(), - ], - ) - : buildNavLeft(context, ref), + body: Stack( + children: [ + isMobile ? navigationShell : buildNavLeft(context, ref), + const AudiobookPlayer(), + ], + ), bottomNavigationBar: isMobile ? buildNavBottom(context, ref) : null, ), ); } Widget buildNavLeft(BuildContext context, WidgetRef ref) { - return Row( - children: [ - SafeArea( - child: NavigationRail( - minWidth: 60, - minExtendedWidth: 120, - extended: MediaQuery.of(context).size.width > 640, - // extended: false, - destinations: _navigationItems.map((item) { - final isDestinationLibrary = item.name == 'Library'; - var currentLibrary = ref.watch(currentLibraryProvider).valueOrNull; - final libraryIcon = AbsIcons.getIconByName( - currentLibrary?.icon, - ); - final destinationWidget = NavigationRailDestination( - icon: Icon( - isDestinationLibrary ? libraryIcon ?? item.icon : item.icon, - ), - selectedIcon: Icon( - isDestinationLibrary ? libraryIcon ?? item.activeIcon : item.activeIcon, - ), - label: Text(isDestinationLibrary ? currentLibrary?.name ?? item.name : item.name), - // tooltip: item.tooltip, - ); - // if (isDestinationLibrary) { - // return GestureDetector( - // onSecondaryTap: () => showLibrarySwitcher(context, ref), - // onDoubleTap: () => showLibrarySwitcher(context, ref), - // child: - // destinationWidget, // Wrap the actual NavigationDestination - // ); - // } else { - // // Return the unwrapped destination for other items - // return destinationWidget; - // } - return destinationWidget; - // return NavigationRailDestination(icon: Icon(nav.icon), label: Text(nav.name)); - }).toList(), - selectedIndex: navigationShell.currentIndex, - onDestinationSelected: (int index) { - print(index); - _onTap(context, index, ref); - }, + final isPlayerActive = ref.watch(isPlayerActiveProvider); + return Padding( + padding: EdgeInsets.only(bottom: isPlayerActive ? playerMinHeight : 0), + child: Row( + children: [ + SafeArea( + child: NavigationRail( + minWidth: 60, + minExtendedWidth: 120, + extended: MediaQuery.of(context).size.width > 640, + // extended: false, + destinations: _navigationItems(context).map((item) { + final isDestinationLibrary = item.name == S.of(context).library; + var currentLibrary = ref.watch(currentLibraryProvider).valueOrNull; + final libraryIcon = AbsIcons.getIconByName( + currentLibrary?.icon, + ); + final destinationWidget = NavigationRailDestination( + icon: Icon( + isDestinationLibrary ? libraryIcon ?? item.icon : item.icon, + ), + selectedIcon: Icon( + isDestinationLibrary ? libraryIcon ?? item.activeIcon : item.activeIcon, + ), + label: Text(isDestinationLibrary ? currentLibrary?.name ?? item.name : item.name), + // tooltip: item.tooltip, + ); + // if (isDestinationLibrary) { + // return GestureDetector( + // onSecondaryTap: () => showLibrarySwitcher(context, ref), + // onDoubleTap: () => showLibrarySwitcher(context, ref), + // child: + // destinationWidget, // Wrap the actual NavigationDestination + // ); + // } else { + // // Return the unwrapped destination for other items + // return destinationWidget; + // } + return destinationWidget; + // return NavigationRailDestination(icon: Icon(nav.icon), label: Text(nav.name)); + }).toList(), + selectedIndex: navigationShell.currentIndex, + onDestinationSelected: (int index) { + print(index); + _onTap(context, index, ref); + }, + ), ), - ), - VerticalDivider(width: 0.5, thickness: 0.5), - Expanded( - child: Stack( - children: [ - navigationShell, - const AudiobookPlayer(), - ], + VerticalDivider(width: 0.5, thickness: 0.5), + Expanded( + child: navigationShell, ), - ), - ], + ], + ), ); } @@ -184,8 +182,8 @@ class ScaffoldWithNavBar extends HookConsumerWidget { // world scenario, the items would most likely be generated from the // branches of the shell route, which can be fetched using // `navigationShell.route.branches`. - destinations: _navigationItems.map((item) { - final isDestinationLibrary = item.name == 'Library'; + destinations: _navigationItems(context).map((item) { + final isDestinationLibrary = item.name == S.of(context).library; var currentLibrary = ref.watch(currentLibraryProvider).valueOrNull; final libraryIcon = AbsIcons.getIconByName( currentLibrary?.icon, @@ -217,6 +215,42 @@ class ScaffoldWithNavBar extends HookConsumerWidget { ); } + List<_NavigationItem> _navigationItems(BuildContext context) { + return [ + _NavigationItem( + // name: 'Home', + name: S.of(context).home, + icon: Icons.home_outlined, + activeIcon: Icons.home, + ), + // Library + _NavigationItem( + // name: 'Library', + name: S.of(context).library, + icon: Icons.book_outlined, + activeIcon: Icons.book, + // tooltip: 'Browse your library', + tooltip: S.of(context).exploreTooltip, + ), + _NavigationItem( + // name: 'Explore', + name: S.of(context).explore, + icon: Icons.search_outlined, + activeIcon: Icons.search, + // tooltip: 'Search and Explore', + tooltip: S.of(context).exploreTooltip, + ), + _NavigationItem( + // name: 'You', + name: S.of(context).you, + icon: Icons.account_circle_outlined, + activeIcon: Icons.account_circle, + // tooltip: 'Your Profile and Settings', + tooltip: S.of(context).youTooltip, + ), + ]; + } + /// Navigate to the current location of the branch at the provided index when /// tapping an item in the BottomNavigationBar. void _onTap(BuildContext context, int index, WidgetRef ref) { @@ -267,32 +301,32 @@ class ScaffoldWithNavBar extends HookConsumerWidget { // list of constants with names and icons so that they can be used in the bottom navigation bar // and reused for nav rail and other places -const _navigationItems = [ - _NavigationItem( - name: 'Home', - icon: Icons.home_outlined, - activeIcon: Icons.home, - ), - // Library - _NavigationItem( - name: 'Library', - icon: Icons.book_outlined, - activeIcon: Icons.book, - tooltip: 'Browse your library', - ), - _NavigationItem( - name: 'Explore', - icon: Icons.search_outlined, - activeIcon: Icons.search, - tooltip: 'Search and Explore', - ), - _NavigationItem( - name: 'You', - icon: Icons.account_circle_outlined, - activeIcon: Icons.account_circle, - tooltip: 'Your Profile and Settings', - ), -]; +// const _navigationItems = [ +// _NavigationItem( +// name: 'Home', +// icon: Icons.home_outlined, +// activeIcon: Icons.home, +// ), +// // Library +// _NavigationItem( +// name: 'Library', +// icon: Icons.book_outlined, +// activeIcon: Icons.book, +// tooltip: 'Browse your library', +// ), +// _NavigationItem( +// name: 'Explore', +// icon: Icons.search_outlined, +// activeIcon: Icons.search, +// tooltip: 'Search and Explore', +// ), +// _NavigationItem( +// name: 'You', +// icon: Icons.account_circle_outlined, +// activeIcon: Icons.account_circle, +// tooltip: 'Your Profile and Settings', +// ), +// ]; class _NavigationItem { const _NavigationItem({ diff --git a/lib/settings/models/app_settings.dart b/lib/settings/models/app_settings.dart index fe1f7be..350e2d0 100644 --- a/lib/settings/models/app_settings.dart +++ b/lib/settings/models/app_settings.dart @@ -12,18 +12,17 @@ part 'app_settings.g.dart'; @freezed class AppSettings with _$AppSettings { const factory AppSettings({ + @Default('zh') String language, @Default(ThemeSettings()) ThemeSettings themeSettings, @Default(PlayerSettings()) PlayerSettings playerSettings, @Default(SleepTimerSettings()) SleepTimerSettings sleepTimerSettings, @Default(DownloadSettings()) DownloadSettings downloadSettings, @Default(NotificationSettings()) NotificationSettings notificationSettings, - @Default(ShakeDetectionSettings()) - ShakeDetectionSettings shakeDetectionSettings, + @Default(ShakeDetectionSettings()) ShakeDetectionSettings shakeDetectionSettings, @Default(HomePageSettings()) HomePageSettings homePageSettings, }) = _AppSettings; - factory AppSettings.fromJson(Map json) => - _$AppSettingsFromJson(json); + factory AppSettings.fromJson(Map json) => _$AppSettingsFromJson(json); } @freezed @@ -37,17 +36,14 @@ class ThemeSettings with _$ThemeSettings { @Default(true) bool useCurrentPlayerThemeThroughoutApp, }) = _ThemeSettings; - factory ThemeSettings.fromJson(Map json) => - _$ThemeSettingsFromJson(json); + factory ThemeSettings.fromJson(Map json) => _$ThemeSettingsFromJson(json); } @freezed class PlayerSettings with _$PlayerSettings { const factory PlayerSettings({ - @Default(MinimizedPlayerSettings()) - MinimizedPlayerSettings miniPlayerSettings, - @Default(ExpandedPlayerSettings()) - ExpandedPlayerSettings expandedPlayerSettings, + @Default(MinimizedPlayerSettings()) MinimizedPlayerSettings miniPlayerSettings, + @Default(ExpandedPlayerSettings()) ExpandedPlayerSettings expandedPlayerSettings, @Default(1) double preferredDefaultVolume, @Default(1) double preferredDefaultSpeed, @Default([1, 1.25, 1.5, 1.75, 2]) List speedOptions, @@ -60,8 +56,7 @@ class PlayerSettings with _$PlayerSettings { @Default(true) bool configurePlayerForEveryBook, }) = _PlayerSettings; - factory PlayerSettings.fromJson(Map json) => - _$PlayerSettingsFromJson(json); + factory PlayerSettings.fromJson(Map json) => _$PlayerSettingsFromJson(json); } @freezed @@ -144,8 +139,7 @@ class DownloadSettings with _$DownloadSettings { @Default(3) int maxConcurrentByGroup, }) = _DownloadSettings; - factory DownloadSettings.fromJson(Map json) => - _$DownloadSettingsFromJson(json); + factory DownloadSettings.fromJson(Map json) => _$DownloadSettingsFromJson(json); } @freezed @@ -202,8 +196,7 @@ class ShakeDetectionSettings with _$ShakeDetectionSettings { @Default(ShakeDirection.horizontal) ShakeDirection direction, @Default(5) double threshold, @Default(ShakeAction.resetSleepTimer) ShakeAction shakeAction, - @Default({ShakeDetectedFeedback.vibrate}) - Set feedback, + @Default({ShakeDetectedFeedback.vibrate}) Set feedback, @Default(0.5) double beepVolume, /// the duration to wait before the shake detection is enabled again @@ -241,6 +234,5 @@ class HomePageSettings with _$HomePageSettings { @Default(false) bool showPlayButtonOnListenAgainShelf, }) = _HomePageSettings; - factory HomePageSettings.fromJson(Map json) => - _$HomePageSettingsFromJson(json); + factory HomePageSettings.fromJson(Map json) => _$HomePageSettingsFromJson(json); } diff --git a/lib/settings/models/app_settings.freezed.dart b/lib/settings/models/app_settings.freezed.dart index 17dea47..d0363e7 100644 --- a/lib/settings/models/app_settings.freezed.dart +++ b/lib/settings/models/app_settings.freezed.dart @@ -20,6 +20,7 @@ AppSettings _$AppSettingsFromJson(Map json) { /// @nodoc mixin _$AppSettings { + String get language => throw _privateConstructorUsedError; ThemeSettings get themeSettings => throw _privateConstructorUsedError; PlayerSettings get playerSettings => throw _privateConstructorUsedError; SleepTimerSettings get sleepTimerSettings => @@ -48,7 +49,8 @@ abstract class $AppSettingsCopyWith<$Res> { _$AppSettingsCopyWithImpl<$Res, AppSettings>; @useResult $Res call( - {ThemeSettings themeSettings, + {String language, + ThemeSettings themeSettings, PlayerSettings playerSettings, SleepTimerSettings sleepTimerSettings, DownloadSettings downloadSettings, @@ -80,6 +82,7 @@ class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings> @pragma('vm:prefer-inline') @override $Res call({ + Object? language = null, Object? themeSettings = null, Object? playerSettings = null, Object? sleepTimerSettings = null, @@ -89,6 +92,10 @@ class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings> Object? homePageSettings = null, }) { return _then(_value.copyWith( + language: null == language + ? _value.language + : language // ignore: cast_nullable_to_non_nullable + as String, themeSettings: null == themeSettings ? _value.themeSettings : themeSettings // ignore: cast_nullable_to_non_nullable @@ -203,7 +210,8 @@ abstract class _$$AppSettingsImplCopyWith<$Res> @override @useResult $Res call( - {ThemeSettings themeSettings, + {String language, + ThemeSettings themeSettings, PlayerSettings playerSettings, SleepTimerSettings sleepTimerSettings, DownloadSettings downloadSettings, @@ -240,6 +248,7 @@ class __$$AppSettingsImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ + Object? language = null, Object? themeSettings = null, Object? playerSettings = null, Object? sleepTimerSettings = null, @@ -249,6 +258,10 @@ class __$$AppSettingsImplCopyWithImpl<$Res> Object? homePageSettings = null, }) { return _then(_$AppSettingsImpl( + language: null == language + ? _value.language + : language // ignore: cast_nullable_to_non_nullable + as String, themeSettings: null == themeSettings ? _value.themeSettings : themeSettings // ignore: cast_nullable_to_non_nullable @@ -285,7 +298,8 @@ class __$$AppSettingsImplCopyWithImpl<$Res> @JsonSerializable() class _$AppSettingsImpl implements _AppSettings { const _$AppSettingsImpl( - {this.themeSettings = const ThemeSettings(), + {this.language = 'zh', + this.themeSettings = const ThemeSettings(), this.playerSettings = const PlayerSettings(), this.sleepTimerSettings = const SleepTimerSettings(), this.downloadSettings = const DownloadSettings(), @@ -296,6 +310,9 @@ class _$AppSettingsImpl implements _AppSettings { factory _$AppSettingsImpl.fromJson(Map json) => _$$AppSettingsImplFromJson(json); + @override + @JsonKey() + final String language; @override @JsonKey() final ThemeSettings themeSettings; @@ -320,7 +337,7 @@ class _$AppSettingsImpl implements _AppSettings { @override String toString() { - return 'AppSettings(themeSettings: $themeSettings, playerSettings: $playerSettings, sleepTimerSettings: $sleepTimerSettings, downloadSettings: $downloadSettings, notificationSettings: $notificationSettings, shakeDetectionSettings: $shakeDetectionSettings, homePageSettings: $homePageSettings)'; + return 'AppSettings(language: $language, themeSettings: $themeSettings, playerSettings: $playerSettings, sleepTimerSettings: $sleepTimerSettings, downloadSettings: $downloadSettings, notificationSettings: $notificationSettings, shakeDetectionSettings: $shakeDetectionSettings, homePageSettings: $homePageSettings)'; } @override @@ -328,6 +345,8 @@ class _$AppSettingsImpl implements _AppSettings { return identical(this, other) || (other.runtimeType == runtimeType && other is _$AppSettingsImpl && + (identical(other.language, language) || + other.language == language) && (identical(other.themeSettings, themeSettings) || other.themeSettings == themeSettings) && (identical(other.playerSettings, playerSettings) || @@ -348,6 +367,7 @@ class _$AppSettingsImpl implements _AppSettings { @override int get hashCode => Object.hash( runtimeType, + language, themeSettings, playerSettings, sleepTimerSettings, @@ -374,7 +394,8 @@ class _$AppSettingsImpl implements _AppSettings { abstract class _AppSettings implements AppSettings { const factory _AppSettings( - {final ThemeSettings themeSettings, + {final String language, + final ThemeSettings themeSettings, final PlayerSettings playerSettings, final SleepTimerSettings sleepTimerSettings, final DownloadSettings downloadSettings, @@ -385,6 +406,8 @@ abstract class _AppSettings implements AppSettings { factory _AppSettings.fromJson(Map json) = _$AppSettingsImpl.fromJson; + @override + String get language; @override ThemeSettings get themeSettings; @override diff --git a/lib/settings/models/app_settings.g.dart b/lib/settings/models/app_settings.g.dart index 131458c..2f7e09f 100644 --- a/lib/settings/models/app_settings.g.dart +++ b/lib/settings/models/app_settings.g.dart @@ -8,6 +8,7 @@ part of 'app_settings.dart'; _$AppSettingsImpl _$$AppSettingsImplFromJson(Map json) => _$AppSettingsImpl( + language: json['language'] as String? ?? 'zh', themeSettings: json['themeSettings'] == null ? const ThemeSettings() : ThemeSettings.fromJson( @@ -40,6 +41,7 @@ _$AppSettingsImpl _$$AppSettingsImplFromJson(Map json) => Map _$$AppSettingsImplToJson(_$AppSettingsImpl instance) => { + 'language': instance.language, 'themeSettings': instance.themeSettings, 'playerSettings': instance.playerSettings, 'sleepTimerSettings': instance.sleepTimerSettings, diff --git a/lib/settings/view/app_settings_page.dart b/lib/settings/view/app_settings_page.dart index 15386fe..fe8f5e9 100644 --- a/lib/settings/view/app_settings_page.dart +++ b/lib/settings/view/app_settings_page.dart @@ -7,6 +7,7 @@ import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:material_symbols_icons/symbols.dart'; +import 'package:vaani/generated/l10n.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/settings/models/app_settings.dart' as model; @@ -18,14 +19,13 @@ class AppSettingsPage extends HookConsumerWidget { const AppSettingsPage({ super.key, }); - @override Widget build(BuildContext context, WidgetRef ref) { final appSettings = ref.watch(appSettingsProvider); final sleepTimerSettings = appSettings.sleepTimerSettings; - + final locales = {'en': 'English', 'zh': '中文'}; return SimpleSettingsPage( - title: const Text('App Settings'), + title: Text(S.of(context).appSettings), sections: [ // General section SettingsSection( @@ -34,25 +34,42 @@ class AppSettingsPage extends HookConsumerWidget { vertical: 8.0, ), title: Text( - 'General', + S.of(context).general, style: Theme.of(context).textTheme.titleLarge, ), tiles: [ SettingsTile( - title: const Text('Player Settings'), + title: Text(S.of(context).language), leading: const Icon(Icons.play_arrow), - description: const Text( - 'Customize the player settings', + trailing: DropdownButton( + value: appSettings.language, + items: S.delegate.supportedLocales.map((locale) { + return DropdownMenuItem( + value: locale.languageCode, + child: Text(locales[locale.languageCode] ?? 'unknown'), + ); + }).toList(), + onChanged: (value) { + ref.read(appSettingsProvider.notifier).update( + appSettings.copyWith( + language: value!, + ), + ); + }, ), + description: Text(S.of(context).languageDescription), + ), + SettingsTile( + title: Text(S.of(context).playerSettings), + leading: const Icon(Icons.play_arrow), + description: Text(S.of(context).playerSettingsDescription), onPressed: (context) { context.pushNamed(Routes.playerSettings.name); }, ), NavigationWithSwitchTile( - title: const Text('Auto Turn On Sleep Timer'), - description: const Text( - 'Automatically turn on the sleep timer based on the time of day', - ), + title: Text(S.of(context).autoTurnOnSleepTimer), + description: Text(S.of(context).automaticallyDescription), leading: sleepTimerSettings.autoTurnOnTimer ? const Icon(Symbols.time_auto, fill: 1) : const Icon(Symbols.timer_off, fill: 1), @@ -69,10 +86,10 @@ class AppSettingsPage extends HookConsumerWidget { }, ), NavigationWithSwitchTile( - title: const Text('Shake Detector'), + title: Text(S.of(context).shakeDetector), leading: const Icon(Icons.vibration), - description: const Text( - 'Customize the shake detector settings', + description: Text( + S.of(context).shakeDetectorDescription, ), value: appSettings.shakeDetectionSettings.isEnabled, onPressed: (context) { @@ -96,36 +113,30 @@ class AppSettingsPage extends HookConsumerWidget { vertical: 8.0, ), title: Text( - 'Appearance', + S.of(context).appearance, style: Theme.of(context).textTheme.titleLarge, ), tiles: [ SettingsTile.navigation( leading: const Icon(Icons.color_lens), - title: const Text('Theme Settings'), - description: const Text( - 'Customize the app theme', - ), + title: Text(S.of(context).themeSettings), + description: Text(S.of(context).themeSettingsDescription), onPressed: (context) { context.pushNamed(Routes.themeSettings.name); }, ), SettingsTile( - title: const Text('Notification Media Player'), + title: Text(S.of(context).notificationMediaPlayer), leading: const Icon(Icons.play_lesson), - description: const Text( - 'Customize the media player in notifications', - ), + description: Text(S.of(context).notificationMediaPlayerDescription), onPressed: (context) { context.pushNamed(Routes.notificationSettings.name); }, ), SettingsTile.navigation( leading: const Icon(Icons.home_filled), - title: const Text('Home Page Settings'), - description: const Text( - 'Customize the home page', - ), + title: Text(S.of(context).homePageSettings), + description: Text(S.of(context).homePageSettingsDescription), onPressed: (context) { context.pushNamed(Routes.homePageSettings.name); }, @@ -140,15 +151,15 @@ class AppSettingsPage extends HookConsumerWidget { vertical: 8.0, ), title: Text( - 'Backup and Restore', + S.of(context).backupAndRestore, style: Theme.of(context).textTheme.titleLarge, ), tiles: [ SettingsTile( - title: const Text('Copy to Clipboard'), + title: Text(S.of(context).copyToClipboard), leading: const Icon(Icons.copy), - description: const Text( - 'Copy the app settings to the clipboard', + description: Text( + S.of(context).copyToClipboardDescription, ), onPressed: (context) async { // copy to clipboard @@ -159,18 +170,16 @@ class AppSettingsPage extends HookConsumerWidget { ); // show toast ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Settings copied to clipboard'), + SnackBar( + content: Text(S.of(context).copyToClipboardToast), ), ); }, ), SettingsTile( - title: const Text('Restore'), + title: Text(S.of(context).restore), leading: const Icon(Icons.restore), - description: const Text( - 'Restore the app settings from the backup', - ), + description: Text(S.of(context).restoreDescription), onPressed: (context) { // show a dialog to get the backup showDialog( @@ -184,33 +193,29 @@ class AppSettingsPage extends HookConsumerWidget { // a button to reset the app settings SettingsTile( - title: const Text('Reset App Settings'), + title: Text(S.of(context).resetAppSettings), leading: const Icon(Icons.settings_backup_restore), - description: const Text( - 'Reset the app settings to the default values', - ), + description: Text(S.of(context).resetAppSettingsDescription), onPressed: (context) async { // confirm the reset final res = await showDialog( context: context, builder: (context) { return AlertDialog( - title: const Text('Reset App Settings'), - content: const Text( - 'Are you sure you want to reset the app settings?', - ), + title: Text(S.of(context).resetAppSettings), + content: Text(S.of(context).resetAppSettingsDialog), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(false); }, - child: const Text('Cancel'), + child: Text(S.of(context).cancel), ), TextButton( onPressed: () { Navigator.of(context).pop(true); }, - child: const Text('Reset'), + child: Text(S.of(context).reset), ), ], ); @@ -242,14 +247,14 @@ class RestoreDialogue extends HookConsumerWidget { final settingsInputController = useTextEditingController(); return AlertDialog( - title: const Text('Restore Backup'), + title: Text(S.of(context).restoreBackup), content: Form( key: formKey, child: TextFormField( autofocus: true, decoration: InputDecoration( - labelText: 'Backup', - hintText: 'Paste the backup here', + labelText: S.of(context).backup, + hintText: S.of(context).restoreBackupHint, // clear button suffixIcon: IconButton( icon: Icon(Icons.clear), @@ -260,7 +265,7 @@ class RestoreDialogue extends HookConsumerWidget { ), validator: (value) { if (value == null || value.isEmpty) { - return 'Please paste the backup here'; + return S.of(context).restoreBackupValidator; } try { // try to decode the backup @@ -268,7 +273,7 @@ class RestoreDialogue extends HookConsumerWidget { jsonDecode(value), ); } catch (e) { - return 'Invalid backup'; + return S.of(context).restoreBackupInvalid; } return null; }, @@ -281,8 +286,8 @@ class RestoreDialogue extends HookConsumerWidget { if (formKey.currentState!.validate()) { if (settings.value == null) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Invalid backup'), + SnackBar( + content: Text(S.of(context).restoreBackupInvalid), ), ); return; @@ -291,19 +296,19 @@ class RestoreDialogue extends HookConsumerWidget { settingsInputController.clear(); Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Settings restored'), + SnackBar( + content: Text(S.of(context).restoreBackupSuccess), ), ); } else { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Invalid backup'), + SnackBar( + content: Text(S.of(context).restoreBackupInvalid), ), ); } }, - child: const Text('Restore'), + child: Text(S.of(context).restore), ), ], ); diff --git a/lib/settings/view/buttons.dart b/lib/settings/view/buttons.dart index 4a15b41..77fb1a0 100644 --- a/lib/settings/view/buttons.dart +++ b/lib/settings/view/buttons.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:vaani/generated/l10n.dart'; class OkButton extends StatelessWidget { const OkButton({ @@ -12,7 +13,7 @@ class OkButton extends StatelessWidget { Widget build(BuildContext context) { return TextButton( onPressed: onPressed, - child: const Text('OK'), + child: Text(S.of(context).ok), ); } } @@ -32,7 +33,7 @@ class CancelButton extends StatelessWidget { onPressed?.call(); Navigator.of(context).pop(); }, - child: const Text('Cancel'), + child: Text(S.of(context).cancel), ); } } diff --git a/lib/shared/widgets/shelves/book_shelf.dart b/lib/shared/widgets/shelves/book_shelf.dart index 4fad4be..13e7ebc 100644 --- a/lib/shared/widgets/shelves/book_shelf.dart +++ b/lib/shared/widgets/shelves/book_shelf.dart @@ -99,8 +99,7 @@ class BookOnShelf extends HookConsumerWidget { onTap: handleTapOnBook, borderRadius: BorderRadius.circular(10), child: Padding( - padding: - const EdgeInsets.only(bottom: 8.0, right: 4.0, left: 4.0), + padding: const EdgeInsets.only(bottom: 8.0, right: 4.0, left: 4.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -112,9 +111,7 @@ class BookOnShelf extends HookConsumerWidget { alignment: Alignment.bottomRight, children: [ Hero( - tag: HeroTagPrefixes.bookCover + - item.id + - heroTagSuffix, + tag: HeroTagPrefixes.bookCover + item.id + heroTagSuffix, child: ClipRRect( borderRadius: BorderRadius.circular(10), child: AnimatedSwitcher( @@ -128,17 +125,13 @@ class BookOnShelf extends HookConsumerWidget { var imageWidget = Image.memory( image, fit: BoxFit.fill, - cacheWidth: (height * - 1.2 * - MediaQuery.of(context) - .devicePixelRatio) - .round(), + cacheWidth: + (height * 1.2 * MediaQuery.of(context).devicePixelRatio) + .round(), ); return Container( decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, + color: Theme.of(context).colorScheme.onPrimaryContainer, ), child: imageWidget, ); @@ -213,8 +206,7 @@ class _BookOnShelfPlayButton extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final me = ref.watch(meProvider); final player = ref.watch(audiobookPlayerProvider); - final isCurrentBookSetInPlayer = - player.book?.libraryItemId == libraryItemId; + final isCurrentBookSetInPlayer = player.book?.libraryItemId == libraryItemId; final isPlayingThisBook = player.playing && isCurrentBookSetInPlayer; final userProgress = me.valueOrNull?.mediaProgress @@ -242,8 +234,7 @@ class _BookOnShelfPlayButton extends HookConsumerWidget { return Theme( // if current book is set in player, get theme from the cover image data: ThemeData( - colorScheme: - coverColorScheme.valueOrNull ?? Theme.of(context).colorScheme, + colorScheme: coverColorScheme.valueOrNull ?? Theme.of(context).colorScheme, ), child: Padding( padding: EdgeInsets.all(strokeWidth / 2 + 2), @@ -258,10 +249,7 @@ class _BookOnShelfPlayButton extends HookConsumerWidget { child: CircularProgressIndicator( value: userProgress.progress, strokeWidth: strokeWidth, - backgroundColor: Theme.of(context) - .colorScheme - .onPrimary - .withValues(alpha: 0.8), + backgroundColor: Theme.of(context).colorScheme.onPrimary.withValues(alpha: 0.8), valueColor: AlwaysStoppedAnimation( Theme.of(context).colorScheme.primary, ), @@ -279,15 +267,11 @@ class _BookOnShelfPlayButton extends HookConsumerWidget { const Size(size, size), ), backgroundColor: WidgetStateProperty.all( - Theme.of(context) - .colorScheme - .onPrimary - .withValues(alpha: 0.9), + Theme.of(context).colorScheme.onPrimary.withValues(alpha: 0.9), ), ), onPressed: () async { - final book = - await ref.watch(libraryItemProvider(libraryItemId).future); + final book = await ref.watch(libraryItemProvider(libraryItemId).future); libraryItemPlayButtonOnPressed( ref: ref, @@ -324,10 +308,8 @@ class BookCoverSkeleton extends StatelessWidget { child: SizedBox( width: 150, child: Shimmer.fromColors( - baseColor: - Theme.of(context).colorScheme.surface.withValues(alpha: 0.3), - highlightColor: - Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.1), + baseColor: Theme.of(context).colorScheme.surface.withValues(alpha: 0.3), + highlightColor: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.1), child: Container( color: Theme.of(context).colorScheme.surface, ), diff --git a/pubspec.lock b/pubspec.lock index 86706e8..95f73d9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -507,6 +507,11 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index daa3a2e..27551e1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -96,6 +96,8 @@ dependencies: shimmer: ^3.0.0 url_launcher: ^6.2.6 vibration: ^3.1.3 + flutter_localizations: + sdk: flutter dev_dependencies: build_runner: ^2.4.9 custom_lint: ^0.7.0 @@ -152,3 +154,5 @@ flutter: - family: AbsIcons fonts: - asset: assets/fonts/AbsIcons.ttf +flutter_intl: + enabled: true