From 90b30707dfef2572599e907aec51ca6120fab25b Mon Sep 17 00:00:00 2001 From: liang-work Date: Sun, 4 Jan 2026 22:07:13 +0800 Subject: [PATCH 01/16] =?UTF-8?q?=E2=9E=95Added=20localization=20dependenc?= =?UTF-8?q?ies=20and=20also=20upgraded=20some=20plugins.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pubspec.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 9da184a..be7ae76 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,8 +34,11 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + flutter_localizations: + sdk: flutter + intl: any flutter_hooks: ^0.21.3+1 - hooks_riverpod: ^3.0.3 + hooks_riverpod: ^3.1.0 media_kit: ^1.2.6 media_kit_libs_audio: ^1.0.7 drift: ^2.30.0 @@ -43,7 +46,7 @@ dependencies: sqlite3_flutter_libs: ^0.5.41 path_provider: ^2.1.5 path: ^1.9.1 - riverpod_annotation: ^3.0.3 + riverpod_annotation: ^4.0.0 file_picker: ^10.3.8 flutter_media_metadata: ^1.0.0+1 animations: ^2.1.1 @@ -88,6 +91,7 @@ flutter: # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true + generate: true # To add assets to your application, add an assets section, like this: assets: From c17d2d7fc0953141bdb745a00d7effff09ce313b Mon Sep 17 00:00:00 2001 From: liang-work Date: Sun, 4 Jan 2026 22:08:36 +0800 Subject: [PATCH 02/16] =?UTF-8?q?=E2=9C=A8=20Supports=20localized=20langua?= =?UTF-8?q?ges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/playlist_repository.g.dart | 7 +- lib/data/track_repository.g.dart | 7 +- lib/l10n/app_en.arb | 342 ++++++++ lib/l10n/app_localizations.dart | 968 +++++++++++++++++++++ lib/l10n/app_localizations_en.dart | 494 +++++++++++ lib/l10n/app_localizations_zh.dart | 482 ++++++++++ lib/l10n/app_zh.arb | 342 ++++++++ lib/main.dart | 15 + lib/providers/audio_provider.g.dart | 25 +- lib/providers/db_provider.g.dart | 4 +- lib/providers/locale_provider.dart | 19 + lib/providers/locale_provider.g.dart | 62 ++ lib/providers/lrc_fetcher_provider.g.dart | 7 +- lib/providers/settings_provider.g.dart | 51 +- lib/providers/theme_provider.g.dart | 26 +- lib/ui/screens/album_detail_screen.dart | 9 +- lib/ui/screens/library_screen.dart | 5 +- lib/ui/screens/player_screen.dart | 9 +- lib/ui/screens/playlist_detail_screen.dart | 9 +- lib/ui/screens/settings_screen.dart | 50 ++ lib/ui/tabs/albums_tab.dart | 3 +- lib/ui/tabs/playlists_tab.dart | 17 +- lib/ui/widgets/mini_player.dart | 14 +- lib/ui/widgets/track_tile.dart | 3 +- 24 files changed, 2868 insertions(+), 102 deletions(-) create mode 100644 lib/l10n/app_en.arb create mode 100644 lib/l10n/app_localizations.dart create mode 100644 lib/l10n/app_localizations_en.dart create mode 100644 lib/l10n/app_localizations_zh.dart create mode 100644 lib/l10n/app_zh.arb create mode 100644 lib/providers/locale_provider.dart create mode 100644 lib/providers/locale_provider.g.dart diff --git a/lib/data/playlist_repository.g.dart b/lib/data/playlist_repository.g.dart index 043a9f6..5ba4e77 100644 --- a/lib/data/playlist_repository.g.dart +++ b/lib/data/playlist_repository.g.dart @@ -10,11 +10,11 @@ part of 'playlist_repository.dart'; // ignore_for_file: type=lint, type=warning @ProviderFor(PlaylistRepository) -const playlistRepositoryProvider = PlaylistRepositoryProvider._(); +final playlistRepositoryProvider = PlaylistRepositoryProvider._(); final class PlaylistRepositoryProvider extends $AsyncNotifierProvider { - const PlaylistRepositoryProvider._() + PlaylistRepositoryProvider._() : super( from: null, argument: null, @@ -41,7 +41,6 @@ abstract class _$PlaylistRepository extends $AsyncNotifier { @$mustCallSuper @override void runBuild() { - build(); final ref = this.ref as $Ref, void>; final element = ref.element @@ -51,6 +50,6 @@ abstract class _$PlaylistRepository extends $AsyncNotifier { Object?, Object? >; - element.handleValue(ref, null); + element.handleCreate(ref, build); } } diff --git a/lib/data/track_repository.g.dart b/lib/data/track_repository.g.dart index 130c1ae..a81859f 100644 --- a/lib/data/track_repository.g.dart +++ b/lib/data/track_repository.g.dart @@ -10,11 +10,11 @@ part of 'track_repository.dart'; // ignore_for_file: type=lint, type=warning @ProviderFor(TrackRepository) -const trackRepositoryProvider = TrackRepositoryProvider._(); +final trackRepositoryProvider = TrackRepositoryProvider._(); final class TrackRepositoryProvider extends $AsyncNotifierProvider { - const TrackRepositoryProvider._() + TrackRepositoryProvider._() : super( from: null, argument: null, @@ -40,7 +40,6 @@ abstract class _$TrackRepository extends $AsyncNotifier { @$mustCallSuper @override void runBuild() { - build(); final ref = this.ref as $Ref, void>; final element = ref.element @@ -50,6 +49,6 @@ abstract class _$TrackRepository extends $AsyncNotifier { Object?, Object? >; - element.handleValue(ref, null); + element.handleCreate(ref, build); } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..206521a --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,342 @@ +{ + "noMediaSelected": "No media selected", + "noLyricsAvailable": "No Lyrics Available", + "fetchLyrics": "Fetch Lyrics", + "searchLyricsWith": "Search lyrics with {searchTerm}", + "@searchLyricsWith": { + "placeholders": { + "searchTerm": { + "type": "String" + } + } + }, + "whereToSearchLyrics": "Where do you want to search lyrics from?", + "musixmatch": "Musixmatch", + "netease": "NetEase", + "lrclib": "Lrclib", + "manualImport": "Manual Import", + "cancel": "Cancel", + "lyricsOptions": "Lyrics Options", + "refetch": "Re-fetch", + "clear": "Clear", + "liveSyncLyrics": "Live Sync Lyrics", + "manualOffset": "Manual Offset", + "adjustLyricsTiming": "Adjust Lyrics Timing", + "enterOffsetMs": "Enter offset in milliseconds.\nPositive values delay lyrics, negative values advance them.", + "offsetMs": "Offset (ms)", + "save": "Save", + "liveLyricsSync": "Live Lyrics Sync", + "offset": "Offset: {value}ms", + "@offset": { + "placeholders": { + "value": { + "type": "int" + } + } + }, + "minus100ms": "-100ms", + "plus10ms": "+10ms", + "reset": "Reset", + "plus100ms": "+100ms", + "fineAdjustment": "Fine Adjustment", + "onlyTimedLyricsCanBeSynced": "Only timed lyrics can be synced", + "errorLoadingLyrics": "Error loading lyrics: {error}", + "@errorLoadingLyrics": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "errorParsingLyrics": "Error parsing lyrics: {error}", + "@errorParsingLyrics": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "noTracksInQueue": "No tracks in queue", + "unknownArtist": "Unknown Artist", + "showLyrics": "Show Lyrics", + "showQueue": "Show Queue", + "showCover": "Show Cover", + "importedLyricsLines": "Imported {count} lyrics lines for \"{title}\"", + "@importedLyricsLines": { + "placeholders": { + "count": { + "type": "int" + }, + "title": { + "type": "String" + } + } + }, + "selected": "{count} selected", + "@selected": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "addToPlaylist": "Add to Playlist", + "delete": "Delete", + "groovyBox": "GroovyBox", + "library": "Library", + "importFiles": "Import Files", + "searchTracks": "Search tracks...", + "searchTracksWithCount": "Search tracks... ({total} tracks)", + "@searchTracksWithCount": { + "placeholders": { + "total": { + "type": "int" + } + } + }, + "searchTracksFiltered": "Search tracks... ({filtered} of {total} tracks)", + "@searchTracksFiltered": { + "placeholders": { + "filtered": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "error": "Error: {message}", + "@error": { + "placeholders": { + "message": { + "type": "String" + } + } + }, + "noTracksYet": "No tracks yet. Add some!", + "noTracksMatchSearch": "No tracks match your search.", + "deleteTrack": "Delete Track?", + "confirmDeleteTrack": "Are you sure you want to delete \"{title}\"? This cannot be undone.", + "@confirmDeleteTrack": { + "placeholders": { + "title": { + "type": "String" + } + } + }, + "deletedTrack": "Deleted \"{title}\"", + "@deletedTrack": { + "placeholders": { + "title": { + "type": "String" + } + } + }, + "viewDetails": "View Details", + "editMetadata": "Edit Metadata", + "importLyrics": "Import Lyrics", + "noPlaylistsAvailable": "No playlists available. Create one first!", + "addedToPlaylist": "Added to {name}", + "@addedToPlaylist": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "trackDetails": "Track Details", + "close": "Close", + "title": "Title", + "artist": "Artist", + "album": "Album", + "duration": "Duration", + "fileSize": "File Size", + "filePath": "File Path", + "dateAdded": "Date Added", + "albumArt": "Album Art", + "present": "Present", + "editTrack": "Edit Track", + "addedTracksToPlaylist": "Added {count} tracks to {name}", + "@addedTracksToPlaylist": { + "placeholders": { + "count": { + "type": "int" + }, + "name": { + "type": "String" + } + } + }, + "deleteTracks": "Delete Tracks?", + "confirmDeleteTracks": "Are you sure you want to delete {count} tracks? This will remove them from your device.", + "@confirmDeleteTracks": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "deletedTracks": "Deleted {count} tracks", + "@deletedTracks": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "batchImportComplete": "Batch import complete: {matched} matched, {notMatched} not matched", + "@batchImportComplete": { + "placeholders": { + "matched": { + "type": "int" + }, + "notMatched": { + "type": "int" + } + } + }, + "settings": "Settings", + "autoScan": "Auto Scan", + "autoScanMusicLibraries": "Auto-scan music libraries", + "autoScanDescription": "Automatically scan music libraries for new music files", + "watchForChanges": "Watch for changes", + "watchForChangesDescription": "Monitor music libraries for file changes", + "musicLibraries": "Music Libraries", + "scanLibraries": "Scan Libraries", + "addMusicLibrary": "Add Music Library", + "addMusicLibraryDescription": "Add folder libraries to index music files. Files will be copied to internal storage for playback.", + "noMusicLibrariesAdded": "No music libraries added yet.", + "errorLoadingLibraries": "Error loading libraries: {error}", + "@errorLoadingLibraries": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "remoteProviders": "Remote Providers", + "indexRemoteProviders": "Index Remote Providers", + "addRemoteProvider": "Add Remote Provider", + "remoteProvidersDescription": "Connect to remote media servers like Jellyfin to access your music library.", + "noRemoteProvidersAdded": "No remote providers added yet.", + "errorLoadingProviders": "Error loading providers: {error}", + "@errorLoadingProviders": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "playerSettings": "Player Settings", + "playerSettingsDescription": "Configure player behavior and display options.", + "defaultPlayerScreen": "Default Player Screen", + "defaultPlayerScreenDescription": "Choose which screen to show when opening the player.", + "lyricsMode": "Lyrics Mode", + "lyricsModeDescription": "Choose how lyrics are displayed.", + "continuePlaying": "Continue Playing", + "continuePlayingDescription": "Continue playing music after the queue is empty", + "databaseManagement": "Database Management", + "databaseManagementDescription": "Manage your music database and cached files.", + "resetTrackDatabase": "Reset Track Database", + "resetTrackDatabaseDescription": "Remove all tracks from database and delete cached files. This action cannot be undone.", + "errorLoadingSettings": "Error loading settings: {error}", + "@errorLoadingSettings": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "addedMusicLibrary": "Added music library: {path}", + "@addedMusicLibrary": { + "placeholders": { + "path": { + "type": "String" + } + } + }, + "errorAddingLibrary": "Error adding library: {error}", + "@errorAddingLibrary": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "librariesScannedSuccessfully": "Libraries scanned successfully", + "errorScanningLibraries": "Error scanning libraries: {error}", + "@errorScanningLibraries": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "noActiveRemoteProviders": "No active remote providers to index", + "indexedRemoteProviders": "Indexed {count} remote provider(s)", + "@indexedRemoteProviders": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "errorIndexingRemoteProviders": "Error indexing remote providers: {error}", + "@errorIndexingRemoteProviders": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "serverUrl": "Server URL", + "serverUrlHint": "https://your-jellyfin-server.com", + "username": "Username", + "password": "Password", + "add": "Add", + "allFieldsRequired": "All fields are required", + "addedRemoteProvider": "Added remote provider: {url}", + "@addedRemoteProvider": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "errorAddingProvider": "Error adding provider: {error}", + "@errorAddingProvider": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "confirmResetTrackDatabase": "This will permanently delete all tracks from the database and remove all cached music files and album art. This action cannot be undone.\n\nAre you sure you want to continue?", + "trackDatabaseReset": "Track database has been reset", + "errorResettingDatabase": "Error resetting database: {error}", + "@errorResettingDatabase": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "noTracksInAlbum": "No tracks in this album", + "playAll": "Play All", + "addToQueue": "Add to Queue", + "noTracksInPlaylist": "No tracks in this playlist", + "noAlbumsFound": "No albums found", + "createOne": "Create One", + "addNewPlaylist": "Add a new playlist", + "newPlaylist": "New Playlist", + "playlistName": "Playlist Name", + "create": "Create", + "noPlaylistsYet": "No playlists yet", + "queue": "Queue", + "appSettings": "App Settings", + "appSettingsDescription": "Configure app-wide settings and preferences.", + "language": "Language", + "languageDescription": "Choose the app language.", + "english": "English", + "chinese": "中文" +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart new file mode 100644 index 0000000..867dff8 --- /dev/null +++ b/lib/l10n/app_localizations.dart @@ -0,0 +1,968 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_en.dart'; +import 'app_localizations_zh.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations? of(BuildContext context) { + return Localizations.of(context, AppLocalizations); + } + + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('en'), + Locale('zh'), + ]; + + /// No description provided for @noMediaSelected. + /// + /// In en, this message translates to: + /// **'No media selected'** + String get noMediaSelected; + + /// No description provided for @noLyricsAvailable. + /// + /// In en, this message translates to: + /// **'No Lyrics Available'** + String get noLyricsAvailable; + + /// No description provided for @fetchLyrics. + /// + /// In en, this message translates to: + /// **'Fetch Lyrics'** + String get fetchLyrics; + + /// No description provided for @searchLyricsWith. + /// + /// In en, this message translates to: + /// **'Search lyrics with {searchTerm}'** + String searchLyricsWith(String searchTerm); + + /// No description provided for @whereToSearchLyrics. + /// + /// In en, this message translates to: + /// **'Where do you want to search lyrics from?'** + String get whereToSearchLyrics; + + /// No description provided for @musixmatch. + /// + /// In en, this message translates to: + /// **'Musixmatch'** + String get musixmatch; + + /// No description provided for @netease. + /// + /// In en, this message translates to: + /// **'NetEase'** + String get netease; + + /// No description provided for @lrclib. + /// + /// In en, this message translates to: + /// **'Lrclib'** + String get lrclib; + + /// No description provided for @manualImport. + /// + /// In en, this message translates to: + /// **'Manual Import'** + String get manualImport; + + /// No description provided for @cancel. + /// + /// In en, this message translates to: + /// **'Cancel'** + String get cancel; + + /// No description provided for @lyricsOptions. + /// + /// In en, this message translates to: + /// **'Lyrics Options'** + String get lyricsOptions; + + /// No description provided for @refetch. + /// + /// In en, this message translates to: + /// **'Re-fetch'** + String get refetch; + + /// No description provided for @clear. + /// + /// In en, this message translates to: + /// **'Clear'** + String get clear; + + /// No description provided for @liveSyncLyrics. + /// + /// In en, this message translates to: + /// **'Live Sync Lyrics'** + String get liveSyncLyrics; + + /// No description provided for @manualOffset. + /// + /// In en, this message translates to: + /// **'Manual Offset'** + String get manualOffset; + + /// No description provided for @adjustLyricsTiming. + /// + /// In en, this message translates to: + /// **'Adjust Lyrics Timing'** + String get adjustLyricsTiming; + + /// No description provided for @enterOffsetMs. + /// + /// In en, this message translates to: + /// **'Enter offset in milliseconds.\nPositive values delay lyrics, negative values advance them.'** + String get enterOffsetMs; + + /// No description provided for @offsetMs. + /// + /// In en, this message translates to: + /// **'Offset (ms)'** + String get offsetMs; + + /// No description provided for @save. + /// + /// In en, this message translates to: + /// **'Save'** + String get save; + + /// No description provided for @liveLyricsSync. + /// + /// In en, this message translates to: + /// **'Live Lyrics Sync'** + String get liveLyricsSync; + + /// No description provided for @offset. + /// + /// In en, this message translates to: + /// **'Offset: {value}ms'** + String offset(int value); + + /// No description provided for @minus100ms. + /// + /// In en, this message translates to: + /// **'-100ms'** + String get minus100ms; + + /// No description provided for @plus10ms. + /// + /// In en, this message translates to: + /// **'+10ms'** + String get plus10ms; + + /// No description provided for @reset. + /// + /// In en, this message translates to: + /// **'Reset'** + String get reset; + + /// No description provided for @plus100ms. + /// + /// In en, this message translates to: + /// **'+100ms'** + String get plus100ms; + + /// No description provided for @fineAdjustment. + /// + /// In en, this message translates to: + /// **'Fine Adjustment'** + String get fineAdjustment; + + /// No description provided for @onlyTimedLyricsCanBeSynced. + /// + /// In en, this message translates to: + /// **'Only timed lyrics can be synced'** + String get onlyTimedLyricsCanBeSynced; + + /// No description provided for @errorLoadingLyrics. + /// + /// In en, this message translates to: + /// **'Error loading lyrics: {error}'** + String errorLoadingLyrics(String error); + + /// No description provided for @errorParsingLyrics. + /// + /// In en, this message translates to: + /// **'Error parsing lyrics: {error}'** + String errorParsingLyrics(String error); + + /// No description provided for @noTracksInQueue. + /// + /// In en, this message translates to: + /// **'No tracks in queue'** + String get noTracksInQueue; + + /// No description provided for @unknownArtist. + /// + /// In en, this message translates to: + /// **'Unknown Artist'** + String get unknownArtist; + + /// No description provided for @showLyrics. + /// + /// In en, this message translates to: + /// **'Show Lyrics'** + String get showLyrics; + + /// No description provided for @showQueue. + /// + /// In en, this message translates to: + /// **'Show Queue'** + String get showQueue; + + /// No description provided for @showCover. + /// + /// In en, this message translates to: + /// **'Show Cover'** + String get showCover; + + /// No description provided for @importedLyricsLines. + /// + /// In en, this message translates to: + /// **'Imported {count} lyrics lines for \"{title}\"'** + String importedLyricsLines(int count, String title); + + /// No description provided for @selected. + /// + /// In en, this message translates to: + /// **'{count} selected'** + String selected(int count); + + /// No description provided for @addToPlaylist. + /// + /// In en, this message translates to: + /// **'Add to Playlist'** + String get addToPlaylist; + + /// No description provided for @delete. + /// + /// In en, this message translates to: + /// **'Delete'** + String get delete; + + /// No description provided for @groovyBox. + /// + /// In en, this message translates to: + /// **'GroovyBox'** + String get groovyBox; + + /// No description provided for @library. + /// + /// In en, this message translates to: + /// **'Library'** + String get library; + + /// No description provided for @importFiles. + /// + /// In en, this message translates to: + /// **'Import Files'** + String get importFiles; + + /// No description provided for @searchTracks. + /// + /// In en, this message translates to: + /// **'Search tracks...'** + String get searchTracks; + + /// No description provided for @searchTracksWithCount. + /// + /// In en, this message translates to: + /// **'Search tracks... ({total} tracks)'** + String searchTracksWithCount(int total); + + /// No description provided for @searchTracksFiltered. + /// + /// In en, this message translates to: + /// **'Search tracks... ({filtered} of {total} tracks)'** + String searchTracksFiltered(int filtered, int total); + + /// No description provided for @error. + /// + /// In en, this message translates to: + /// **'Error: {message}'** + String error(String message); + + /// No description provided for @noTracksYet. + /// + /// In en, this message translates to: + /// **'No tracks yet. Add some!'** + String get noTracksYet; + + /// No description provided for @noTracksMatchSearch. + /// + /// In en, this message translates to: + /// **'No tracks match your search.'** + String get noTracksMatchSearch; + + /// No description provided for @deleteTrack. + /// + /// In en, this message translates to: + /// **'Delete Track?'** + String get deleteTrack; + + /// No description provided for @confirmDeleteTrack. + /// + /// In en, this message translates to: + /// **'Are you sure you want to delete \"{title}\"? This cannot be undone.'** + String confirmDeleteTrack(String title); + + /// No description provided for @deletedTrack. + /// + /// In en, this message translates to: + /// **'Deleted \"{title}\"'** + String deletedTrack(String title); + + /// No description provided for @viewDetails. + /// + /// In en, this message translates to: + /// **'View Details'** + String get viewDetails; + + /// No description provided for @editMetadata. + /// + /// In en, this message translates to: + /// **'Edit Metadata'** + String get editMetadata; + + /// No description provided for @importLyrics. + /// + /// In en, this message translates to: + /// **'Import Lyrics'** + String get importLyrics; + + /// No description provided for @noPlaylistsAvailable. + /// + /// In en, this message translates to: + /// **'No playlists available. Create one first!'** + String get noPlaylistsAvailable; + + /// No description provided for @addedToPlaylist. + /// + /// In en, this message translates to: + /// **'Added to {name}'** + String addedToPlaylist(String name); + + /// No description provided for @trackDetails. + /// + /// In en, this message translates to: + /// **'Track Details'** + String get trackDetails; + + /// No description provided for @close. + /// + /// In en, this message translates to: + /// **'Close'** + String get close; + + /// No description provided for @title. + /// + /// In en, this message translates to: + /// **'Title'** + String get title; + + /// No description provided for @artist. + /// + /// In en, this message translates to: + /// **'Artist'** + String get artist; + + /// No description provided for @album. + /// + /// In en, this message translates to: + /// **'Album'** + String get album; + + /// No description provided for @duration. + /// + /// In en, this message translates to: + /// **'Duration'** + String get duration; + + /// No description provided for @fileSize. + /// + /// In en, this message translates to: + /// **'File Size'** + String get fileSize; + + /// No description provided for @filePath. + /// + /// In en, this message translates to: + /// **'File Path'** + String get filePath; + + /// No description provided for @dateAdded. + /// + /// In en, this message translates to: + /// **'Date Added'** + String get dateAdded; + + /// No description provided for @albumArt. + /// + /// In en, this message translates to: + /// **'Album Art'** + String get albumArt; + + /// No description provided for @present. + /// + /// In en, this message translates to: + /// **'Present'** + String get present; + + /// No description provided for @editTrack. + /// + /// In en, this message translates to: + /// **'Edit Track'** + String get editTrack; + + /// No description provided for @addedTracksToPlaylist. + /// + /// In en, this message translates to: + /// **'Added {count} tracks to {name}'** + String addedTracksToPlaylist(int count, String name); + + /// No description provided for @deleteTracks. + /// + /// In en, this message translates to: + /// **'Delete Tracks?'** + String get deleteTracks; + + /// No description provided for @confirmDeleteTracks. + /// + /// In en, this message translates to: + /// **'Are you sure you want to delete {count} tracks? This will remove them from your device.'** + String confirmDeleteTracks(int count); + + /// No description provided for @deletedTracks. + /// + /// In en, this message translates to: + /// **'Deleted {count} tracks'** + String deletedTracks(int count); + + /// No description provided for @batchImportComplete. + /// + /// In en, this message translates to: + /// **'Batch import complete: {matched} matched, {notMatched} not matched'** + String batchImportComplete(int matched, int notMatched); + + /// No description provided for @settings. + /// + /// In en, this message translates to: + /// **'Settings'** + String get settings; + + /// No description provided for @autoScan. + /// + /// In en, this message translates to: + /// **'Auto Scan'** + String get autoScan; + + /// No description provided for @autoScanMusicLibraries. + /// + /// In en, this message translates to: + /// **'Auto-scan music libraries'** + String get autoScanMusicLibraries; + + /// No description provided for @autoScanDescription. + /// + /// In en, this message translates to: + /// **'Automatically scan music libraries for new music files'** + String get autoScanDescription; + + /// No description provided for @watchForChanges. + /// + /// In en, this message translates to: + /// **'Watch for changes'** + String get watchForChanges; + + /// No description provided for @watchForChangesDescription. + /// + /// In en, this message translates to: + /// **'Monitor music libraries for file changes'** + String get watchForChangesDescription; + + /// No description provided for @musicLibraries. + /// + /// In en, this message translates to: + /// **'Music Libraries'** + String get musicLibraries; + + /// No description provided for @scanLibraries. + /// + /// In en, this message translates to: + /// **'Scan Libraries'** + String get scanLibraries; + + /// No description provided for @addMusicLibrary. + /// + /// In en, this message translates to: + /// **'Add Music Library'** + String get addMusicLibrary; + + /// No description provided for @addMusicLibraryDescription. + /// + /// In en, this message translates to: + /// **'Add folder libraries to index music files. Files will be copied to internal storage for playback.'** + String get addMusicLibraryDescription; + + /// No description provided for @noMusicLibrariesAdded. + /// + /// In en, this message translates to: + /// **'No music libraries added yet.'** + String get noMusicLibrariesAdded; + + /// No description provided for @errorLoadingLibraries. + /// + /// In en, this message translates to: + /// **'Error loading libraries: {error}'** + String errorLoadingLibraries(String error); + + /// No description provided for @remoteProviders. + /// + /// In en, this message translates to: + /// **'Remote Providers'** + String get remoteProviders; + + /// No description provided for @indexRemoteProviders. + /// + /// In en, this message translates to: + /// **'Index Remote Providers'** + String get indexRemoteProviders; + + /// No description provided for @addRemoteProvider. + /// + /// In en, this message translates to: + /// **'Add Remote Provider'** + String get addRemoteProvider; + + /// No description provided for @remoteProvidersDescription. + /// + /// In en, this message translates to: + /// **'Connect to remote media servers like Jellyfin to access your music library.'** + String get remoteProvidersDescription; + + /// No description provided for @noRemoteProvidersAdded. + /// + /// In en, this message translates to: + /// **'No remote providers added yet.'** + String get noRemoteProvidersAdded; + + /// No description provided for @errorLoadingProviders. + /// + /// In en, this message translates to: + /// **'Error loading providers: {error}'** + String errorLoadingProviders(String error); + + /// No description provided for @playerSettings. + /// + /// In en, this message translates to: + /// **'Player Settings'** + String get playerSettings; + + /// No description provided for @playerSettingsDescription. + /// + /// In en, this message translates to: + /// **'Configure player behavior and display options.'** + String get playerSettingsDescription; + + /// No description provided for @defaultPlayerScreen. + /// + /// In en, this message translates to: + /// **'Default Player Screen'** + String get defaultPlayerScreen; + + /// No description provided for @defaultPlayerScreenDescription. + /// + /// In en, this message translates to: + /// **'Choose which screen to show when opening the player.'** + String get defaultPlayerScreenDescription; + + /// No description provided for @lyricsMode. + /// + /// In en, this message translates to: + /// **'Lyrics Mode'** + String get lyricsMode; + + /// No description provided for @lyricsModeDescription. + /// + /// In en, this message translates to: + /// **'Choose how lyrics are displayed.'** + String get lyricsModeDescription; + + /// No description provided for @continuePlaying. + /// + /// In en, this message translates to: + /// **'Continue Playing'** + String get continuePlaying; + + /// No description provided for @continuePlayingDescription. + /// + /// In en, this message translates to: + /// **'Continue playing music after the queue is empty'** + String get continuePlayingDescription; + + /// No description provided for @databaseManagement. + /// + /// In en, this message translates to: + /// **'Database Management'** + String get databaseManagement; + + /// No description provided for @databaseManagementDescription. + /// + /// In en, this message translates to: + /// **'Manage your music database and cached files.'** + String get databaseManagementDescription; + + /// No description provided for @resetTrackDatabase. + /// + /// In en, this message translates to: + /// **'Reset Track Database'** + String get resetTrackDatabase; + + /// No description provided for @resetTrackDatabaseDescription. + /// + /// In en, this message translates to: + /// **'Remove all tracks from database and delete cached files. This action cannot be undone.'** + String get resetTrackDatabaseDescription; + + /// No description provided for @errorLoadingSettings. + /// + /// In en, this message translates to: + /// **'Error loading settings: {error}'** + String errorLoadingSettings(String error); + + /// No description provided for @addedMusicLibrary. + /// + /// In en, this message translates to: + /// **'Added music library: {path}'** + String addedMusicLibrary(String path); + + /// No description provided for @errorAddingLibrary. + /// + /// In en, this message translates to: + /// **'Error adding library: {error}'** + String errorAddingLibrary(String error); + + /// No description provided for @librariesScannedSuccessfully. + /// + /// In en, this message translates to: + /// **'Libraries scanned successfully'** + String get librariesScannedSuccessfully; + + /// No description provided for @errorScanningLibraries. + /// + /// In en, this message translates to: + /// **'Error scanning libraries: {error}'** + String errorScanningLibraries(String error); + + /// No description provided for @noActiveRemoteProviders. + /// + /// In en, this message translates to: + /// **'No active remote providers to index'** + String get noActiveRemoteProviders; + + /// No description provided for @indexedRemoteProviders. + /// + /// In en, this message translates to: + /// **'Indexed {count} remote provider(s)'** + String indexedRemoteProviders(int count); + + /// No description provided for @errorIndexingRemoteProviders. + /// + /// In en, this message translates to: + /// **'Error indexing remote providers: {error}'** + String errorIndexingRemoteProviders(String error); + + /// No description provided for @serverUrl. + /// + /// In en, this message translates to: + /// **'Server URL'** + String get serverUrl; + + /// No description provided for @serverUrlHint. + /// + /// In en, this message translates to: + /// **'https://your-jellyfin-server.com'** + String get serverUrlHint; + + /// No description provided for @username. + /// + /// In en, this message translates to: + /// **'Username'** + String get username; + + /// No description provided for @password. + /// + /// In en, this message translates to: + /// **'Password'** + String get password; + + /// No description provided for @add. + /// + /// In en, this message translates to: + /// **'Add'** + String get add; + + /// No description provided for @allFieldsRequired. + /// + /// In en, this message translates to: + /// **'All fields are required'** + String get allFieldsRequired; + + /// No description provided for @addedRemoteProvider. + /// + /// In en, this message translates to: + /// **'Added remote provider: {url}'** + String addedRemoteProvider(String url); + + /// No description provided for @errorAddingProvider. + /// + /// In en, this message translates to: + /// **'Error adding provider: {error}'** + String errorAddingProvider(String error); + + /// No description provided for @confirmResetTrackDatabase. + /// + /// In en, this message translates to: + /// **'This will permanently delete all tracks from the database and remove all cached music files and album art. This action cannot be undone.\n\nAre you sure you want to continue?'** + String get confirmResetTrackDatabase; + + /// No description provided for @trackDatabaseReset. + /// + /// In en, this message translates to: + /// **'Track database has been reset'** + String get trackDatabaseReset; + + /// No description provided for @errorResettingDatabase. + /// + /// In en, this message translates to: + /// **'Error resetting database: {error}'** + String errorResettingDatabase(String error); + + /// No description provided for @noTracksInAlbum. + /// + /// In en, this message translates to: + /// **'No tracks in this album'** + String get noTracksInAlbum; + + /// No description provided for @playAll. + /// + /// In en, this message translates to: + /// **'Play All'** + String get playAll; + + /// No description provided for @addToQueue. + /// + /// In en, this message translates to: + /// **'Add to Queue'** + String get addToQueue; + + /// No description provided for @noTracksInPlaylist. + /// + /// In en, this message translates to: + /// **'No tracks in this playlist'** + String get noTracksInPlaylist; + + /// No description provided for @noAlbumsFound. + /// + /// In en, this message translates to: + /// **'No albums found'** + String get noAlbumsFound; + + /// No description provided for @createOne. + /// + /// In en, this message translates to: + /// **'Create One'** + String get createOne; + + /// No description provided for @addNewPlaylist. + /// + /// In en, this message translates to: + /// **'Add a new playlist'** + String get addNewPlaylist; + + /// No description provided for @newPlaylist. + /// + /// In en, this message translates to: + /// **'New Playlist'** + String get newPlaylist; + + /// No description provided for @playlistName. + /// + /// In en, this message translates to: + /// **'Playlist Name'** + String get playlistName; + + /// No description provided for @create. + /// + /// In en, this message translates to: + /// **'Create'** + String get create; + + /// No description provided for @noPlaylistsYet. + /// + /// In en, this message translates to: + /// **'No playlists yet'** + String get noPlaylistsYet; + + /// No description provided for @queue. + /// + /// In en, this message translates to: + /// **'Queue'** + String get queue; + + /// No description provided for @appSettings. + /// + /// In en, this message translates to: + /// **'App Settings'** + String get appSettings; + + /// No description provided for @appSettingsDescription. + /// + /// In en, this message translates to: + /// **'Configure app-wide settings and preferences.'** + String get appSettingsDescription; + + /// No description provided for @language. + /// + /// In en, this message translates to: + /// **'Language'** + String get language; + + /// No description provided for @languageDescription. + /// + /// In en, this message translates to: + /// **'Choose the app language.'** + String get languageDescription; + + /// No description provided for @english. + /// + /// In en, this message translates to: + /// **'English'** + String get english; + + /// No description provided for @chinese. + /// + /// In en, this message translates to: + /// **'中文'** + String get chinese; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['en', 'zh'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': + return AppLocalizationsEn(); + case 'zh': + return AppLocalizationsZh(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); +} diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart new file mode 100644 index 0000000..b1084d0 --- /dev/null +++ b/lib/l10n/app_localizations_en.dart @@ -0,0 +1,494 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get noMediaSelected => 'No media selected'; + + @override + String get noLyricsAvailable => 'No Lyrics Available'; + + @override + String get fetchLyrics => 'Fetch Lyrics'; + + @override + String searchLyricsWith(String searchTerm) { + return 'Search lyrics with $searchTerm'; + } + + @override + String get whereToSearchLyrics => 'Where do you want to search lyrics from?'; + + @override + String get musixmatch => 'Musixmatch'; + + @override + String get netease => 'NetEase'; + + @override + String get lrclib => 'Lrclib'; + + @override + String get manualImport => 'Manual Import'; + + @override + String get cancel => 'Cancel'; + + @override + String get lyricsOptions => 'Lyrics Options'; + + @override + String get refetch => 'Re-fetch'; + + @override + String get clear => 'Clear'; + + @override + String get liveSyncLyrics => 'Live Sync Lyrics'; + + @override + String get manualOffset => 'Manual Offset'; + + @override + String get adjustLyricsTiming => 'Adjust Lyrics Timing'; + + @override + String get enterOffsetMs => + 'Enter offset in milliseconds.\nPositive values delay lyrics, negative values advance them.'; + + @override + String get offsetMs => 'Offset (ms)'; + + @override + String get save => 'Save'; + + @override + String get liveLyricsSync => 'Live Lyrics Sync'; + + @override + String offset(int value) { + return 'Offset: ${value}ms'; + } + + @override + String get minus100ms => '-100ms'; + + @override + String get plus10ms => '+10ms'; + + @override + String get reset => 'Reset'; + + @override + String get plus100ms => '+100ms'; + + @override + String get fineAdjustment => 'Fine Adjustment'; + + @override + String get onlyTimedLyricsCanBeSynced => 'Only timed lyrics can be synced'; + + @override + String errorLoadingLyrics(String error) { + return 'Error loading lyrics: $error'; + } + + @override + String errorParsingLyrics(String error) { + return 'Error parsing lyrics: $error'; + } + + @override + String get noTracksInQueue => 'No tracks in queue'; + + @override + String get unknownArtist => 'Unknown Artist'; + + @override + String get showLyrics => 'Show Lyrics'; + + @override + String get showQueue => 'Show Queue'; + + @override + String get showCover => 'Show Cover'; + + @override + String importedLyricsLines(int count, String title) { + return 'Imported $count lyrics lines for \"$title\"'; + } + + @override + String selected(int count) { + return '$count selected'; + } + + @override + String get addToPlaylist => 'Add to Playlist'; + + @override + String get delete => 'Delete'; + + @override + String get groovyBox => 'GroovyBox'; + + @override + String get library => 'Library'; + + @override + String get importFiles => 'Import Files'; + + @override + String get searchTracks => 'Search tracks...'; + + @override + String searchTracksWithCount(int total) { + return 'Search tracks... ($total tracks)'; + } + + @override + String searchTracksFiltered(int filtered, int total) { + return 'Search tracks... ($filtered of $total tracks)'; + } + + @override + String error(String message) { + return 'Error: $message'; + } + + @override + String get noTracksYet => 'No tracks yet. Add some!'; + + @override + String get noTracksMatchSearch => 'No tracks match your search.'; + + @override + String get deleteTrack => 'Delete Track?'; + + @override + String confirmDeleteTrack(String title) { + return 'Are you sure you want to delete \"$title\"? This cannot be undone.'; + } + + @override + String deletedTrack(String title) { + return 'Deleted \"$title\"'; + } + + @override + String get viewDetails => 'View Details'; + + @override + String get editMetadata => 'Edit Metadata'; + + @override + String get importLyrics => 'Import Lyrics'; + + @override + String get noPlaylistsAvailable => + 'No playlists available. Create one first!'; + + @override + String addedToPlaylist(String name) { + return 'Added to $name'; + } + + @override + String get trackDetails => 'Track Details'; + + @override + String get close => 'Close'; + + @override + String get title => 'Title'; + + @override + String get artist => 'Artist'; + + @override + String get album => 'Album'; + + @override + String get duration => 'Duration'; + + @override + String get fileSize => 'File Size'; + + @override + String get filePath => 'File Path'; + + @override + String get dateAdded => 'Date Added'; + + @override + String get albumArt => 'Album Art'; + + @override + String get present => 'Present'; + + @override + String get editTrack => 'Edit Track'; + + @override + String addedTracksToPlaylist(int count, String name) { + return 'Added $count tracks to $name'; + } + + @override + String get deleteTracks => 'Delete Tracks?'; + + @override + String confirmDeleteTracks(int count) { + return 'Are you sure you want to delete $count tracks? This will remove them from your device.'; + } + + @override + String deletedTracks(int count) { + return 'Deleted $count tracks'; + } + + @override + String batchImportComplete(int matched, int notMatched) { + return 'Batch import complete: $matched matched, $notMatched not matched'; + } + + @override + String get settings => 'Settings'; + + @override + String get autoScan => 'Auto Scan'; + + @override + String get autoScanMusicLibraries => 'Auto-scan music libraries'; + + @override + String get autoScanDescription => + 'Automatically scan music libraries for new music files'; + + @override + String get watchForChanges => 'Watch for changes'; + + @override + String get watchForChangesDescription => + 'Monitor music libraries for file changes'; + + @override + String get musicLibraries => 'Music Libraries'; + + @override + String get scanLibraries => 'Scan Libraries'; + + @override + String get addMusicLibrary => 'Add Music Library'; + + @override + String get addMusicLibraryDescription => + 'Add folder libraries to index music files. Files will be copied to internal storage for playback.'; + + @override + String get noMusicLibrariesAdded => 'No music libraries added yet.'; + + @override + String errorLoadingLibraries(String error) { + return 'Error loading libraries: $error'; + } + + @override + String get remoteProviders => 'Remote Providers'; + + @override + String get indexRemoteProviders => 'Index Remote Providers'; + + @override + String get addRemoteProvider => 'Add Remote Provider'; + + @override + String get remoteProvidersDescription => + 'Connect to remote media servers like Jellyfin to access your music library.'; + + @override + String get noRemoteProvidersAdded => 'No remote providers added yet.'; + + @override + String errorLoadingProviders(String error) { + return 'Error loading providers: $error'; + } + + @override + String get playerSettings => 'Player Settings'; + + @override + String get playerSettingsDescription => + 'Configure player behavior and display options.'; + + @override + String get defaultPlayerScreen => 'Default Player Screen'; + + @override + String get defaultPlayerScreenDescription => + 'Choose which screen to show when opening the player.'; + + @override + String get lyricsMode => 'Lyrics Mode'; + + @override + String get lyricsModeDescription => 'Choose how lyrics are displayed.'; + + @override + String get continuePlaying => 'Continue Playing'; + + @override + String get continuePlayingDescription => + 'Continue playing music after the queue is empty'; + + @override + String get databaseManagement => 'Database Management'; + + @override + String get databaseManagementDescription => + 'Manage your music database and cached files.'; + + @override + String get resetTrackDatabase => 'Reset Track Database'; + + @override + String get resetTrackDatabaseDescription => + 'Remove all tracks from database and delete cached files. This action cannot be undone.'; + + @override + String errorLoadingSettings(String error) { + return 'Error loading settings: $error'; + } + + @override + String addedMusicLibrary(String path) { + return 'Added music library: $path'; + } + + @override + String errorAddingLibrary(String error) { + return 'Error adding library: $error'; + } + + @override + String get librariesScannedSuccessfully => 'Libraries scanned successfully'; + + @override + String errorScanningLibraries(String error) { + return 'Error scanning libraries: $error'; + } + + @override + String get noActiveRemoteProviders => 'No active remote providers to index'; + + @override + String indexedRemoteProviders(int count) { + return 'Indexed $count remote provider(s)'; + } + + @override + String errorIndexingRemoteProviders(String error) { + return 'Error indexing remote providers: $error'; + } + + @override + String get serverUrl => 'Server URL'; + + @override + String get serverUrlHint => 'https://your-jellyfin-server.com'; + + @override + String get username => 'Username'; + + @override + String get password => 'Password'; + + @override + String get add => 'Add'; + + @override + String get allFieldsRequired => 'All fields are required'; + + @override + String addedRemoteProvider(String url) { + return 'Added remote provider: $url'; + } + + @override + String errorAddingProvider(String error) { + return 'Error adding provider: $error'; + } + + @override + String get confirmResetTrackDatabase => + 'This will permanently delete all tracks from the database and remove all cached music files and album art. This action cannot be undone.\n\nAre you sure you want to continue?'; + + @override + String get trackDatabaseReset => 'Track database has been reset'; + + @override + String errorResettingDatabase(String error) { + return 'Error resetting database: $error'; + } + + @override + String get noTracksInAlbum => 'No tracks in this album'; + + @override + String get playAll => 'Play All'; + + @override + String get addToQueue => 'Add to Queue'; + + @override + String get noTracksInPlaylist => 'No tracks in this playlist'; + + @override + String get noAlbumsFound => 'No albums found'; + + @override + String get createOne => 'Create One'; + + @override + String get addNewPlaylist => 'Add a new playlist'; + + @override + String get newPlaylist => 'New Playlist'; + + @override + String get playlistName => 'Playlist Name'; + + @override + String get create => 'Create'; + + @override + String get noPlaylistsYet => 'No playlists yet'; + + @override + String get queue => 'Queue'; + + @override + String get appSettings => 'App Settings'; + + @override + String get appSettingsDescription => + 'Configure app-wide settings and preferences.'; + + @override + String get language => 'Language'; + + @override + String get languageDescription => 'Choose the app language.'; + + @override + String get english => 'English'; + + @override + String get chinese => '中文'; +} diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart new file mode 100644 index 0000000..418f63d --- /dev/null +++ b/lib/l10n/app_localizations_zh.dart @@ -0,0 +1,482 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Chinese (`zh`). +class AppLocalizationsZh extends AppLocalizations { + AppLocalizationsZh([String locale = 'zh']) : super(locale); + + @override + String get noMediaSelected => '未选择媒体'; + + @override + String get noLyricsAvailable => '无歌词可用'; + + @override + String get fetchLyrics => '获取歌词'; + + @override + String searchLyricsWith(String searchTerm) { + return '使用 $searchTerm 搜索歌词'; + } + + @override + String get whereToSearchLyrics => '您想从哪里搜索歌词?'; + + @override + String get musixmatch => 'Musixmatch'; + + @override + String get netease => '网易云音乐'; + + @override + String get lrclib => 'Lrclib'; + + @override + String get manualImport => '手动导入'; + + @override + String get cancel => '取消'; + + @override + String get lyricsOptions => '歌词选项'; + + @override + String get refetch => '重新获取'; + + @override + String get clear => '清除'; + + @override + String get liveSyncLyrics => '实时同步歌词'; + + @override + String get manualOffset => '手动偏移'; + + @override + String get adjustLyricsTiming => '调整歌词时间'; + + @override + String get enterOffsetMs => '输入偏移量(毫秒)。\n正值延迟歌词显示,负值提前显示。'; + + @override + String get offsetMs => '偏移量(毫秒)'; + + @override + String get save => '保存'; + + @override + String get liveLyricsSync => '实时歌词同步'; + + @override + String offset(int value) { + return '偏移量:$value毫秒'; + } + + @override + String get minus100ms => '-100毫秒'; + + @override + String get plus10ms => '+10毫秒'; + + @override + String get reset => '重置'; + + @override + String get plus100ms => '+100毫秒'; + + @override + String get fineAdjustment => '精细调整'; + + @override + String get onlyTimedLyricsCanBeSynced => '只有带时间戳的歌词才能同步'; + + @override + String errorLoadingLyrics(String error) { + return '加载歌词时出错:$error'; + } + + @override + String errorParsingLyrics(String error) { + return '解析歌词时出错:$error'; + } + + @override + String get noTracksInQueue => '队列中没有曲目'; + + @override + String get unknownArtist => '未知艺术家'; + + @override + String get showLyrics => '显示歌词'; + + @override + String get showQueue => '显示队列'; + + @override + String get showCover => '显示封面'; + + @override + String importedLyricsLines(int count, String title) { + return '为\"$title\"导入了 $count 行歌词'; + } + + @override + String selected(int count) { + return '已选择 $count 项'; + } + + @override + String get addToPlaylist => '添加到播放列表'; + + @override + String get delete => '删除'; + + @override + String get groovyBox => 'GroovyBox'; + + @override + String get library => '音乐库'; + + @override + String get importFiles => '导入文件'; + + @override + String get searchTracks => '搜索曲目...'; + + @override + String searchTracksWithCount(int total) { + return '搜索曲目...(共 $total 首)'; + } + + @override + String searchTracksFiltered(int filtered, int total) { + return '搜索曲目...($filtered / $total 首)'; + } + + @override + String error(String message) { + return '错误:$message'; + } + + @override + String get noTracksYet => '还没有曲目,请添加一些!'; + + @override + String get noTracksMatchSearch => '没有匹配搜索的曲目。'; + + @override + String get deleteTrack => '删除曲目?'; + + @override + String confirmDeleteTrack(String title) { + return '您确定要删除\"$title\"吗?此操作无法撤销。'; + } + + @override + String deletedTrack(String title) { + return '已删除\"$title\"'; + } + + @override + String get viewDetails => '查看详情'; + + @override + String get editMetadata => '编辑元数据'; + + @override + String get importLyrics => '导入歌词'; + + @override + String get noPlaylistsAvailable => '没有可用的播放列表,请先创建一个!'; + + @override + String addedToPlaylist(String name) { + return '已添加到 $name'; + } + + @override + String get trackDetails => '曲目详情'; + + @override + String get close => '关闭'; + + @override + String get title => '标题'; + + @override + String get artist => '艺术家'; + + @override + String get album => '专辑'; + + @override + String get duration => '时长'; + + @override + String get fileSize => '文件大小'; + + @override + String get filePath => '文件路径'; + + @override + String get dateAdded => '添加日期'; + + @override + String get albumArt => '专辑封面'; + + @override + String get present => '存在'; + + @override + String get editTrack => '编辑曲目'; + + @override + String addedTracksToPlaylist(int count, String name) { + return '已将 $count 首曲目添加到 $name'; + } + + @override + String get deleteTracks => '删除曲目?'; + + @override + String confirmDeleteTracks(int count) { + return '您确定要删除 $count 首曲目吗?这将从您的设备中移除它们。'; + } + + @override + String deletedTracks(int count) { + return '已删除 $count 首曲目'; + } + + @override + String batchImportComplete(int matched, int notMatched) { + return '批量导入完成:$matched 匹配,$notMatched 不匹配'; + } + + @override + String get settings => '设置'; + + @override + String get autoScan => '自动扫描'; + + @override + String get autoScanMusicLibraries => '自动扫描音乐库'; + + @override + String get autoScanDescription => '自动扫描音乐库中的新音乐文件'; + + @override + String get watchForChanges => '监视更改'; + + @override + String get watchForChangesDescription => '监视音乐库的文件更改'; + + @override + String get musicLibraries => '音乐库'; + + @override + String get scanLibraries => '扫描库'; + + @override + String get addMusicLibrary => '添加音乐库'; + + @override + String get addMusicLibraryDescription => '添加文件夹库来索引音乐文件。文件将被复制到内部存储以供播放。'; + + @override + String get noMusicLibrariesAdded => '尚未添加音乐库。'; + + @override + String errorLoadingLibraries(String error) { + return '加载库时出错:$error'; + } + + @override + String get remoteProviders => '远程提供商'; + + @override + String get indexRemoteProviders => '索引远程提供商'; + + @override + String get addRemoteProvider => '添加远程提供商'; + + @override + String get remoteProvidersDescription => '连接到远程媒体服务器,如Jellyfin,来访问您的音乐库。'; + + @override + String get noRemoteProvidersAdded => '尚未添加远程提供商。'; + + @override + String errorLoadingProviders(String error) { + return '加载提供商时出错:$error'; + } + + @override + String get playerSettings => '播放器设置'; + + @override + String get playerSettingsDescription => '配置播放器行为和显示选项。'; + + @override + String get defaultPlayerScreen => '默认播放器屏幕'; + + @override + String get defaultPlayerScreenDescription => '选择打开播放器时显示的屏幕。'; + + @override + String get lyricsMode => '歌词模式'; + + @override + String get lyricsModeDescription => '选择歌词的显示方式。'; + + @override + String get continuePlaying => '继续播放'; + + @override + String get continuePlayingDescription => '队列为空后继续播放音乐'; + + @override + String get databaseManagement => '数据库管理'; + + @override + String get databaseManagementDescription => '管理您的音乐数据库和缓存文件。'; + + @override + String get resetTrackDatabase => '重置曲目数据库'; + + @override + String get resetTrackDatabaseDescription => '从数据库中移除所有曲目并删除缓存文件。此操作无法撤销。'; + + @override + String errorLoadingSettings(String error) { + return '加载设置时出错:$error'; + } + + @override + String addedMusicLibrary(String path) { + return '已添加音乐库:$path'; + } + + @override + String errorAddingLibrary(String error) { + return '添加库时出错:$error'; + } + + @override + String get librariesScannedSuccessfully => '库扫描成功'; + + @override + String errorScanningLibraries(String error) { + return '扫描库时出错:$error'; + } + + @override + String get noActiveRemoteProviders => '没有活动的远程提供商可索引'; + + @override + String indexedRemoteProviders(int count) { + return '已索引 $count 个远程提供商'; + } + + @override + String errorIndexingRemoteProviders(String error) { + return '索引远程提供商时出错:$error'; + } + + @override + String get serverUrl => '服务器URL'; + + @override + String get serverUrlHint => 'https://your-jellyfin-server.com'; + + @override + String get username => '用户名'; + + @override + String get password => '密码'; + + @override + String get add => '添加'; + + @override + String get allFieldsRequired => '所有字段都是必填的'; + + @override + String addedRemoteProvider(String url) { + return '已添加远程提供商:$url'; + } + + @override + String errorAddingProvider(String error) { + return '添加提供商时出错:$error'; + } + + @override + String get confirmResetTrackDatabase => + '这将永久删除数据库中的所有曲目,并移除所有缓存的音乐文件和专辑封面。此操作无法撤销。\n\n您确定要继续吗?'; + + @override + String get trackDatabaseReset => '曲目数据库已重置'; + + @override + String errorResettingDatabase(String error) { + return '重置数据库时出错:$error'; + } + + @override + String get noTracksInAlbum => '此专辑中没有曲目'; + + @override + String get playAll => '播放全部'; + + @override + String get addToQueue => '添加到队列'; + + @override + String get noTracksInPlaylist => '此播放列表中没有曲目'; + + @override + String get noAlbumsFound => '未找到专辑'; + + @override + String get createOne => '创建一个'; + + @override + String get addNewPlaylist => '添加新播放列表'; + + @override + String get newPlaylist => '新播放列表'; + + @override + String get playlistName => '播放列表名称'; + + @override + String get create => '创建'; + + @override + String get noPlaylistsYet => '还没有播放列表'; + + @override + String get queue => '队列'; + + @override + String get appSettings => '应用设置'; + + @override + String get appSettingsDescription => '配置应用范围的设置和偏好。'; + + @override + String get language => '语言'; + + @override + String get languageDescription => '选择应用语言。'; + + @override + String get english => 'English'; + + @override + String get chinese => '中文'; +} diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb new file mode 100644 index 0000000..b932df0 --- /dev/null +++ b/lib/l10n/app_zh.arb @@ -0,0 +1,342 @@ +{ + "noMediaSelected": "未选择媒体", + "noLyricsAvailable": "无歌词可用", + "fetchLyrics": "获取歌词", + "searchLyricsWith": "使用 {searchTerm} 搜索歌词", + "@searchLyricsWith": { + "placeholders": { + "searchTerm": { + "type": "String" + } + } + }, + "whereToSearchLyrics": "您想从哪里搜索歌词?", + "musixmatch": "Musixmatch", + "netease": "网易云音乐", + "lrclib": "Lrclib", + "manualImport": "手动导入", + "cancel": "取消", + "lyricsOptions": "歌词选项", + "refetch": "重新获取", + "clear": "清除", + "liveSyncLyrics": "实时同步歌词", + "manualOffset": "手动偏移", + "adjustLyricsTiming": "调整歌词时间", + "enterOffsetMs": "输入偏移量(毫秒)。\n正值延迟歌词显示,负值提前显示。", + "offsetMs": "偏移量(毫秒)", + "save": "保存", + "liveLyricsSync": "实时歌词同步", + "offset": "偏移量:{value}毫秒", + "@offset": { + "placeholders": { + "value": { + "type": "int" + } + } + }, + "minus100ms": "-100毫秒", + "plus10ms": "+10毫秒", + "reset": "重置", + "plus100ms": "+100毫秒", + "fineAdjustment": "精细调整", + "onlyTimedLyricsCanBeSynced": "只有带时间戳的歌词才能同步", + "errorLoadingLyrics": "加载歌词时出错:{error}", + "@errorLoadingLyrics": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "errorParsingLyrics": "解析歌词时出错:{error}", + "@errorParsingLyrics": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "noTracksInQueue": "队列中没有曲目", + "unknownArtist": "未知艺术家", + "showLyrics": "显示歌词", + "showQueue": "显示队列", + "showCover": "显示封面", + "importedLyricsLines": "为\"{title}\"导入了 {count} 行歌词", + "@importedLyricsLines": { + "placeholders": { + "count": { + "type": "int" + }, + "title": { + "type": "String" + } + } + }, + "selected": "已选择 {count} 项", + "@selected": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "addToPlaylist": "添加到播放列表", + "delete": "删除", + "groovyBox": "GroovyBox", + "library": "音乐库", + "importFiles": "导入文件", + "searchTracks": "搜索曲目...", + "searchTracksWithCount": "搜索曲目...(共 {total} 首)", + "@searchTracksWithCount": { + "placeholders": { + "total": { + "type": "int" + } + } + }, + "searchTracksFiltered": "搜索曲目...({filtered} / {total} 首)", + "@searchTracksFiltered": { + "placeholders": { + "filtered": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "error": "错误:{message}", + "@error": { + "placeholders": { + "message": { + "type": "String" + } + } + }, + "noTracksYet": "还没有曲目,请添加一些!", + "noTracksMatchSearch": "没有匹配搜索的曲目。", + "deleteTrack": "删除曲目?", + "confirmDeleteTrack": "您确定要删除\"{title}\"吗?此操作无法撤销。", + "@confirmDeleteTrack": { + "placeholders": { + "title": { + "type": "String" + } + } + }, + "deletedTrack": "已删除\"{title}\"", + "@deletedTrack": { + "placeholders": { + "title": { + "type": "String" + } + } + }, + "viewDetails": "查看详情", + "editMetadata": "编辑元数据", + "importLyrics": "导入歌词", + "noPlaylistsAvailable": "没有可用的播放列表,请先创建一个!", + "addedToPlaylist": "已添加到 {name}", + "@addedToPlaylist": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "trackDetails": "曲目详情", + "close": "关闭", + "title": "标题", + "artist": "艺术家", + "album": "专辑", + "duration": "时长", + "fileSize": "文件大小", + "filePath": "文件路径", + "dateAdded": "添加日期", + "albumArt": "专辑封面", + "present": "存在", + "editTrack": "编辑曲目", + "addedTracksToPlaylist": "已将 {count} 首曲目添加到 {name}", + "@addedTracksToPlaylist": { + "placeholders": { + "count": { + "type": "int" + }, + "name": { + "type": "String" + } + } + }, + "deleteTracks": "删除曲目?", + "confirmDeleteTracks": "您确定要删除 {count} 首曲目吗?这将从您的设备中移除它们。", + "@confirmDeleteTracks": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "deletedTracks": "已删除 {count} 首曲目", + "@deletedTracks": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "batchImportComplete": "批量导入完成:{matched} 匹配,{notMatched} 不匹配", + "@batchImportComplete": { + "placeholders": { + "matched": { + "type": "int" + }, + "notMatched": { + "type": "int" + } + } + }, + "settings": "设置", + "autoScan": "自动扫描", + "autoScanMusicLibraries": "自动扫描音乐库", + "autoScanDescription": "自动扫描音乐库中的新音乐文件", + "watchForChanges": "监视更改", + "watchForChangesDescription": "监视音乐库的文件更改", + "musicLibraries": "音乐库", + "scanLibraries": "扫描库", + "addMusicLibrary": "添加音乐库", + "addMusicLibraryDescription": "添加文件夹库来索引音乐文件。文件将被复制到内部存储以供播放。", + "noMusicLibrariesAdded": "尚未添加音乐库。", + "errorLoadingLibraries": "加载库时出错:{error}", + "@errorLoadingLibraries": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "remoteProviders": "远程提供商", + "indexRemoteProviders": "索引远程提供商", + "addRemoteProvider": "添加远程提供商", + "remoteProvidersDescription": "连接到远程媒体服务器,如Jellyfin,来访问您的音乐库。", + "noRemoteProvidersAdded": "尚未添加远程提供商。", + "errorLoadingProviders": "加载提供商时出错:{error}", + "@errorLoadingProviders": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "playerSettings": "播放器设置", + "playerSettingsDescription": "配置播放器行为和显示选项。", + "defaultPlayerScreen": "默认播放器屏幕", + "defaultPlayerScreenDescription": "选择打开播放器时显示的屏幕。", + "lyricsMode": "歌词模式", + "lyricsModeDescription": "选择歌词的显示方式。", + "continuePlaying": "继续播放", + "continuePlayingDescription": "队列为空后继续播放音乐", + "databaseManagement": "数据库管理", + "databaseManagementDescription": "管理您的音乐数据库和缓存文件。", + "resetTrackDatabase": "重置曲目数据库", + "resetTrackDatabaseDescription": "从数据库中移除所有曲目并删除缓存文件。此操作无法撤销。", + "errorLoadingSettings": "加载设置时出错:{error}", + "@errorLoadingSettings": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "addedMusicLibrary": "已添加音乐库:{path}", + "@addedMusicLibrary": { + "placeholders": { + "path": { + "type": "String" + } + } + }, + "errorAddingLibrary": "添加库时出错:{error}", + "@errorAddingLibrary": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "librariesScannedSuccessfully": "库扫描成功", + "errorScanningLibraries": "扫描库时出错:{error}", + "@errorScanningLibraries": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "noActiveRemoteProviders": "没有活动的远程提供商可索引", + "indexedRemoteProviders": "已索引 {count} 个远程提供商", + "@indexedRemoteProviders": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "errorIndexingRemoteProviders": "索引远程提供商时出错:{error}", + "@errorIndexingRemoteProviders": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "serverUrl": "服务器URL", + "serverUrlHint": "https://your-jellyfin-server.com", + "username": "用户名", + "password": "密码", + "add": "添加", + "allFieldsRequired": "所有字段都是必填的", + "addedRemoteProvider": "已添加远程提供商:{url}", + "@addedRemoteProvider": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "errorAddingProvider": "添加提供商时出错:{error}", + "@errorAddingProvider": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "confirmResetTrackDatabase": "这将永久删除数据库中的所有曲目,并移除所有缓存的音乐文件和专辑封面。此操作无法撤销。\n\n您确定要继续吗?", + "trackDatabaseReset": "曲目数据库已重置", + "errorResettingDatabase": "重置数据库时出错:{error}", + "@errorResettingDatabase": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "noTracksInAlbum": "此专辑中没有曲目", + "playAll": "播放全部", + "addToQueue": "添加到队列", + "noTracksInPlaylist": "此播放列表中没有曲目", + "noAlbumsFound": "未找到专辑", + "createOne": "创建一个", + "addNewPlaylist": "添加新播放列表", + "newPlaylist": "新播放列表", + "playlistName": "播放列表名称", + "create": "创建", + "noPlaylistsYet": "还没有播放列表", + "queue": "队列", + "appSettings": "应用设置", + "appSettingsDescription": "配置应用范围的设置和偏好。", + "language": "语言", + "languageDescription": "选择应用语言。", + "english": "English", + "chinese": "中文" +} diff --git a/lib/main.dart b/lib/main.dart index c099ce7..cbd3b1b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:groovybox/l10n/app_localizations.dart'; import 'package:groovybox/logic/audio_handler.dart'; import 'package:groovybox/logic/window_helpers.dart'; import 'package:groovybox/providers/audio_provider.dart'; +import 'package:groovybox/providers/locale_provider.dart'; import 'package:groovybox/providers/theme_provider.dart'; import 'package:groovybox/router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -52,6 +55,7 @@ class GroovyApp extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final themeMode = ref.watch(themeProvider); + final locale = ref.watch(localeProvider); final router = ref.watch(routerProvider); return MaterialApp.router( @@ -60,7 +64,18 @@ class GroovyApp extends ConsumerWidget { theme: ref.watch(lightThemeProvider), darkTheme: ref.watch(darkThemeProvider), themeMode: themeMode, + locale: locale, routerConfig: router, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ + Locale('en'), // English + Locale('zh'), // Chinese + ], ); } } diff --git a/lib/providers/audio_provider.g.dart b/lib/providers/audio_provider.g.dart index a24ff18..4f3228c 100644 --- a/lib/providers/audio_provider.g.dart +++ b/lib/providers/audio_provider.g.dart @@ -10,12 +10,12 @@ part of 'audio_provider.dart'; // ignore_for_file: type=lint, type=warning @ProviderFor(audioHandler) -const audioHandlerProvider = AudioHandlerProvider._(); +final audioHandlerProvider = AudioHandlerProvider._(); final class AudioHandlerProvider extends $FunctionalProvider with $Provider { - const AudioHandlerProvider._() + AudioHandlerProvider._() : super( from: null, argument: null, @@ -51,11 +51,11 @@ final class AudioHandlerProvider String _$audioHandlerHash() => r'65fbd92e049fe4f3a0763516f1e68e1614f7630f'; @ProviderFor(CurrentTrackNotifier) -const currentTrackProvider = CurrentTrackNotifierProvider._(); +final currentTrackProvider = CurrentTrackNotifierProvider._(); final class CurrentTrackNotifierProvider extends $NotifierProvider { - const CurrentTrackNotifierProvider._() + CurrentTrackNotifierProvider._() : super( from: null, argument: null, @@ -90,7 +90,6 @@ abstract class _$CurrentTrackNotifier extends $Notifier { @$mustCallSuper @override void runBuild() { - final created = build(); final ref = this.ref as $Ref; final element = ref.element @@ -100,16 +99,16 @@ abstract class _$CurrentTrackNotifier extends $Notifier { Object?, Object? >; - element.handleValue(ref, created); + element.handleCreate(ref, build); } } @ProviderFor(CurrentTrackMetadataNotifier) -const currentTrackMetadataProvider = CurrentTrackMetadataNotifierProvider._(); +final currentTrackMetadataProvider = CurrentTrackMetadataNotifierProvider._(); final class CurrentTrackMetadataNotifierProvider extends $NotifierProvider { - const CurrentTrackMetadataNotifierProvider._() + CurrentTrackMetadataNotifierProvider._() : super( from: null, argument: null, @@ -145,7 +144,6 @@ abstract class _$CurrentTrackMetadataNotifier @$mustCallSuper @override void runBuild() { - final created = build(); final ref = this.ref as $Ref; final element = ref.element @@ -155,16 +153,16 @@ abstract class _$CurrentTrackMetadataNotifier Object?, Object? >; - element.handleValue(ref, created); + element.handleCreate(ref, build); } } @ProviderFor(RemoteTrackLoadingNotifier) -const remoteTrackLoadingProvider = RemoteTrackLoadingNotifierProvider._(); +final remoteTrackLoadingProvider = RemoteTrackLoadingNotifierProvider._(); final class RemoteTrackLoadingNotifierProvider extends $NotifierProvider { - const RemoteTrackLoadingNotifierProvider._() + RemoteTrackLoadingNotifierProvider._() : super( from: null, argument: null, @@ -199,7 +197,6 @@ abstract class _$RemoteTrackLoadingNotifier extends $Notifier { @$mustCallSuper @override void runBuild() { - final created = build(); final ref = this.ref as $Ref; final element = ref.element @@ -209,6 +206,6 @@ abstract class _$RemoteTrackLoadingNotifier extends $Notifier { Object?, Object? >; - element.handleValue(ref, created); + element.handleCreate(ref, build); } } diff --git a/lib/providers/db_provider.g.dart b/lib/providers/db_provider.g.dart index 112dc52..cb45101 100644 --- a/lib/providers/db_provider.g.dart +++ b/lib/providers/db_provider.g.dart @@ -10,12 +10,12 @@ part of 'db_provider.dart'; // ignore_for_file: type=lint, type=warning @ProviderFor(database) -const databaseProvider = DatabaseProvider._(); +final databaseProvider = DatabaseProvider._(); final class DatabaseProvider extends $FunctionalProvider with $Provider { - const DatabaseProvider._() + DatabaseProvider._() : super( from: null, argument: null, diff --git a/lib/providers/locale_provider.dart b/lib/providers/locale_provider.dart new file mode 100644 index 0000000..8fc6dc0 --- /dev/null +++ b/lib/providers/locale_provider.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'locale_provider.g.dart'; + +@Riverpod(keepAlive: true) +class LocaleNotifier extends _$LocaleNotifier { + @override + Locale build() { + return const Locale('en'); // Default to English + } + + void setLocale(Locale locale) { + state = locale; + } + + void setEnglish() => state = const Locale('en'); + void setChinese() => state = const Locale('zh'); +} diff --git a/lib/providers/locale_provider.g.dart b/lib/providers/locale_provider.g.dart new file mode 100644 index 0000000..26efa61 --- /dev/null +++ b/lib/providers/locale_provider.g.dart @@ -0,0 +1,62 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'locale_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(LocaleNotifier) +final localeProvider = LocaleNotifierProvider._(); + +final class LocaleNotifierProvider + extends $NotifierProvider { + LocaleNotifierProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'localeProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$localeNotifierHash(); + + @$internal + @override + LocaleNotifier create() => LocaleNotifier(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(Locale value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$localeNotifierHash() => r'6218f767e67a0c2ae33762728b94c55c1bca352a'; + +abstract class _$LocaleNotifier extends $Notifier { + Locale build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + Locale, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/providers/lrc_fetcher_provider.g.dart b/lib/providers/lrc_fetcher_provider.g.dart index 4e734e4..9f150cb 100644 --- a/lib/providers/lrc_fetcher_provider.g.dart +++ b/lib/providers/lrc_fetcher_provider.g.dart @@ -10,11 +10,11 @@ part of 'lrc_fetcher_provider.dart'; // ignore_for_file: type=lint, type=warning @ProviderFor(LyricsFetcher) -const lyricsFetcherProvider = LyricsFetcherProvider._(); +final lyricsFetcherProvider = LyricsFetcherProvider._(); final class LyricsFetcherProvider extends $NotifierProvider { - const LyricsFetcherProvider._() + LyricsFetcherProvider._() : super( from: null, argument: null, @@ -48,7 +48,6 @@ abstract class _$LyricsFetcher extends $Notifier { @$mustCallSuper @override void runBuild() { - final created = build(); final ref = this.ref as $Ref; final element = ref.element @@ -58,6 +57,6 @@ abstract class _$LyricsFetcher extends $Notifier { Object?, Object? >; - element.handleValue(ref, created); + element.handleCreate(ref, build); } } diff --git a/lib/providers/settings_provider.g.dart b/lib/providers/settings_provider.g.dart index 967f212..51dc6d0 100644 --- a/lib/providers/settings_provider.g.dart +++ b/lib/providers/settings_provider.g.dart @@ -10,11 +10,11 @@ part of 'settings_provider.dart'; // ignore_for_file: type=lint, type=warning @ProviderFor(SettingsNotifier) -const settingsProvider = SettingsNotifierProvider._(); +final settingsProvider = SettingsNotifierProvider._(); final class SettingsNotifierProvider extends $AsyncNotifierProvider { - const SettingsNotifierProvider._() + SettingsNotifierProvider._() : super( from: null, argument: null, @@ -33,14 +33,13 @@ final class SettingsNotifierProvider SettingsNotifier create() => SettingsNotifier(); } -String _$settingsNotifierHash() => r'7c3a92d9ac94e175b79a3a4485bd9bbcc1e860f9'; +String _$settingsNotifierHash() => r'6825709a9c6ccdb5b2402aac1c75d742f5344527'; abstract class _$SettingsNotifier extends $AsyncNotifier { FutureOr build(); @$mustCallSuper @override void runBuild() { - final created = build(); final ref = this.ref as $Ref, SettingsState>; final element = ref.element @@ -50,16 +49,16 @@ abstract class _$SettingsNotifier extends $AsyncNotifier { Object?, Object? >; - element.handleValue(ref, created); + element.handleCreate(ref, build); } } @ProviderFor(ImportModeNotifier) -const importModeProvider = ImportModeNotifierProvider._(); +final importModeProvider = ImportModeNotifierProvider._(); final class ImportModeNotifierProvider extends $NotifierProvider { - const ImportModeNotifierProvider._() + ImportModeNotifierProvider._() : super( from: null, argument: null, @@ -94,7 +93,6 @@ abstract class _$ImportModeNotifier extends $Notifier { @$mustCallSuper @override void runBuild() { - final created = build(); final ref = this.ref as $Ref; final element = ref.element @@ -104,16 +102,16 @@ abstract class _$ImportModeNotifier extends $Notifier { Object?, Object? >; - element.handleValue(ref, created); + element.handleCreate(ref, build); } } @ProviderFor(AutoScanNotifier) -const autoScanProvider = AutoScanNotifierProvider._(); +final autoScanProvider = AutoScanNotifierProvider._(); final class AutoScanNotifierProvider extends $NotifierProvider { - const AutoScanNotifierProvider._() + AutoScanNotifierProvider._() : super( from: null, argument: null, @@ -147,7 +145,6 @@ abstract class _$AutoScanNotifier extends $Notifier { @$mustCallSuper @override void runBuild() { - final created = build(); final ref = this.ref as $Ref; final element = ref.element @@ -157,16 +154,16 @@ abstract class _$AutoScanNotifier extends $Notifier { Object?, Object? >; - element.handleValue(ref, created); + element.handleCreate(ref, build); } } @ProviderFor(WatchForChangesNotifier) -const watchForChangesProvider = WatchForChangesNotifierProvider._(); +final watchForChangesProvider = WatchForChangesNotifierProvider._(); final class WatchForChangesNotifierProvider extends $NotifierProvider { - const WatchForChangesNotifierProvider._() + WatchForChangesNotifierProvider._() : super( from: null, argument: null, @@ -201,7 +198,6 @@ abstract class _$WatchForChangesNotifier extends $Notifier { @$mustCallSuper @override void runBuild() { - final created = build(); final ref = this.ref as $Ref; final element = ref.element @@ -211,17 +207,17 @@ abstract class _$WatchForChangesNotifier extends $Notifier { Object?, Object? >; - element.handleValue(ref, created); + element.handleCreate(ref, build); } } @ProviderFor(DefaultPlayerScreenNotifier) -const defaultPlayerScreenProvider = DefaultPlayerScreenNotifierProvider._(); +final defaultPlayerScreenProvider = DefaultPlayerScreenNotifierProvider._(); final class DefaultPlayerScreenNotifierProvider extends $NotifierProvider { - const DefaultPlayerScreenNotifierProvider._() + DefaultPlayerScreenNotifierProvider._() : super( from: null, argument: null, @@ -257,7 +253,6 @@ abstract class _$DefaultPlayerScreenNotifier @$mustCallSuper @override void runBuild() { - final created = build(); final ref = this.ref as $Ref; final element = ref.element @@ -267,16 +262,16 @@ abstract class _$DefaultPlayerScreenNotifier Object?, Object? >; - element.handleValue(ref, created); + element.handleCreate(ref, build); } } @ProviderFor(LyricsModeNotifier) -const lyricsModeProvider = LyricsModeNotifierProvider._(); +final lyricsModeProvider = LyricsModeNotifierProvider._(); final class LyricsModeNotifierProvider extends $NotifierProvider { - const LyricsModeNotifierProvider._() + LyricsModeNotifierProvider._() : super( from: null, argument: null, @@ -311,7 +306,6 @@ abstract class _$LyricsModeNotifier extends $Notifier { @$mustCallSuper @override void runBuild() { - final created = build(); final ref = this.ref as $Ref; final element = ref.element @@ -321,16 +315,16 @@ abstract class _$LyricsModeNotifier extends $Notifier { Object?, Object? >; - element.handleValue(ref, created); + element.handleCreate(ref, build); } } @ProviderFor(ContinuePlaysNotifier) -const continuePlaysProvider = ContinuePlaysNotifierProvider._(); +final continuePlaysProvider = ContinuePlaysNotifierProvider._(); final class ContinuePlaysNotifierProvider extends $NotifierProvider { - const ContinuePlaysNotifierProvider._() + ContinuePlaysNotifierProvider._() : super( from: null, argument: null, @@ -365,7 +359,6 @@ abstract class _$ContinuePlaysNotifier extends $Notifier { @$mustCallSuper @override void runBuild() { - final created = build(); final ref = this.ref as $Ref; final element = ref.element @@ -375,6 +368,6 @@ abstract class _$ContinuePlaysNotifier extends $Notifier { Object?, Object? >; - element.handleValue(ref, created); + element.handleCreate(ref, build); } } diff --git a/lib/providers/theme_provider.g.dart b/lib/providers/theme_provider.g.dart index f67ba85..1c4d68b 100644 --- a/lib/providers/theme_provider.g.dart +++ b/lib/providers/theme_provider.g.dart @@ -10,11 +10,11 @@ part of 'theme_provider.dart'; // ignore_for_file: type=lint, type=warning @ProviderFor(ThemeNotifier) -const themeProvider = ThemeNotifierProvider._(); +final themeProvider = ThemeNotifierProvider._(); final class ThemeNotifierProvider extends $NotifierProvider { - const ThemeNotifierProvider._() + ThemeNotifierProvider._() : super( from: null, argument: null, @@ -48,7 +48,6 @@ abstract class _$ThemeNotifier extends $Notifier { @$mustCallSuper @override void runBuild() { - final created = build(); final ref = this.ref as $Ref; final element = ref.element @@ -58,16 +57,16 @@ abstract class _$ThemeNotifier extends $Notifier { Object?, Object? >; - element.handleValue(ref, created); + element.handleCreate(ref, build); } } @ProviderFor(SeedColorNotifier) -const seedColorProvider = SeedColorNotifierProvider._(); +final seedColorProvider = SeedColorNotifierProvider._(); final class SeedColorNotifierProvider extends $NotifierProvider { - const SeedColorNotifierProvider._() + SeedColorNotifierProvider._() : super( from: null, argument: null, @@ -101,7 +100,6 @@ abstract class _$SeedColorNotifier extends $Notifier { @$mustCallSuper @override void runBuild() { - final created = build(); final ref = this.ref as $Ref; final element = ref.element @@ -111,17 +109,17 @@ abstract class _$SeedColorNotifier extends $Notifier { Object?, Object? >; - element.handleValue(ref, created); + element.handleCreate(ref, build); } } @ProviderFor(currentTheme) -const currentThemeProvider = CurrentThemeProvider._(); +final currentThemeProvider = CurrentThemeProvider._(); final class CurrentThemeProvider extends $FunctionalProvider with $Provider { - const CurrentThemeProvider._() + CurrentThemeProvider._() : super( from: null, argument: null, @@ -157,12 +155,12 @@ final class CurrentThemeProvider String _$currentThemeHash() => r'29c9080ae24ba144ebb6e0aac60b16bebcc8a919'; @ProviderFor(lightTheme) -const lightThemeProvider = LightThemeProvider._(); +final lightThemeProvider = LightThemeProvider._(); final class LightThemeProvider extends $FunctionalProvider with $Provider { - const LightThemeProvider._() + LightThemeProvider._() : super( from: null, argument: null, @@ -198,12 +196,12 @@ final class LightThemeProvider String _$lightThemeHash() => r'be4e02c30ddc60a134ed2a1f7124caf162894889'; @ProviderFor(darkTheme) -const darkThemeProvider = DarkThemeProvider._(); +final darkThemeProvider = DarkThemeProvider._(); final class DarkThemeProvider extends $FunctionalProvider with $Provider { - const DarkThemeProvider._() + DarkThemeProvider._() : super( from: null, argument: null, diff --git a/lib/ui/screens/album_detail_screen.dart b/lib/ui/screens/album_detail_screen.dart index 64d7e17..d290f91 100644 --- a/lib/ui/screens/album_detail_screen.dart +++ b/lib/ui/screens/album_detail_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:gap/gap.dart'; import 'package:groovybox/data/db.dart'; import 'package:groovybox/data/playlist_repository.dart'; +import 'package:groovybox/l10n/app_localizations.dart'; import 'package:groovybox/providers/audio_provider.dart'; import 'package:groovybox/ui/widgets/track_tile.dart'; import 'package:groovybox/ui/widgets/universal_image.dart'; @@ -73,9 +74,9 @@ class AlbumDetailScreen extends HookConsumerWidget { final tracks = snapshot.data!; if (tracks.isEmpty) { - return const SizedBox( + return SizedBox( height: 200, - child: Center(child: Text('No tracks in this album')), + child: Center(child: Text(AppLocalizations.of(context)!.noTracksInAlbum)), ); } @@ -96,7 +97,7 @@ class AlbumDetailScreen extends HookConsumerWidget { _playAlbum(ref, tracks); }, icon: const Icon(Symbols.play_arrow), - label: const Text('Play All'), + label: Text(AppLocalizations.of(context)!.playAll), ), ), SizedBox( @@ -106,7 +107,7 @@ class AlbumDetailScreen extends HookConsumerWidget { _addToQueue(ref, tracks); }, icon: const Icon(Symbols.queue_music), - label: const Text('Add to Queue'), + label: Text(AppLocalizations.of(context)!.addToQueue), ), ), ], diff --git a/lib/ui/screens/library_screen.dart b/lib/ui/screens/library_screen.dart index aad9c1c..c0dc7f8 100644 --- a/lib/ui/screens/library_screen.dart +++ b/lib/ui/screens/library_screen.dart @@ -6,6 +6,7 @@ import 'package:gap/gap.dart'; import 'package:groovybox/data/db.dart'; import 'package:groovybox/data/playlist_repository.dart'; import 'package:groovybox/data/track_repository.dart'; +import 'package:groovybox/l10n/app_localizations.dart'; import 'package:groovybox/logic/lyrics_parser.dart'; import 'package:groovybox/logic/window_helpers.dart'; import 'package:groovybox/providers/audio_provider.dart'; @@ -82,7 +83,7 @@ class LibraryScreen extends HookConsumerWidget { onPressed: clearSelection, ), title: Text( - '${selectedTrackIds.value.length} selected', + AppLocalizations.of(context)!.selected(selectedTrackIds.value.length), ).textColor(Theme.of(context).colorScheme.onPrimary), backgroundColor: Theme.of(context).colorScheme.primary, actions: [ @@ -257,7 +258,7 @@ class LibraryScreen extends HookConsumerWidget { onPressed: clearSelection, ), title: Text( - '${selectedTrackIds.value.length} selected', + AppLocalizations.of(context)!.selected(selectedTrackIds.value.length), ).textColor(Theme.of(context).colorScheme.onPrimary), backgroundColor: Theme.of(context).colorScheme.primary, actions: [ diff --git a/lib/ui/screens/player_screen.dart b/lib/ui/screens/player_screen.dart index 302be4c..8414fed 100644 --- a/lib/ui/screens/player_screen.dart +++ b/lib/ui/screens/player_screen.dart @@ -11,6 +11,7 @@ import 'package:drift/drift.dart' as drift; import 'package:gap/gap.dart'; import 'package:groovybox/data/db.dart' as db; import 'package:groovybox/data/track_repository.dart'; +import 'package:groovybox/l10n/app_localizations.dart'; import 'package:groovybox/logic/lrc_providers.dart'; import 'package:groovybox/logic/lyrics_parser.dart'; import 'package:groovybox/logic/metadata_service.dart'; @@ -68,7 +69,7 @@ class PlayerScreen extends HookConsumerWidget { final index = snapshot.data?.index ?? 0; final medias = snapshot.data?.medias ?? []; if (medias.isEmpty || index < 0 || index >= medias.length) { - return const Center(child: Text('No media selected')); + return Center(child: Text(AppLocalizations.of(context)!.noMediaSelected)); } final media = medias[index]; @@ -614,7 +615,7 @@ class _PlayerLyrics extends HookConsumerWidget { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('No Lyrics Available'), + Text(AppLocalizations.of(context)!.noLyricsAvailable), const SizedBox(height: 16), if (lyricsFetcher.isLoading) @@ -625,7 +626,7 @@ class _PlayerLyrics extends HookConsumerWidget { else ElevatedButton.icon( icon: const Icon(Symbols.download), - label: const Text('Fetch Lyrics'), + label: Text(AppLocalizations.of(context)!.fetchLyrics), onPressed: () => _showFetchLyricsDialog( context, ref, @@ -742,7 +743,7 @@ class _FetchLyricsDialog extends StatelessWidget { .trim(); return AlertDialog( - title: const Text('Fetch Lyrics'), + title: Text(AppLocalizations.of(context)!.fetchLyrics), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/ui/screens/playlist_detail_screen.dart b/lib/ui/screens/playlist_detail_screen.dart index 870fa90..4bbe8bf 100644 --- a/lib/ui/screens/playlist_detail_screen.dart +++ b/lib/ui/screens/playlist_detail_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:gap/gap.dart'; import 'package:groovybox/data/db.dart'; import 'package:groovybox/data/playlist_repository.dart'; +import 'package:groovybox/l10n/app_localizations.dart'; import 'package:groovybox/providers/audio_provider.dart'; import 'package:groovybox/ui/widgets/track_tile.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -48,10 +49,10 @@ class PlaylistDetailScreen extends HookConsumerWidget { final tracks = snapshot.data!; if (tracks.isEmpty) { - return const SizedBox( + return SizedBox( height: 200, child: Center( - child: Text('No tracks in this playlist'), + child: Text(AppLocalizations.of(context)!.noTracksInPlaylist), ), ); } @@ -73,7 +74,7 @@ class PlaylistDetailScreen extends HookConsumerWidget { _playPlaylist(ref, tracks); }, icon: const Icon(Symbols.play_arrow), - label: const Text('Play All'), + label: Text(AppLocalizations.of(context)!.playAll), ), ), SizedBox( @@ -83,7 +84,7 @@ class PlaylistDetailScreen extends HookConsumerWidget { _addToQueue(ref, tracks); }, icon: const Icon(Symbols.queue_music), - label: const Text('Add to Queue'), + label: Text(AppLocalizations.of(context)!.addToQueue), ), ), ], diff --git a/lib/ui/screens/settings_screen.dart b/lib/ui/screens/settings_screen.dart index 142ace7..f6a0041 100644 --- a/lib/ui/screens/settings_screen.dart +++ b/lib/ui/screens/settings_screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:groovybox/data/track_repository.dart'; +import 'package:groovybox/l10n/app_localizations.dart'; +import 'package:groovybox/providers/locale_provider.dart'; import 'package:groovybox/providers/settings_provider.dart'; import 'package:groovybox/providers/watch_folder_provider.dart'; import 'package:groovybox/providers/remote_provider.dart'; @@ -400,6 +402,54 @@ class SettingsScreen extends ConsumerWidget { ), ), + // App Settings Section + Card( + margin: EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.appSettings, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ).padding(horizontal: 16, top: 16), + Text( + AppLocalizations.of(context)!.appSettingsDescription, + style: const TextStyle(color: Colors.grey, fontSize: 14), + ).padding(horizontal: 16, bottom: 8), + ListTile( + title: Text(AppLocalizations.of(context)!.language), + subtitle: Text( + AppLocalizations.of(context)!.languageDescription, + ), + trailing: DropdownButtonHideUnderline( + child: DropdownButton( + value: ref.watch(localeProvider), + onChanged: (Locale? value) { + if (value != null) { + ref.read(localeProvider.notifier).setLocale(value); + } + }, + items: const [ + DropdownMenuItem( + value: Locale('en'), + child: Text('English'), + ), + DropdownMenuItem( + value: Locale('zh'), + child: Text('中文'), + ), + ], + ), + ), + ), + const SizedBox(height: 8), + ], + ), + ), + // Database Management Section Card( margin: EdgeInsets.zero, diff --git a/lib/ui/tabs/albums_tab.dart b/lib/ui/tabs/albums_tab.dart index cdd013e..56e5396 100644 --- a/lib/ui/tabs/albums_tab.dart +++ b/lib/ui/tabs/albums_tab.dart @@ -1,6 +1,7 @@ import 'package:animations/animations.dart'; import 'package:flutter/material.dart'; import 'package:groovybox/data/playlist_repository.dart'; +import 'package:groovybox/l10n/app_localizations.dart'; import 'package:groovybox/ui/screens/album_detail_screen.dart'; import 'package:groovybox/ui/widgets/universal_image.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -22,7 +23,7 @@ class AlbumsTab extends HookConsumerWidget { final albums = snapshot.data!; if (albums.isEmpty) { - return const Center(child: Text('No albums found')); + return Center(child: Text(AppLocalizations.of(context)!.noAlbumsFound)); } return GridView.builder( diff --git a/lib/ui/tabs/playlists_tab.dart b/lib/ui/tabs/playlists_tab.dart index 5955440..137da63 100644 --- a/lib/ui/tabs/playlists_tab.dart +++ b/lib/ui/tabs/playlists_tab.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:groovybox/data/db.dart'; import 'package:groovybox/data/playlist_repository.dart'; +import 'package:groovybox/l10n/app_localizations.dart'; import 'package:groovybox/ui/screens/playlist_detail_screen.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -19,30 +20,30 @@ class PlaylistsTab extends HookConsumerWidget { ListTile( leading: const Icon(Symbols.add), trailing: const Icon(Symbols.chevron_right).padding(right: 8), - title: Text('Create One'), - subtitle: Text('Add a new playlist'), + title: Text(AppLocalizations.of(context)!.createOne), + subtitle: Text(AppLocalizations.of(context)!.addNewPlaylist), onTap: () async { final nameController = TextEditingController(); final name = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('New Playlist'), + title: Text(AppLocalizations.of(context)!.newPlaylist), content: TextField( controller: nameController, - decoration: const InputDecoration( - labelText: 'Playlist Name', + decoration: InputDecoration( + labelText: AppLocalizations.of(context)!.playlistName, ), autofocus: true, ), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + child: Text(AppLocalizations.of(context)!.cancel), ), TextButton( onPressed: () => Navigator.pop(context, nameController.text), - child: const Text('Create'), + child: Text(AppLocalizations.of(context)!.create), ), ], ), @@ -63,7 +64,7 @@ class PlaylistsTab extends HookConsumerWidget { final playlists = snapshot.data!; if (playlists.isEmpty) { - return const Center(child: Text('No playlists yet')); + return Center(child: Text(AppLocalizations.of(context)!.noPlaylistsYet)); } return ListView.builder( diff --git a/lib/ui/widgets/mini_player.dart b/lib/ui/widgets/mini_player.dart index b7e1c00..c99d225 100644 --- a/lib/ui/widgets/mini_player.dart +++ b/lib/ui/widgets/mini_player.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:groovybox/data/db.dart' as db; - +import 'package:groovybox/l10n/app_localizations.dart'; import 'package:groovybox/providers/audio_provider.dart'; import 'package:groovybox/ui/screens/player_screen.dart'; import 'package:groovybox/ui/widgets/track_tile.dart'; @@ -174,7 +174,7 @@ class _MobileMiniPlayer extends HookConsumerWidget { overflow: TextOverflow.ellipsis, ), Text( - currentMetadata?.artist ?? 'Unknown Artist', + currentMetadata?.artist ?? AppLocalizations.of(context)!.unknownArtist, style: Theme.of(context).textTheme.bodySmall, maxLines: 1, overflow: TextOverflow.ellipsis, @@ -408,7 +408,7 @@ class _DesktopMiniPlayer extends HookConsumerWidget { overflow: TextOverflow.ellipsis, ), Text( - currentMetadata?.artist ?? 'Unknown Artist', + currentMetadata?.artist ?? AppLocalizations.of(context)!.unknownArtist, style: Theme.of( context, ).textTheme.bodySmall, @@ -604,7 +604,7 @@ class _DesktopMiniPlayer extends HookConsumerWidget { padding: const EdgeInsets.fromLTRB(24, 12, 24, 8), child: Row( children: [ - const Text('Queue', style: TextStyle(fontSize: 20)), + Text(AppLocalizations.of(context)!.queue, style: TextStyle(fontSize: 20)), const Spacer(), IconButton( icon: const Icon(Symbols.close), @@ -621,7 +621,7 @@ class _DesktopMiniPlayer extends HookConsumerWidget { builder: (context, snapshot) { final playlist = snapshot.data; if (playlist == null || playlist.medias.isEmpty) { - return const Center(child: Text('No tracks in queue')); + return Center(child: Text(AppLocalizations.of(context)!.noTracksInQueue)); } return ReorderableListView.builder( @@ -678,7 +678,7 @@ class _DesktopMiniPlayer extends HookConsumerWidget { title: Uri.parse(media.uri).pathSegments.last, artist: media.extras?['artist'] as String? ?? - 'Unknown Artist', + AppLocalizations.of(context)!.unknownArtist, album: media.extras?['album'] as String?, duration: null, artUri: null, @@ -727,7 +727,7 @@ class _DesktopMiniPlayer extends HookConsumerWidget { ).pathSegments.last, artist: media.extras?['artist'] as String? ?? - 'Unknown Artist', + AppLocalizations.of(context)!.unknownArtist, album: media.extras?['album'] as String?, duration: null, artUri: null, diff --git a/lib/ui/widgets/track_tile.dart b/lib/ui/widgets/track_tile.dart index bac9a1f..75d1f6a 100644 --- a/lib/ui/widgets/track_tile.dart +++ b/lib/ui/widgets/track_tile.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:groovybox/data/db.dart' as db; +import 'package:groovybox/l10n/app_localizations.dart'; import 'package:groovybox/ui/widgets/universal_image.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -73,7 +74,7 @@ class TrackTile extends StatelessWidget { ), ), subtitle: Text( - '${track.artist ?? 'Unknown Artist'} • ${_formatDuration(track.duration)}', + '${track.artist ?? AppLocalizations.of(context)!.unknownArtist} • ${_formatDuration(track.duration)}', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( From f76a603b391d817e0fd96aca8bf886ec0e769ee0 Mon Sep 17 00:00:00 2001 From: liang-work Date: Sun, 4 Jan 2026 22:11:21 +0800 Subject: [PATCH 03/16] =?UTF-8?q?=F0=9F=94=A7=20Add=20a=20localized=20lang?= =?UTF-8?q?uage=20configuration=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- l10n.yaml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 l10n.yaml diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..15338f2 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,3 @@ +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart From cc9ee5412f0818e58b30c42337b1d38a15529558 Mon Sep 17 00:00:00 2001 From: liang-work Date: Mon, 5 Jan 2026 21:36:49 +0800 Subject: [PATCH 04/16] =?UTF-8?q?=E2=9C=A8=20Complete=20localization=20sup?= =?UTF-8?q?port?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/l10n/app_en.arb | 56 +++++++++- lib/l10n/app_localizations.dart | 48 ++++++++ lib/l10n/app_localizations_en.dart | 24 ++++ lib/l10n/app_localizations_zh.dart | 24 ++++ lib/l10n/app_zh.arb | 56 +++++++++- lib/main.dart | 21 +++- lib/ui/screens/library_screen.dart | 86 +++++++------- lib/ui/screens/player_screen.dart | 71 ++++++------ lib/ui/screens/settings_screen.dart | 166 ++++++++++++++-------------- lib/ui/tabs/playlists_tab.dart | 2 +- 10 files changed, 383 insertions(+), 171 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 206521a..1a20f3f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -338,5 +338,59 @@ "language": "Language", "languageDescription": "Choose the app language.", "english": "English", - "chinese": "中文" + "chinese": "中文", + "settingsTitle": "Settings", + "tracks": "Tracks", + "albums": "Albums", + "playlists": "Playlists", + "autoScan": "Auto Scan", + "autoScanMusicLibraries": "Auto-scan music libraries", + "autoScanDescription": "Automatically scan music libraries for new music files", + "watchForChanges": "Watch for changes", + "watchForChangesDescription": "Monitor music libraries for file changes", + "musicLibraries": "Music Libraries", + "scanLibraries": "Scan Libraries", + "addMusicLibrary": "Add Music Library", + "addMusicLibraryDescription": "Add folder libraries to index music files. Files will be copied to internal storage for playback.", + "noMusicLibrariesAdded": "No music libraries added yet.", + "remoteProviders": "Remote Providers", + "indexRemoteProviders": "Index Remote Providers", + "addRemoteProvider": "Add Remote Provider", + "remoteProvidersDescription": "Connect to remote media servers like Jellyfin to access your music library.", + "noRemoteProvidersAdded": "No remote providers added yet.", + "playerSettings": "Player Settings", + "playerSettingsDescription": "Configure player behavior and display options.", + "defaultPlayerScreen": "Default Player Screen", + "defaultPlayerScreenDescription": "Choose which screen to show when opening the player.", + "lyricsMode": "Lyrics Mode", + "lyricsModeDescription": "Choose how lyrics are displayed.", + "continuePlaying": "Continue Playing", + "continuePlayingDescription": "Continue playing music after the queue is empty", + "databaseManagement": "Database Management", + "databaseManagementDescription": "Manage your music database and cached files.", + "resetTrackDatabase": "Reset Track Database", + "resetTrackDatabaseDescription": "Remove all tracks from database and delete cached files. This action cannot be undone.", + "addedMusicLibrary": "Added music library: {path}", + "errorAddingLibrary": "Error adding library: {error}", + "librariesScannedSuccessfully": "Libraries scanned successfully", + "errorScanningLibraries": "Error scanning libraries: {error}", + "noActiveRemoteProviders": "No active remote providers to index", + "indexedRemoteProviders": "Indexed {count} remote provider(s)", + "errorIndexingRemoteProviders": "Error indexing remote providers: {error}", + "addedRemoteProvider": "Added remote provider: {url}", + "errorAddingProvider": "Error adding provider: {error}", + "confirmResetTrackDatabase": "This will permanently delete all tracks from the database and remove all cached music files and album art. This action cannot be undone.\n\nAre you sure you want to continue?", + "trackDatabaseReset": "Track database has been reset", + "errorResettingDatabase": "Error resetting database: {error}", + "addRemoteProviderDialog": "Add Remote Provider", + "serverUrl": "Server URL", + "serverUrlHint": "https://your-jellyfin-server.com", + "username": "Username", + "password": "Password", + "add": "Add", + "allFieldsRequired": "All fields are required", + "reset": "Reset", + "imported": "Imported", + "lyricsLines":"lyrics lines for", + "createdAt": "created at" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 867dff8..75061b4 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -931,6 +931,54 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'中文'** String get chinese; + + /// No description provided for @settingsTitle. + /// + /// In en, this message translates to: + /// **'Settings'** + String get settingsTitle; + + /// No description provided for @tracks. + /// + /// In en, this message translates to: + /// **'Tracks'** + String get tracks; + + /// No description provided for @albums. + /// + /// In en, this message translates to: + /// **'Albums'** + String get albums; + + /// No description provided for @playlists. + /// + /// In en, this message translates to: + /// **'Playlists'** + String get playlists; + + /// No description provided for @addRemoteProviderDialog. + /// + /// In en, this message translates to: + /// **'Add Remote Provider'** + String get addRemoteProviderDialog; + + /// No description provided for @imported. + /// + /// In en, this message translates to: + /// **'Imported'** + String get imported; + + /// No description provided for @lyricsLines. + /// + /// In en, this message translates to: + /// **'lyrics lines for'** + String get lyricsLines; + + /// No description provided for @createdAt. + /// + /// In en, this message translates to: + /// **'created at'** + String get createdAt; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index b1084d0..9a3078c 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -491,4 +491,28 @@ class AppLocalizationsEn extends AppLocalizations { @override String get chinese => '中文'; + + @override + String get settingsTitle => 'Settings'; + + @override + String get tracks => 'Tracks'; + + @override + String get albums => 'Albums'; + + @override + String get playlists => 'Playlists'; + + @override + String get addRemoteProviderDialog => 'Add Remote Provider'; + + @override + String get imported => 'Imported'; + + @override + String get lyricsLines => 'lyrics lines for'; + + @override + String get createdAt => 'created at'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 418f63d..2effcbc 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -479,4 +479,28 @@ class AppLocalizationsZh extends AppLocalizations { @override String get chinese => '中文'; + + @override + String get settingsTitle => '设置'; + + @override + String get tracks => '曲目'; + + @override + String get albums => '专辑'; + + @override + String get playlists => '播放列表'; + + @override + String get addRemoteProviderDialog => '添加远程提供商'; + + @override + String get imported => '已导入'; + + @override + String get lyricsLines => '歌词'; + + @override + String get createdAt => '创建于'; } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index b932df0..19aa86f 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -338,5 +338,59 @@ "language": "语言", "languageDescription": "选择应用语言。", "english": "English", - "chinese": "中文" + "chinese": "中文", + "settingsTitle": "设置", + "tracks": "曲目", + "albums": "专辑", + "playlists": "播放列表", + "autoScan": "自动扫描", + "autoScanMusicLibraries": "自动扫描音乐库", + "autoScanDescription": "自动扫描音乐库中的新音乐文件", + "watchForChanges": "监视更改", + "watchForChangesDescription": "监视音乐库的文件更改", + "musicLibraries": "音乐库", + "scanLibraries": "扫描库", + "addMusicLibrary": "添加音乐库", + "addMusicLibraryDescription": "添加文件夹库来索引音乐文件。文件将被复制到内部存储以供播放。", + "noMusicLibrariesAdded": "尚未添加音乐库。", + "remoteProviders": "远程提供商", + "indexRemoteProviders": "索引远程提供商", + "addRemoteProvider": "添加远程提供商", + "remoteProvidersDescription": "连接到远程媒体服务器,如Jellyfin,来访问您的音乐库。", + "noRemoteProvidersAdded": "尚未添加远程提供商。", + "playerSettings": "播放器设置", + "playerSettingsDescription": "配置播放器行为和显示选项。", + "defaultPlayerScreen": "默认播放器屏幕", + "defaultPlayerScreenDescription": "选择打开播放器时显示的屏幕。", + "lyricsMode": "歌词模式", + "lyricsModeDescription": "选择歌词的显示方式。", + "continuePlaying": "继续播放", + "continuePlayingDescription": "队列为空后继续播放音乐", + "databaseManagement": "数据库管理", + "databaseManagementDescription": "管理您的音乐数据库和缓存文件。", + "resetTrackDatabase": "重置曲目数据库", + "resetTrackDatabaseDescription": "从数据库中移除所有曲目并删除缓存文件。此操作无法撤销。", + "addedMusicLibrary": "已添加音乐库:{path}", + "errorAddingLibrary": "添加库时出错:{error}", + "librariesScannedSuccessfully": "库扫描成功", + "errorScanningLibraries": "扫描库时出错:{error}", + "noActiveRemoteProviders": "没有活动的远程提供商可索引", + "indexedRemoteProviders": "已索引 {count} 个远程提供商", + "errorIndexingRemoteProviders": "索引远程提供商时出错:{error}", + "addedRemoteProvider": "已添加远程提供商:{url}", + "errorAddingProvider": "添加提供商时出错:{error}", + "confirmResetTrackDatabase": "这将永久删除数据库中的所有曲目,并移除所有缓存的音乐文件和专辑封面。此操作无法撤销。\n\n您确定要继续吗?", + "trackDatabaseReset": "曲目数据库已重置", + "errorResettingDatabase": "重置数据库时出错:{error}", + "addRemoteProviderDialog": "添加远程提供商", + "serverUrl": "服务器URL", + "serverUrlHint": "https://your-jellyfin-server.com", + "username": "用户名", + "password": "密码", + "add": "添加", + "allFieldsRequired": "所有字段都是必填的", + "reset": "重置", + "imported": "已导入", + "lyricsLines": "歌词", + "createdAt":"创建于" } diff --git a/lib/main.dart b/lib/main.dart index cbd3b1b..35be7ff 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -42,22 +42,37 @@ Future main() async { // Get the provider container and set it on the audio handler final container = ProviderScope.containerOf(context); _audioHandler.setProviderContainer(container); - return const GroovyApp(); + return GroovyApp(); }, ), ), ); } -class GroovyApp extends ConsumerWidget { +class GroovyApp extends ConsumerStatefulWidget { const GroovyApp({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _GroovyAppState(); +} + +class _GroovyAppState extends ConsumerState { + @override + Widget build(BuildContext context) { final themeMode = ref.watch(themeProvider); final locale = ref.watch(localeProvider); final router = ref.watch(routerProvider); + // Listen to locale changes and force rebuild when locale changes + ref.listen(localeProvider, (previous, next) { + if (previous != next) { + // Force rebuild of the entire app when locale changes + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() {}); + }); + } + }); + return MaterialApp.router( title: 'GroovyBox', debugShowCheckedModeBanner: false, diff --git a/lib/ui/screens/library_screen.dart b/lib/ui/screens/library_screen.dart index c0dc7f8..6aa032f 100644 --- a/lib/ui/screens/library_screen.dart +++ b/lib/ui/screens/library_screen.dart @@ -90,7 +90,7 @@ class LibraryScreen extends HookConsumerWidget { IconButton( icon: const Icon(Symbols.playlist_add), color: Theme.of(context).colorScheme.onPrimary, - tooltip: 'Add to Playlist', + tooltip: AppLocalizations.of(context)!.addToPlaylist, onPressed: () { _batchAddToPlaylist( context, @@ -103,7 +103,7 @@ class LibraryScreen extends HookConsumerWidget { IconButton( icon: const Icon(Symbols.delete), color: Theme.of(context).colorScheme.onPrimary, - tooltip: 'Delete', + tooltip: AppLocalizations.of(context)!.delete, onPressed: () { _batchDelete( context, @@ -140,7 +140,7 @@ class LibraryScreen extends HookConsumerWidget { ), ], ) - : const Text('Library'), + : Text(AppLocalizations.of(context)!.library), actions: [ IconButton( onPressed: () { @@ -150,7 +150,7 @@ class LibraryScreen extends HookConsumerWidget { ), IconButton( icon: const Icon(Symbols.add_circle_outline), - tooltip: 'Import Files', + tooltip: AppLocalizations.of(context)!.importFiles, onPressed: () async { final result = await FilePicker.platform.pickFiles( type: FileType.any, @@ -208,18 +208,18 @@ class LibraryScreen extends HookConsumerWidget { extended: isExtraLargeScreen, selectedIndex: selectedTab!.value, onDestinationSelected: (index) => selectedTab.value = index, - destinations: const [ + destinations: [ NavigationRailDestination( icon: Icon(Symbols.audiotrack), - label: Text('Tracks'), + label: Text(AppLocalizations.of(context)!.tracks), ), NavigationRailDestination( icon: Icon(Symbols.album), - label: Text('Albums'), + label: Text(AppLocalizations.of(context)!.albums), ), NavigationRailDestination( icon: Icon(Symbols.queue_music), - label: Text('Playlists'), + label: Text(AppLocalizations.of(context)!.playlists), ), ], ), @@ -264,7 +264,7 @@ class LibraryScreen extends HookConsumerWidget { actions: [ IconButton( icon: const Icon(Symbols.playlist_add), - tooltip: 'Add to Playlist', + tooltip: AppLocalizations.of(context)!.addToPlaylist, color: Theme.of(context).colorScheme.onPrimary, onPressed: () { _batchAddToPlaylist( @@ -277,7 +277,7 @@ class LibraryScreen extends HookConsumerWidget { ), IconButton( icon: const Icon(Symbols.delete), - tooltip: 'Delete', + tooltip: AppLocalizations.of(context)!.delete, color: Theme.of(context).colorScheme.onPrimary, onPressed: () { _batchDelete( @@ -293,12 +293,12 @@ class LibraryScreen extends HookConsumerWidget { ) : AppBar( centerTitle: true, - title: const Text('Library'), - bottom: const TabBar( + title: Text(AppLocalizations.of(context)!.library), + bottom: TabBar( tabs: [ - Tab(text: 'Tracks', icon: Icon(Symbols.audiotrack)), - Tab(text: 'Albums', icon: Icon(Symbols.album)), - Tab(text: 'Playlists', icon: Icon(Symbols.queue_music)), + Tab(text: AppLocalizations.of(context)!.tracks, icon: Icon(Symbols.audiotrack)), + Tab(text: AppLocalizations.of(context)!.albums, icon: Icon(Symbols.album)), + Tab(text: AppLocalizations.of(context)!.playlists, icon: Icon(Symbols.queue_music)), ], ), actions: [ @@ -310,7 +310,7 @@ class LibraryScreen extends HookConsumerWidget { ), IconButton( icon: const Icon(Symbols.add_circle_outline), - tooltip: 'Import Files', + tooltip: AppLocalizations.of(context)!.importFiles, onPressed: () async { final result = await FilePicker.platform.pickFiles( type: FileType.any, @@ -392,12 +392,12 @@ class LibraryScreen extends HookConsumerWidget { // Calculate hintText String hintText; if (!snapshot.hasData || snapshot.hasError) { - hintText = 'Search tracks...'; + hintText = AppLocalizations.of(context)!.searchTracks; } else { final tracks = snapshot.data!; final totalTracks = tracks.length; if (searchQuery.value.isEmpty) { - hintText = 'Search tracks... ($totalTracks tracks)'; + hintText = '${AppLocalizations.of(context)!.searchTracks} ($totalTracks ${AppLocalizations.of(context)!.tracks})'; } else { final query = searchQuery.value.toLowerCase(); final filteredCount = tracks.where((track) { @@ -434,7 +434,7 @@ class LibraryScreen extends HookConsumerWidget { } else { final tracks = snapshot.data!; if (tracks.isEmpty) { - mainContent = const Center(child: Text('No tracks yet. Add some!')); + mainContent = Center(child: Text(AppLocalizations.of(context)!.noTracksYet)); } else { List filteredTracks; if (searchQuery.value.isEmpty) { @@ -636,7 +636,7 @@ class LibraryScreen extends HookConsumerWidget { children: [ ListTile( leading: const Icon(Symbols.playlist_add), - title: const Text('Add to Playlist'), + title: Text(AppLocalizations.of(context)!.addToPlaylist), onTap: () { Navigator.pop(context); _showAddToPlaylistDialog(context, ref, track); @@ -644,7 +644,7 @@ class LibraryScreen extends HookConsumerWidget { ), ListTile( leading: const Icon(Symbols.info), - title: const Text('View Details'), + title: Text(AppLocalizations.of(context)!.viewDetails), onTap: () { Navigator.pop(context); _showTrackDetails(context, ref, track); @@ -652,7 +652,7 @@ class LibraryScreen extends HookConsumerWidget { ), ListTile( leading: const Icon(Symbols.edit), - title: const Text('Edit Metadata'), + title: Text(AppLocalizations.of(context)!.editMetadata), onTap: () { Navigator.pop(context); _showEditDialog(context, ref, track); @@ -660,7 +660,7 @@ class LibraryScreen extends HookConsumerWidget { ), ListTile( leading: const Icon(Symbols.lyrics), - title: const Text('Import Lyrics'), + title: Text(AppLocalizations.of(context)!.importLyrics), onTap: () { Navigator.pop(context); _importLyricsForTrack(context, ref, track); @@ -811,16 +811,16 @@ class LibraryScreen extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - _buildDetailRow('Title', track.title), - _buildDetailRow('Artist', track.artist ?? 'Unknown'), - _buildDetailRow('Album', track.album ?? 'Unknown'), - _buildDetailRow('Duration', _formatDuration(track.duration)), - _buildDetailRow('File Size', fileSize), - _buildDetailRow('Library', libraryName), - _buildDetailRow('File Path', track.path), - _buildDetailRow('Date Added', dateAdded), + _buildDetailRow(AppLocalizations.of(context)!.title, track.title), + _buildDetailRow(AppLocalizations.of(context)!.artist, track.artist ?? 'Unknown'), + _buildDetailRow(AppLocalizations.of(context)!.album, track.album ?? 'Unknown'), + _buildDetailRow(AppLocalizations.of(context)!.duration, _formatDuration(track.duration)), + _buildDetailRow(AppLocalizations.of(context)!.fileSize, fileSize), + _buildDetailRow(AppLocalizations.of(context)!.library, libraryName), + _buildDetailRow(AppLocalizations.of(context)!.filePath, track.path), + _buildDetailRow(AppLocalizations.of(context)!.dateAdded, dateAdded), if (track.artUri != null) - _buildDetailRow('Album Art', 'Present'), + _buildDetailRow(AppLocalizations.of(context)!.albumArt, 'Present'), ], ), ), @@ -828,7 +828,7 @@ class LibraryScreen extends HookConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Close'), + child: Text(AppLocalizations.of(context)!.close), ), ], ), @@ -865,7 +865,7 @@ class LibraryScreen extends HookConsumerWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Edit Track'), + title: Text(AppLocalizations.of(context)!.editTrack), content: ConstrainedBox( constraints: BoxConstraints(maxWidth: screenSize.width * 0.8), child: Column( @@ -874,15 +874,15 @@ class LibraryScreen extends HookConsumerWidget { children: [ TextField( controller: titleController, - decoration: const InputDecoration(labelText: 'Title'), + decoration: InputDecoration(labelText: AppLocalizations.of(context)!.title), ), TextField( controller: artistController, - decoration: const InputDecoration(labelText: 'Artist'), + decoration: InputDecoration(labelText: AppLocalizations.of(context)!.artist), ), TextField( controller: albumController, - decoration: const InputDecoration(labelText: 'Album'), + decoration: InputDecoration(labelText: AppLocalizations.of(context)!.album), ), ], ), @@ -930,7 +930,7 @@ class LibraryScreen extends HookConsumerWidget { context: context, builder: (context) { return AlertDialog( - title: const Text('Add to Playlist'), + title: Text(AppLocalizations.of(context)!.addToPlaylist), content: ConstrainedBox( constraints: BoxConstraints( maxWidth: screenSize.width * 0.8, @@ -988,7 +988,7 @@ class LibraryScreen extends HookConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + child: Text(AppLocalizations.of(context)!.cancel), ), ], ); @@ -1005,7 +1005,7 @@ class LibraryScreen extends HookConsumerWidget { final confirm = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Delete Tracks?'), + title: Text(AppLocalizations.of(context)!.deleteTracks), content: Text( 'Are you sure you want to delete ${trackIds.length} tracks? ' 'This will remove them from your device.', @@ -1013,12 +1013,12 @@ class LibraryScreen extends HookConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), + child: Text(AppLocalizations.of(context)!.cancel), ), TextButton( onPressed: () => Navigator.pop(context, true), style: TextButton.styleFrom(foregroundColor: Colors.red), - child: const Text('Delete'), + child: Text(AppLocalizations.of(context)!.delete), ), ], ), @@ -1064,7 +1064,7 @@ class LibraryScreen extends HookConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - 'Imported ${lyricsData.lines.length} lyrics lines for "${track.title}"', + '${AppLocalizations.of(context)!.imported} ${lyricsData.lines.length} ${AppLocalizations.of(context)!.lyricsLines} for "${track.title}"', ), ), ); diff --git a/lib/ui/screens/player_screen.dart b/lib/ui/screens/player_screen.dart index 8414fed..d3532b2 100644 --- a/lib/ui/screens/player_screen.dart +++ b/lib/ui/screens/player_screen.dart @@ -711,7 +711,7 @@ class _PlayerLyrics extends HookConsumerWidget { } } } catch (e) { - return Center(child: Text('Error parsing lyrics: $e')); + return Center(child: Text(AppLocalizations.of(context)!.errorLoadingLyrics(e.toString()))); } } } @@ -753,15 +753,15 @@ class _FetchLyricsDialog extends StatelessWidget { text: TextSpan( style: TextStyle(color: Theme.of(context).colorScheme.onSurface), children: [ - const TextSpan(text: 'Search lyrics with '), + TextSpan(text: AppLocalizations.of(context)!.searchLyricsWith(searchTerm.split(' ').first)), TextSpan( - text: searchTerm, + text: ' $searchTerm', style: const TextStyle(fontWeight: FontWeight.bold), ), ], ), ), - Text('Where do you want to search lyrics from?'), + Text(AppLocalizations.of(context)!.whereToSearchLyrics), Card( child: Column( mainAxisSize: MainAxisSize.min, @@ -769,7 +769,7 @@ class _FetchLyricsDialog extends StatelessWidget { ListTile( dense: true, leading: const Icon(Symbols.library_music), - title: const Text('Musixmatch'), + title: Text(AppLocalizations.of(context)!.musixmatch), shape: RoundedRectangleBorder( borderRadius: const BorderRadius.all(Radius.circular(12)), ), @@ -788,7 +788,7 @@ class _FetchLyricsDialog extends StatelessWidget { ListTile( dense: true, leading: const Icon(Symbols.music_video), - title: const Text('NetEase'), + title: Text(AppLocalizations.of(context)!.netease), shape: RoundedRectangleBorder( borderRadius: const BorderRadius.all(Radius.circular(12)), ), @@ -807,7 +807,7 @@ class _FetchLyricsDialog extends StatelessWidget { ListTile( dense: true, leading: const Icon(Symbols.library_books), - title: const Text('Lrclib'), + title: Text(AppLocalizations.of(context)!.lrclib), shape: RoundedRectangleBorder( borderRadius: const BorderRadius.all(Radius.circular(12)), ), @@ -826,7 +826,7 @@ class _FetchLyricsDialog extends StatelessWidget { ListTile( dense: true, leading: const Icon(Symbols.file_upload), - title: const Text('Manual Import'), + title: Text(AppLocalizations.of(context)!.manualImport), shape: RoundedRectangleBorder( borderRadius: const BorderRadius.all(Radius.circular(12)), ), @@ -910,7 +910,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { return IconButton( icon: const Icon(Symbols.settings_applications), iconSize: 24, - tooltip: 'Adjust Lyrics', + tooltip: AppLocalizations.of(context)!.adjustLyricsTiming, onPressed: () => _showLyricsRefreshDialog( context, ref, @@ -974,7 +974,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Lyrics Options'), + title: Text(AppLocalizations.of(context)!.lyricsOptions), content: Column( spacing: 8, mainAxisSize: MainAxisSize.min, @@ -988,7 +988,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { Expanded( child: ElevatedButton.icon( icon: const Icon(Symbols.refresh), - label: const Text('Re-fetch'), + label: Text(AppLocalizations.of(context)!.refetch), onPressed: () { Navigator.of(context).pop(); final metadata = metadataAsync.value; @@ -1008,7 +1008,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { Expanded( child: ElevatedButton.icon( icon: const Icon(Symbols.clear), - label: const Text('Clear'), + label: Text(AppLocalizations.of(context)!.clear), style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, @@ -1060,7 +1060,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { width: double.infinity, child: ElevatedButton.icon( icon: const Icon(Symbols.sync), - label: const Text('Live Sync Lyrics'), + label: Text(AppLocalizations.of(context)!.liveLyricsSync), onPressed: () { Navigator.of(context).pop(); _showLiveLyricsSyncDialog( @@ -1077,7 +1077,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { width: double.infinity, child: ElevatedButton.icon( icon: const Icon(Symbols.tune), - label: const Text('Manual Offset'), + label: Text(AppLocalizations.of(context)!.manualOffset), onPressed: () { Navigator.of(context).pop(); _showLyricsOffsetDialog( @@ -1116,17 +1116,17 @@ class _LyricsAdjustButton extends HookConsumerWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Adjust Lyrics Timing'), + title: Text(AppLocalizations.of(context)!.adjustLyricsTiming), content: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text( - 'Enter offset in milliseconds.\nPositive values delay lyrics, negative values advance them.', + Text( + AppLocalizations.of(context)!.enterOffsetMs, ), const SizedBox(height: 16), TextField( controller: offsetController, - decoration: const InputDecoration(labelText: 'Offset (ms)'), + decoration: InputDecoration(labelText: AppLocalizations.of(context)!.offsetMs), keyboardType: TextInputType.number, ), ], @@ -1196,11 +1196,11 @@ class _ViewToggleButton extends StatelessWidget { String getTooltip() { switch (viewMode.value) { case ViewMode.cover: - return 'Show Lyrics'; + return AppLocalizations.of(context)!.showLyrics; case ViewMode.lyrics: - return 'Show Queue'; + return AppLocalizations.of(context)!.showQueue; case ViewMode.queue: - return 'Show Cover'; + return AppLocalizations.of(context)!.showCover; } } @@ -1251,7 +1251,7 @@ class _QueueView extends HookConsumerWidget { builder: (context, snapshot) { final playlist = snapshot.data; if (playlist == null || playlist.medias.isEmpty) { - return const Center(child: Text('No tracks in queue')); + return Center(child: Text(AppLocalizations.of(context)!.noTracksInQueue)); } return ReorderableListView.builder( @@ -1919,7 +1919,7 @@ class _LiveLyricsSyncDialog extends HookConsumerWidget { return Scaffold( appBar: AppBar( - title: const Text('Live Lyrics Sync'), + title: Text(AppLocalizations.of(context)!.liveLyricsSync), leading: IconButton( icon: const Icon(Symbols.close), onPressed: () => Navigator.of(context).pop(), @@ -1977,14 +1977,7 @@ class _LiveLyricsSyncDialog extends HookConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text('Offset: '), - Text( - '${tempOffset.value}ms', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), + Text(AppLocalizations.of(context)!.offset(tempOffset.value)), ], ), ), @@ -2000,30 +1993,30 @@ class _LiveLyricsSyncDialog extends HookConsumerWidget { children: [ ElevatedButton.icon( icon: const Icon(Symbols.fast_rewind), - label: const Text('-100ms'), + label: Text(AppLocalizations.of(context)!.minus100ms), onPressed: () => tempOffset.value = (tempOffset.value - 100), ), ElevatedButton.icon( icon: const Icon(Symbols.skip_previous), - label: const Text('-10ms'), + label: Text(AppLocalizations.of(context)!.plus10ms), onPressed: () => tempOffset.value = (tempOffset.value - 10), ), ElevatedButton.icon( icon: const Icon(Symbols.refresh), - label: const Text('Reset'), + label: Text(AppLocalizations.of(context)!.reset), onPressed: () => tempOffset.value = 0, ), ElevatedButton.icon( icon: const Icon(Symbols.skip_next), - label: const Text('+10ms'), + label: Text(AppLocalizations.of(context)!.plus10ms), onPressed: () => tempOffset.value = (tempOffset.value + 10), ), ElevatedButton.icon( icon: const Icon(Symbols.fast_forward), - label: const Text('+100ms'), + label: Text(AppLocalizations.of(context)!.plus100ms), onPressed: () => tempOffset.value = (tempOffset.value + 100), ), @@ -2036,7 +2029,7 @@ class _LiveLyricsSyncDialog extends HookConsumerWidget { padding: const EdgeInsets.all(16.0), child: Column( children: [ - const Text('Fine Adjustment'), + Text(AppLocalizations.of(context)!.fineAdjustment), Slider( value: tempOffset.value.toDouble().clamp(-5000.0, 5000.0), min: -5000, @@ -2180,7 +2173,7 @@ class _LiveLyricsPreview extends HookConsumerWidget { final lyricsData = LyricsData.fromJsonString(track.lyrics!); if (lyricsData.type != 'timed') { - return const Center(child: Text('Only timed lyrics can be synced')); + return Center(child: Text(AppLocalizations.of(context)!.onlyTimedLyricsCanBeSynced)); } return StreamBuilder( @@ -2261,7 +2254,7 @@ class _LiveLyricsPreview extends HookConsumerWidget { }, ); } catch (e) { - return Center(child: Text('Error loading lyrics: $e')); + return Center(child: Text(AppLocalizations.of(context)!.errorLoadingLyrics(e.toString()))); } } } diff --git a/lib/ui/screens/settings_screen.dart b/lib/ui/screens/settings_screen.dart index f6a0041..954e7bf 100644 --- a/lib/ui/screens/settings_screen.dart +++ b/lib/ui/screens/settings_screen.dart @@ -21,7 +21,7 @@ class SettingsScreen extends ConsumerWidget { final remoteProvidersAsync = ref.watch(remoteProvidersProvider); return Scaffold( - appBar: AppBar(title: const Text('Settings')), + appBar: AppBar(title: Text(AppLocalizations.of(context)!.settingsTitle)), body: settingsAsync.when( data: (settings) => Align( alignment: Alignment.topCenter, @@ -39,17 +39,17 @@ class SettingsScreen extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Auto Scan', - style: TextStyle( + Text( + AppLocalizations.of(context)!.autoScan, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ).padding(horizontal: 16, top: 16), SwitchListTile( - title: const Text('Auto-scan music libraries'), - subtitle: const Text( - 'Automatically scan music libraries for new music files', + title: Text(AppLocalizations.of(context)!.autoScanMusicLibraries), + subtitle: Text( + AppLocalizations.of(context)!.autoScanDescription, ), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(8)), @@ -60,9 +60,9 @@ class SettingsScreen extends ConsumerWidget { }, ), SwitchListTile( - title: const Text('Watch for changes'), - subtitle: const Text( - 'Monitor music libraries for file changes', + title: Text(AppLocalizations.of(context)!.watchForChanges), + subtitle: Text( + AppLocalizations.of(context)!.watchForChangesDescription, ), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(8)), @@ -91,9 +91,9 @@ class SettingsScreen extends ConsumerWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - 'Music Libraries', - style: TextStyle( + Text( + AppLocalizations.of(context)!.musicLibraries, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), @@ -104,7 +104,7 @@ class SettingsScreen extends ConsumerWidget { onPressed: () => _scanLibraries(context, ref), icon: const Icon(Symbols.refresh), - tooltip: 'Scan Libraries', + tooltip: AppLocalizations.of(context)!.scanLibraries, visualDensity: const VisualDensity( horizontal: -4, vertical: -4, @@ -114,7 +114,7 @@ class SettingsScreen extends ConsumerWidget { onPressed: () => _addMusicLibrary(context, ref), icon: const Icon(Symbols.add), - tooltip: 'Add Music Library', + tooltip: AppLocalizations.of(context)!.addMusicLibrary, visualDensity: const VisualDensity( horizontal: -4, vertical: -4, @@ -124,9 +124,9 @@ class SettingsScreen extends ConsumerWidget { ), ], ), - const Text( - 'Add folder libraries to index music files. Files will be copied to internal storage for playback.', - style: TextStyle( + Text( + AppLocalizations.of(context)!.addMusicLibraryDescription, + style: const TextStyle( color: Colors.grey, fontSize: 14, ), @@ -135,9 +135,9 @@ class SettingsScreen extends ConsumerWidget { ).padding(horizontal: 16, top: 16, bottom: 8), watchFoldersAsync.when( data: (folders) => folders.isEmpty - ? const Text( - 'No music libraries added yet.', - style: TextStyle( + ? Text( + AppLocalizations.of(context)!.noMusicLibrariesAdded, + style: const TextStyle( color: Colors.grey, fontSize: 14, ), @@ -209,9 +209,9 @@ class SettingsScreen extends ConsumerWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - 'Remote Providers', - style: TextStyle( + Text( + AppLocalizations.of(context)!.remoteProviders, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), @@ -222,7 +222,7 @@ class SettingsScreen extends ConsumerWidget { onPressed: () => _indexRemoteProviders(context, ref), icon: const Icon(Symbols.refresh), - tooltip: 'Index Remote Providers', + tooltip: AppLocalizations.of(context)!.indexRemoteProviders, visualDensity: const VisualDensity( horizontal: -4, vertical: -4, @@ -232,7 +232,7 @@ class SettingsScreen extends ConsumerWidget { onPressed: () => _addRemoteProvider(context, ref), icon: const Icon(Symbols.add), - tooltip: 'Add Remote Provider', + tooltip: AppLocalizations.of(context)!.addRemoteProvider, visualDensity: const VisualDensity( horizontal: -4, vertical: -4, @@ -242,9 +242,9 @@ class SettingsScreen extends ConsumerWidget { ), ], ), - const Text( - 'Connect to remote media servers like Jellyfin to access your music library.', - style: TextStyle( + Text( + AppLocalizations.of(context)!.remoteProvidersDescription, + style: const TextStyle( color: Colors.grey, fontSize: 14, ), @@ -253,9 +253,9 @@ class SettingsScreen extends ConsumerWidget { ).padding(horizontal: 16, top: 16, bottom: 8), remoteProvidersAsync.when( data: (providers) => providers.isEmpty - ? const Text( - 'No remote providers added yet.', - style: TextStyle( + ? Text( + AppLocalizations.of(context)!.noRemoteProvidersAdded, + style: const TextStyle( color: Colors.grey, fontSize: 14, ), @@ -321,21 +321,21 @@ class SettingsScreen extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Player Settings', - style: TextStyle( + Text( + AppLocalizations.of(context)!.playerSettings, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ).padding(horizontal: 16, top: 16), - const Text( - 'Configure player behavior and display options.', - style: TextStyle(color: Colors.grey, fontSize: 14), + Text( + AppLocalizations.of(context)!.playerSettingsDescription, + style: const TextStyle(color: Colors.grey, fontSize: 14), ).padding(horizontal: 16, bottom: 8), ListTile( - title: const Text('Default Player Screen'), - subtitle: const Text( - 'Choose which screen to show when opening the player.', + title: Text(AppLocalizations.of(context)!.defaultPlayerScreen), + subtitle: Text( + AppLocalizations.of(context)!.defaultPlayerScreenDescription, ), trailing: DropdownButtonHideUnderline( child: DropdownButton( @@ -359,9 +359,9 @@ class SettingsScreen extends ConsumerWidget { ), ), ListTile( - title: const Text('Lyrics Mode'), - subtitle: const Text( - 'Choose how lyrics are displayed.', + title: Text(AppLocalizations.of(context)!.lyricsMode), + subtitle: Text( + AppLocalizations.of(context)!.lyricsModeDescription, ), trailing: DropdownButtonHideUnderline( child: DropdownButton( @@ -383,9 +383,9 @@ class SettingsScreen extends ConsumerWidget { ), ), SwitchListTile( - title: const Text('Continue Playing'), - subtitle: const Text( - 'Continue playing music after the queue is empty', + title: Text(AppLocalizations.of(context)!.continuePlaying), + subtitle: Text( + AppLocalizations.of(context)!.continuePlayingDescription, ), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(8)), @@ -456,21 +456,21 @@ class SettingsScreen extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Database Management', - style: TextStyle( + Text( + AppLocalizations.of(context)!.databaseManagement, + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ).padding(horizontal: 16, top: 16), - const Text( - 'Manage your music database and cached files.', - style: TextStyle(color: Colors.grey, fontSize: 14), + Text( + AppLocalizations.of(context)!.databaseManagementDescription, + style: const TextStyle(color: Colors.grey, fontSize: 14), ).padding(horizontal: 16, bottom: 8), ListTile( - title: const Text('Reset Track Database'), - subtitle: const Text( - 'Remove all tracks from database and delete cached files. This action cannot be undone.', + title: Text(AppLocalizations.of(context)!.resetTrackDatabase), + subtitle: Text( + AppLocalizations.of(context)!.resetTrackDatabaseDescription, ), trailing: ElevatedButton( onPressed: () => _resetTrackDatabase(context, ref), @@ -478,7 +478,7 @@ class SettingsScreen extends ConsumerWidget { backgroundColor: Colors.red, foregroundColor: Colors.white, ), - child: const Text('Reset'), + child: Text(AppLocalizations.of(context)!.reset), ), ), const SizedBox(height: 8), @@ -507,14 +507,14 @@ class SettingsScreen extends ConsumerWidget { await service.addWatchFolder(path, recursive: true); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Added music library: $path')), + SnackBar(content: Text(AppLocalizations.of(context)!.addedMusicLibrary(path))), ); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of( context, - ).showSnackBar(SnackBar(content: Text('Error adding library: $e'))); + ).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.errorAddingLibrary(e.toString())))); } } } @@ -527,14 +527,14 @@ class SettingsScreen extends ConsumerWidget { await service.scanWatchFolders(); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Libraries scanned successfully')), + SnackBar(content: Text(AppLocalizations.of(context)!.librariesScannedSuccessfully)), ); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of( context, - ).showSnackBar(SnackBar(content: Text('Error scanning libraries: $e'))); + ).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.errorScanningLibraries(e.toString())))); } } } @@ -551,8 +551,8 @@ class SettingsScreen extends ConsumerWidget { if (activeProviders.isEmpty) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('No active remote providers to index'), + SnackBar( + content: Text(AppLocalizations.of(context)!.noActiveRemoteProviders), ), ); } @@ -571,7 +571,7 @@ class SettingsScreen extends ConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - 'Indexed ${activeProviders.length} remote provider(s)', + AppLocalizations.of(context)!.indexedRemoteProviders(activeProviders.length), ), ), ); @@ -605,27 +605,27 @@ class SettingsScreen extends ConsumerWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Add Remote Provider'), + title: Text(AppLocalizations.of(context)!.addRemoteProviderDialog), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: serverUrlController, - decoration: const InputDecoration( - labelText: 'Server URL', - hintText: 'https://your-jellyfin-server.com', + decoration: InputDecoration( + labelText: AppLocalizations.of(context)!.serverUrl, + hintText: AppLocalizations.of(context)!.serverUrlHint, ), keyboardType: TextInputType.url, ), const SizedBox(height: 16), TextField( controller: usernameController, - decoration: const InputDecoration(labelText: 'Username'), + decoration: InputDecoration(labelText: AppLocalizations.of(context)!.username), ), const SizedBox(height: 16), TextField( controller: passwordController, - decoration: const InputDecoration(labelText: 'Password'), + decoration: InputDecoration(labelText: AppLocalizations.of(context)!.password), obscureText: true, ), ], @@ -633,7 +633,7 @@ class SettingsScreen extends ConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), + child: Text(AppLocalizations.of(context)!.cancel), ), TextButton( onPressed: () async { @@ -643,7 +643,7 @@ class SettingsScreen extends ConsumerWidget { if (serverUrl.isEmpty || username.isEmpty || password.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('All fields are required')), + SnackBar(content: Text(AppLocalizations.of(context)!.allFieldsRequired)), ); return; } @@ -655,19 +655,19 @@ class SettingsScreen extends ConsumerWidget { Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Added remote provider: $serverUrl'), + content: Text(AppLocalizations.of(context)!.addedRemoteProvider(serverUrl)), ), ); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error adding provider: $e')), + SnackBar(content: Text(AppLocalizations.of(context)!.errorAddingProvider(e.toString()))), ); } } }, - child: const Text('Add'), + child: Text(AppLocalizations.of(context)!.add), ), ], ), @@ -678,14 +678,14 @@ class SettingsScreen extends ConsumerWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Reset Track Database'), - content: const Text( - 'This will permanently delete all tracks from the database and remove all cached music files and album art. This action cannot be undone.\n\nAre you sure you want to continue?', + title: Text(AppLocalizations.of(context)!.resetTrackDatabase), + content: Text( + AppLocalizations.of(context)!.confirmResetTrackDatabase, ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), + child: Text(AppLocalizations.of(context)!.cancel), ), TextButton( onPressed: () async { @@ -697,21 +697,21 @@ class SettingsScreen extends ConsumerWidget { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Track database has been reset'), + SnackBar( + content: Text(AppLocalizations.of(context)!.trackDatabaseReset), ), ); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error resetting database: $e')), + SnackBar(content: Text(AppLocalizations.of(context)!.errorResettingDatabase(e.toString()))), ); } } }, style: TextButton.styleFrom(foregroundColor: Colors.red), - child: const Text('Reset'), + child: Text(AppLocalizations.of(context)!.reset), ), ], ), diff --git a/lib/ui/tabs/playlists_tab.dart b/lib/ui/tabs/playlists_tab.dart index 137da63..9548007 100644 --- a/lib/ui/tabs/playlists_tab.dart +++ b/lib/ui/tabs/playlists_tab.dart @@ -75,7 +75,7 @@ class PlaylistsTab extends HookConsumerWidget { leading: const Icon(Symbols.queue_music), title: Text(playlist.name), subtitle: Text( - '${playlist.createdAt.day}/${playlist.createdAt.month}/${playlist.createdAt.year}', + '${AppLocalizations.of(context)!.createdAt} ${playlist.createdAt.day}/${playlist.createdAt.month}/${playlist.createdAt.year}', ), trailing: IconButton( icon: const Icon(Symbols.delete), From 1638deea865b4f526e9f995ed7eb01445f96de64 Mon Sep 17 00:00:00 2001 From: liang-work Date: Mon, 5 Jan 2026 21:54:09 +0800 Subject: [PATCH 05/16] =?UTF-8?q?=F0=9F=90=9B=20Continue=20adding=20locali?= =?UTF-8?q?zation=20support=20for=20the=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/l10n/app_en.arb | 11 ++++- lib/l10n/app_localizations.dart | 50 +++++++++++++++++++++- lib/l10n/app_localizations_en.dart | 28 +++++++++++- lib/l10n/app_localizations_zh.dart | 24 +++++++++++ lib/l10n/app_zh.arb | 10 ++++- lib/ui/screens/library_screen.dart | 69 ++++++++++++++++++------------ 6 files changed, 159 insertions(+), 33 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1a20f3f..803fb4c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -392,5 +392,14 @@ "reset": "Reset", "imported": "Imported", "lyricsLines":"lyrics lines for", - "createdAt": "created at" + "createdAt": "created at", + "matched": "matched", + "notMatched": "not matched", + "deleted": "deleted", + "confirmDelete": "confirm delete", + "thisWillRemoveThemFromYourDevice":"This will remove them from your device.", + "added": "added", + "to":"to", + "unknown":"Unknown", + "noPlaylistsAvailable":"No Playlists available" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 75061b4..1edcb36 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -419,7 +419,7 @@ abstract class AppLocalizations { /// No description provided for @noPlaylistsAvailable. /// /// In en, this message translates to: - /// **'No playlists available. Create one first!'** + /// **'No Playlists available'** String get noPlaylistsAvailable; /// No description provided for @addedToPlaylist. @@ -979,6 +979,54 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'created at'** String get createdAt; + + /// No description provided for @matched. + /// + /// In en, this message translates to: + /// **'matched'** + String get matched; + + /// No description provided for @notMatched. + /// + /// In en, this message translates to: + /// **'not matched'** + String get notMatched; + + /// No description provided for @deleted. + /// + /// In en, this message translates to: + /// **'deleted'** + String get deleted; + + /// No description provided for @confirmDelete. + /// + /// In en, this message translates to: + /// **'confirm delete'** + String get confirmDelete; + + /// No description provided for @thisWillRemoveThemFromYourDevice. + /// + /// In en, this message translates to: + /// **'This will remove them from your device.'** + String get thisWillRemoveThemFromYourDevice; + + /// No description provided for @added. + /// + /// In en, this message translates to: + /// **'added'** + String get added; + + /// No description provided for @to. + /// + /// In en, this message translates to: + /// **'to'** + String get to; + + /// No description provided for @unknown. + /// + /// In en, this message translates to: + /// **'Unknown'** + String get unknown; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 9a3078c..5608739 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -191,8 +191,7 @@ class AppLocalizationsEn extends AppLocalizations { String get importLyrics => 'Import Lyrics'; @override - String get noPlaylistsAvailable => - 'No playlists available. Create one first!'; + String get noPlaylistsAvailable => 'No Playlists available'; @override String addedToPlaylist(String name) { @@ -515,4 +514,29 @@ class AppLocalizationsEn extends AppLocalizations { @override String get createdAt => 'created at'; + + @override + String get matched => 'matched'; + + @override + String get notMatched => 'not matched'; + + @override + String get deleted => 'deleted'; + + @override + String get confirmDelete => 'confirm delete'; + + @override + String get thisWillRemoveThemFromYourDevice => + 'This will remove them from your device.'; + + @override + String get added => 'added'; + + @override + String get to => 'to'; + + @override + String get unknown => 'Unknown'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 2effcbc..1530cf6 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -503,4 +503,28 @@ class AppLocalizationsZh extends AppLocalizations { @override String get createdAt => '创建于'; + + @override + String get matched => '匹配的'; + + @override + String get notMatched => '未匹配的'; + + @override + String get deleted => '已删除'; + + @override + String get confirmDelete => '确认删除'; + + @override + String get thisWillRemoveThemFromYourDevice => '这将从您的设备上删除。'; + + @override + String get added => '已添加'; + + @override + String get to => '至'; + + @override + String get unknown => '未知'; } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 19aa86f..e231ffe 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -392,5 +392,13 @@ "reset": "重置", "imported": "已导入", "lyricsLines": "歌词", - "createdAt":"创建于" + "createdAt":"创建于", + "matched": "匹配的", + "notMatched": "未匹配的", + "deleted": "已删除", + "confirmDelete": "确认删除", + "thisWillRemoveThemFromYourDevice":"这将从您的设备上删除。", + "added": "已添加", + "to": "至", + "unknown": "未知" } diff --git a/lib/ui/screens/library_screen.dart b/lib/ui/screens/library_screen.dart index 6aa032f..f6eb500 100644 --- a/lib/ui/screens/library_screen.dart +++ b/lib/ui/screens/library_screen.dart @@ -421,7 +421,7 @@ class LibraryScreen extends HookConsumerWidget { return false; }).length; hintText = - 'Search tracks... ($filteredCount of $totalTracks tracks)'; + '${AppLocalizations.of(context)!.searchTracks}... ($filteredCount of $totalTracks ${AppLocalizations.of(context)!.tracks})'; } } @@ -464,8 +464,8 @@ class LibraryScreen extends HookConsumerWidget { } if (filteredTracks.isEmpty && searchQuery.value.isNotEmpty) { - mainContent = const Center( - child: Text('No tracks match your search.'), + mainContent = Center( + child: Text(AppLocalizations.of(context)!.noTracksMatchSearch), ); } else { mainContent = ListView.builder( @@ -492,7 +492,7 @@ class LibraryScreen extends HookConsumerWidget { overflow: TextOverflow.ellipsis, ), subtitle: Text( - '${track.artist ?? 'Unknown Artist'} • ${_formatDuration(track.duration)}', + '${track.artist ?? AppLocalizations.of(context)!.unknownArtist} • ${_formatDuration(track.duration)}', maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -514,15 +514,16 @@ class LibraryScreen extends HookConsumerWidget { context: context, builder: (context) { return AlertDialog( - title: const Text('Delete Track?'), + title: Text(AppLocalizations.of(context)!.deleteTrack), content: Text( - 'Are you sure you want to delete "${track.title}"? This cannot be undone.', + AppLocalizations.of(context)! + .confirmDeleteTrack(track.title), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), - child: const Text('Cancel'), + child: Text(AppLocalizations.of(context)!.cancel), ), TextButton( onPressed: () => @@ -530,7 +531,7 @@ class LibraryScreen extends HookConsumerWidget { style: TextButton.styleFrom( foregroundColor: Colors.red, ), - child: const Text('Delete'), + child: Text(AppLocalizations.of(context)!.delete), ), ], ); @@ -542,7 +543,12 @@ class LibraryScreen extends HookConsumerWidget { .read(trackRepositoryProvider.notifier) .deleteTrack(track.id); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Deleted "${track.title}"')), + SnackBar( + content: Text( + AppLocalizations.of(context)! + .deletedTrack(track.title), + ), + ), ); }, child: TrackTile( @@ -668,8 +674,8 @@ class LibraryScreen extends HookConsumerWidget { ), ListTile( leading: const Icon(Symbols.delete, color: Colors.red), - title: const Text( - 'Delete Track', + title: Text( + AppLocalizations.of(context)!.deleteTrack, style: TextStyle(color: Colors.red), ), onTap: () { @@ -700,7 +706,7 @@ class LibraryScreen extends HookConsumerWidget { // For simplicity, we'll assume the user wants to pick from *current* playlists. // Or we can use a Consumer widget inside the dialog. return AlertDialog( - title: const Text('Add to Playlist'), + title: Text(AppLocalizations.of(context)!.addToPlaylist), content: ConstrainedBox( constraints: BoxConstraints( maxWidth: screenSize.width * 0.8, @@ -719,8 +725,8 @@ class LibraryScreen extends HookConsumerWidget { } final playlists = snapshot.data!; if (playlists.isEmpty) { - return const Text( - 'No playlists available. Create one first!', + return Text( + AppLocalizations.of(context)!.noPlaylistsAvailable, ); } @@ -737,7 +743,10 @@ class LibraryScreen extends HookConsumerWidget { Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Added to ${playlist.name}'), + content: Text( + AppLocalizations.of(context)! + .addedToPlaylist(playlist.name), + ), ), ); }, @@ -753,7 +762,7 @@ class LibraryScreen extends HookConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + child: Text(AppLocalizations.of(context)!.cancel), ), ], ); @@ -767,9 +776,9 @@ class LibraryScreen extends HookConsumerWidget { Track track, ) async { // Try to get file info - String fileSize = 'Unknown'; - String libraryName = 'Unknown'; - String dateAdded = 'Unknown'; + String fileSize = AppLocalizations.of(context)!.unknown; + String libraryName = AppLocalizations.of(context)!.unknown; + String dateAdded = AppLocalizations.of(context)!.unknown; try { final file = File(track.path); @@ -803,7 +812,7 @@ class LibraryScreen extends HookConsumerWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Track Details'), + title: Text(AppLocalizations.of(context)!.trackDetails), content: ConstrainedBox( constraints: BoxConstraints(maxWidth: screenSize.width * 0.8), child: SingleChildScrollView( @@ -890,7 +899,7 @@ class LibraryScreen extends HookConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + child: Text(AppLocalizations.of(context)!.cancel), ), TextButton( onPressed: () { @@ -904,7 +913,7 @@ class LibraryScreen extends HookConsumerWidget { ); Navigator.pop(context); }, - child: const Text('Save'), + child: Text(AppLocalizations.of(context)!.save), ), ], ), @@ -949,7 +958,7 @@ class LibraryScreen extends HookConsumerWidget { } final playlists = snapshot.data!; if (playlists.isEmpty) { - return const Text('No playlists available.'); + return Text(AppLocalizations.of(context)!.noPlaylistsAvailable); } return SingleChildScrollView( @@ -971,7 +980,7 @@ class LibraryScreen extends HookConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - 'Added ${trackIds.length} tracks to ${playlist.name}', + '${AppLocalizations.of(context)!.added} ${trackIds.length} ${AppLocalizations.of(context)!.tracks} ${AppLocalizations.of(context)!.to} ${playlist.name}', ), ), ); @@ -1007,8 +1016,8 @@ class LibraryScreen extends HookConsumerWidget { builder: (context) => AlertDialog( title: Text(AppLocalizations.of(context)!.deleteTracks), content: Text( - 'Are you sure you want to delete ${trackIds.length} tracks? ' - 'This will remove them from your device.', + '${AppLocalizations.of(context)!.confirmDelete} ${trackIds.length} ${AppLocalizations.of(context)!.tracks}? ' + '${AppLocalizations.of(context)!.thisWillRemoveThemFromYourDevice}', ), actions: [ TextButton( @@ -1032,7 +1041,11 @@ class LibraryScreen extends HookConsumerWidget { onSuccess(); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Deleted ${trackIds.length} tracks')), + SnackBar( + content: Text( + '${AppLocalizations.of(context)!.deleted} ${trackIds.length} ${AppLocalizations.of(context)!.tracks}', + ), + ), ); } } @@ -1115,7 +1128,7 @@ class LibraryScreen extends HookConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - 'Batch import complete: $matched matched, $notMatched not matched', + '${AppLocalizations.of(context)!.batchImportComplete} $matched ${AppLocalizations.of(context)!.matched}, $notMatched ${AppLocalizations.of(context)!.notMatched}', ), ), ); From 876fcbab0fec8020d0c8870d0b37782c365e1cac Mon Sep 17 00:00:00 2001 From: liang-work Date: Tue, 6 Jan 2026 19:35:23 +0800 Subject: [PATCH 06/16] =?UTF-8?q?=F0=9F=8C=90=20Supplement=20localization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- coding.txt | 31 ++++++++++++++++++++++++++++ lib/providers/locale_provider.dart | 26 ++++++++++++++++++++--- lib/providers/locale_provider.g.dart | 2 +- lib/ui/screens/player_screen.dart | 6 +++--- 4 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 coding.txt diff --git a/coding.txt b/coding.txt new file mode 100644 index 0000000..54283e6 --- /dev/null +++ b/coding.txt @@ -0,0 +1,31 @@ +代码风格 +我们采用统一的代码风格指南,以确保代码在整个项目中保持一致。 + +括号 +使用 K&R 风格的括号。 +函数定义和控制结构的左括号应与关键字在同一行。 +例如: +if (condition) { + // code +} else { + // code +} + +缩进 +使用 4 个空格进行缩进。 +不要使用制表符(Tab)。 +命名约定 +变量和函数名使用驼峰命名法(camelCase)。 +特殊语言(比如 C#)遵循其社区惯例。 +常量使用全大写字母和下划线分隔(例如: MAX_VALUE)。 +对于 C#,使用 PascalCase(例如: MaxValue)。 +对于 Dart,使用驼峰命名法并在开头添加一个 k(例如: kMaxValue)。 +注释 +我们 Solsynthizers 都自认为是有良好代码习惯,并且总是撰写 Clean Code。 +因此,我们习惯不在代码上加注释,除非是非常复杂的逻辑或算法,或者有特殊的处理逻辑。 +使用中性的命名方法 +避免使用过于具体的名称,确保代码可以在不同场景下复用。 +模块化设计 +将功能拆分为独立的模块,便于维护和复用。 +足够的可配置项 +提供配置选项,以适应不同的使用场景。 \ No newline at end of file diff --git a/lib/providers/locale_provider.dart b/lib/providers/locale_provider.dart index 8fc6dc0..49973d7 100644 --- a/lib/providers/locale_provider.dart +++ b/lib/providers/locale_provider.dart @@ -1,19 +1,39 @@ import 'package:flutter/material.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; part 'locale_provider.g.dart'; @Riverpod(keepAlive: true) class LocaleNotifier extends _$LocaleNotifier { + static const String _localeKey = 'locale'; + @override Locale build() { + // Load saved locale asynchronously + _loadLocale(); return const Locale('en'); // Default to English } - void setLocale(Locale locale) { + Future _loadLocale() async { + final prefs = await SharedPreferences.getInstance(); + final localeString = prefs.getString(_localeKey); + if (localeString == 'zh') { + state = const Locale('zh'); + } + } + + Future setLocale(Locale locale) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_localeKey, locale.languageCode); state = locale; } - void setEnglish() => state = const Locale('en'); - void setChinese() => state = const Locale('zh'); + Future setEnglish() async { + await setLocale(const Locale('en')); + } + + Future setChinese() async { + await setLocale(const Locale('zh')); + } } diff --git a/lib/providers/locale_provider.g.dart b/lib/providers/locale_provider.g.dart index 26efa61..a25e06d 100644 --- a/lib/providers/locale_provider.g.dart +++ b/lib/providers/locale_provider.g.dart @@ -41,7 +41,7 @@ final class LocaleNotifierProvider } } -String _$localeNotifierHash() => r'6218f767e67a0c2ae33762728b94c55c1bca352a'; +String _$localeNotifierHash() => r'838dee78faf52f7e5a7a9c2dc6d666f49e9553ee'; abstract class _$LocaleNotifier extends $Notifier { Locale build(); diff --git a/lib/ui/screens/player_screen.dart b/lib/ui/screens/player_screen.dart index d3532b2..1a5fad3 100644 --- a/lib/ui/screens/player_screen.dart +++ b/lib/ui/screens/player_screen.dart @@ -843,7 +843,7 @@ class _FetchLyricsDialog extends StatelessWidget { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), + child: Text(AppLocalizations.of(context)!.cancel), ), ], ); @@ -1096,7 +1096,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), + child: Text(AppLocalizations.of(context)!.cancel), ), ], ), @@ -1134,7 +1134,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), + child: Text(AppLocalizations.of(context)!.cancel), ), TextButton( onPressed: () async { From d178a2d16d393d251024e97d1a9899395d639c1b Mon Sep 17 00:00:00 2001 From: liang-work Date: Tue, 6 Jan 2026 19:40:21 +0800 Subject: [PATCH 07/16] =?UTF-8?q?=F0=9F=94=A5=20Delete=20incorrectly=20upl?= =?UTF-8?q?oaded=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- coding.txt | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 coding.txt diff --git a/coding.txt b/coding.txt deleted file mode 100644 index 54283e6..0000000 --- a/coding.txt +++ /dev/null @@ -1,31 +0,0 @@ -代码风格 -我们采用统一的代码风格指南,以确保代码在整个项目中保持一致。 - -括号 -使用 K&R 风格的括号。 -函数定义和控制结构的左括号应与关键字在同一行。 -例如: -if (condition) { - // code -} else { - // code -} - -缩进 -使用 4 个空格进行缩进。 -不要使用制表符(Tab)。 -命名约定 -变量和函数名使用驼峰命名法(camelCase)。 -特殊语言(比如 C#)遵循其社区惯例。 -常量使用全大写字母和下划线分隔(例如: MAX_VALUE)。 -对于 C#,使用 PascalCase(例如: MaxValue)。 -对于 Dart,使用驼峰命名法并在开头添加一个 k(例如: kMaxValue)。 -注释 -我们 Solsynthizers 都自认为是有良好代码习惯,并且总是撰写 Clean Code。 -因此,我们习惯不在代码上加注释,除非是非常复杂的逻辑或算法,或者有特殊的处理逻辑。 -使用中性的命名方法 -避免使用过于具体的名称,确保代码可以在不同场景下复用。 -模块化设计 -将功能拆分为独立的模块,便于维护和复用。 -足够的可配置项 -提供配置选项,以适应不同的使用场景。 \ No newline at end of file From e4d56c4558f25b626b8a6d58a2dde441a97eae1a Mon Sep 17 00:00:00 2001 From: liang-work Date: Tue, 6 Jan 2026 22:26:52 +0800 Subject: [PATCH 08/16] =?UTF-8?q?=F0=9F=94=A5=20delete=20old=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- l10n.yaml | 3 - lib/l10n/app_en.arb | 405 ----------- lib/l10n/app_localizations.dart | 1064 ---------------------------- lib/l10n/app_localizations_en.dart | 542 -------------- lib/l10n/app_localizations_zh.dart | 530 -------------- lib/l10n/app_zh.arb | 404 ----------- 6 files changed, 2948 deletions(-) delete mode 100644 l10n.yaml delete mode 100644 lib/l10n/app_en.arb delete mode 100644 lib/l10n/app_localizations.dart delete mode 100644 lib/l10n/app_localizations_en.dart delete mode 100644 lib/l10n/app_localizations_zh.dart delete mode 100644 lib/l10n/app_zh.arb diff --git a/l10n.yaml b/l10n.yaml deleted file mode 100644 index 15338f2..0000000 --- a/l10n.yaml +++ /dev/null @@ -1,3 +0,0 @@ -arb-dir: lib/l10n -template-arb-file: app_en.arb -output-localization-file: app_localizations.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb deleted file mode 100644 index 803fb4c..0000000 --- a/lib/l10n/app_en.arb +++ /dev/null @@ -1,405 +0,0 @@ -{ - "noMediaSelected": "No media selected", - "noLyricsAvailable": "No Lyrics Available", - "fetchLyrics": "Fetch Lyrics", - "searchLyricsWith": "Search lyrics with {searchTerm}", - "@searchLyricsWith": { - "placeholders": { - "searchTerm": { - "type": "String" - } - } - }, - "whereToSearchLyrics": "Where do you want to search lyrics from?", - "musixmatch": "Musixmatch", - "netease": "NetEase", - "lrclib": "Lrclib", - "manualImport": "Manual Import", - "cancel": "Cancel", - "lyricsOptions": "Lyrics Options", - "refetch": "Re-fetch", - "clear": "Clear", - "liveSyncLyrics": "Live Sync Lyrics", - "manualOffset": "Manual Offset", - "adjustLyricsTiming": "Adjust Lyrics Timing", - "enterOffsetMs": "Enter offset in milliseconds.\nPositive values delay lyrics, negative values advance them.", - "offsetMs": "Offset (ms)", - "save": "Save", - "liveLyricsSync": "Live Lyrics Sync", - "offset": "Offset: {value}ms", - "@offset": { - "placeholders": { - "value": { - "type": "int" - } - } - }, - "minus100ms": "-100ms", - "plus10ms": "+10ms", - "reset": "Reset", - "plus100ms": "+100ms", - "fineAdjustment": "Fine Adjustment", - "onlyTimedLyricsCanBeSynced": "Only timed lyrics can be synced", - "errorLoadingLyrics": "Error loading lyrics: {error}", - "@errorLoadingLyrics": { - "placeholders": { - "error": { - "type": "String" - } - } - }, - "errorParsingLyrics": "Error parsing lyrics: {error}", - "@errorParsingLyrics": { - "placeholders": { - "error": { - "type": "String" - } - } - }, - "noTracksInQueue": "No tracks in queue", - "unknownArtist": "Unknown Artist", - "showLyrics": "Show Lyrics", - "showQueue": "Show Queue", - "showCover": "Show Cover", - "importedLyricsLines": "Imported {count} lyrics lines for \"{title}\"", - "@importedLyricsLines": { - "placeholders": { - "count": { - "type": "int" - }, - "title": { - "type": "String" - } - } - }, - "selected": "{count} selected", - "@selected": { - "placeholders": { - "count": { - "type": "int" - } - } - }, - "addToPlaylist": "Add to Playlist", - "delete": "Delete", - "groovyBox": "GroovyBox", - "library": "Library", - "importFiles": "Import Files", - "searchTracks": "Search tracks...", - "searchTracksWithCount": "Search tracks... ({total} tracks)", - "@searchTracksWithCount": { - "placeholders": { - "total": { - "type": "int" - } - } - }, - "searchTracksFiltered": "Search tracks... ({filtered} of {total} tracks)", - "@searchTracksFiltered": { - "placeholders": { - "filtered": { - "type": "int" - }, - "total": { - "type": "int" - } - } - }, - "error": "Error: {message}", - "@error": { - "placeholders": { - "message": { - "type": "String" - } - } - }, - "noTracksYet": "No tracks yet. Add some!", - "noTracksMatchSearch": "No tracks match your search.", - "deleteTrack": "Delete Track?", - "confirmDeleteTrack": "Are you sure you want to delete \"{title}\"? This cannot be undone.", - "@confirmDeleteTrack": { - "placeholders": { - "title": { - "type": "String" - } - } - }, - "deletedTrack": "Deleted \"{title}\"", - "@deletedTrack": { - "placeholders": { - "title": { - "type": "String" - } - } - }, - "viewDetails": "View Details", - "editMetadata": "Edit Metadata", - "importLyrics": "Import Lyrics", - "noPlaylistsAvailable": "No playlists available. Create one first!", - "addedToPlaylist": "Added to {name}", - "@addedToPlaylist": { - "placeholders": { - "name": { - "type": "String" - } - } - }, - "trackDetails": "Track Details", - "close": "Close", - "title": "Title", - "artist": "Artist", - "album": "Album", - "duration": "Duration", - "fileSize": "File Size", - "filePath": "File Path", - "dateAdded": "Date Added", - "albumArt": "Album Art", - "present": "Present", - "editTrack": "Edit Track", - "addedTracksToPlaylist": "Added {count} tracks to {name}", - "@addedTracksToPlaylist": { - "placeholders": { - "count": { - "type": "int" - }, - "name": { - "type": "String" - } - } - }, - "deleteTracks": "Delete Tracks?", - "confirmDeleteTracks": "Are you sure you want to delete {count} tracks? This will remove them from your device.", - "@confirmDeleteTracks": { - "placeholders": { - "count": { - "type": "int" - } - } - }, - "deletedTracks": "Deleted {count} tracks", - "@deletedTracks": { - "placeholders": { - "count": { - "type": "int" - } - } - }, - "batchImportComplete": "Batch import complete: {matched} matched, {notMatched} not matched", - "@batchImportComplete": { - "placeholders": { - "matched": { - "type": "int" - }, - "notMatched": { - "type": "int" - } - } - }, - "settings": "Settings", - "autoScan": "Auto Scan", - "autoScanMusicLibraries": "Auto-scan music libraries", - "autoScanDescription": "Automatically scan music libraries for new music files", - "watchForChanges": "Watch for changes", - "watchForChangesDescription": "Monitor music libraries for file changes", - "musicLibraries": "Music Libraries", - "scanLibraries": "Scan Libraries", - "addMusicLibrary": "Add Music Library", - "addMusicLibraryDescription": "Add folder libraries to index music files. Files will be copied to internal storage for playback.", - "noMusicLibrariesAdded": "No music libraries added yet.", - "errorLoadingLibraries": "Error loading libraries: {error}", - "@errorLoadingLibraries": { - "placeholders": { - "error": { - "type": "String" - } - } - }, - "remoteProviders": "Remote Providers", - "indexRemoteProviders": "Index Remote Providers", - "addRemoteProvider": "Add Remote Provider", - "remoteProvidersDescription": "Connect to remote media servers like Jellyfin to access your music library.", - "noRemoteProvidersAdded": "No remote providers added yet.", - "errorLoadingProviders": "Error loading providers: {error}", - "@errorLoadingProviders": { - "placeholders": { - "error": { - "type": "String" - } - } - }, - "playerSettings": "Player Settings", - "playerSettingsDescription": "Configure player behavior and display options.", - "defaultPlayerScreen": "Default Player Screen", - "defaultPlayerScreenDescription": "Choose which screen to show when opening the player.", - "lyricsMode": "Lyrics Mode", - "lyricsModeDescription": "Choose how lyrics are displayed.", - "continuePlaying": "Continue Playing", - "continuePlayingDescription": "Continue playing music after the queue is empty", - "databaseManagement": "Database Management", - "databaseManagementDescription": "Manage your music database and cached files.", - "resetTrackDatabase": "Reset Track Database", - "resetTrackDatabaseDescription": "Remove all tracks from database and delete cached files. This action cannot be undone.", - "errorLoadingSettings": "Error loading settings: {error}", - "@errorLoadingSettings": { - "placeholders": { - "error": { - "type": "String" - } - } - }, - "addedMusicLibrary": "Added music library: {path}", - "@addedMusicLibrary": { - "placeholders": { - "path": { - "type": "String" - } - } - }, - "errorAddingLibrary": "Error adding library: {error}", - "@errorAddingLibrary": { - "placeholders": { - "error": { - "type": "String" - } - } - }, - "librariesScannedSuccessfully": "Libraries scanned successfully", - "errorScanningLibraries": "Error scanning libraries: {error}", - "@errorScanningLibraries": { - "placeholders": { - "error": { - "type": "String" - } - } - }, - "noActiveRemoteProviders": "No active remote providers to index", - "indexedRemoteProviders": "Indexed {count} remote provider(s)", - "@indexedRemoteProviders": { - "placeholders": { - "count": { - "type": "int" - } - } - }, - "errorIndexingRemoteProviders": "Error indexing remote providers: {error}", - "@errorIndexingRemoteProviders": { - "placeholders": { - "error": { - "type": "String" - } - } - }, - "serverUrl": "Server URL", - "serverUrlHint": "https://your-jellyfin-server.com", - "username": "Username", - "password": "Password", - "add": "Add", - "allFieldsRequired": "All fields are required", - "addedRemoteProvider": "Added remote provider: {url}", - "@addedRemoteProvider": { - "placeholders": { - "url": { - "type": "String" - } - } - }, - "errorAddingProvider": "Error adding provider: {error}", - "@errorAddingProvider": { - "placeholders": { - "error": { - "type": "String" - } - } - }, - "confirmResetTrackDatabase": "This will permanently delete all tracks from the database and remove all cached music files and album art. This action cannot be undone.\n\nAre you sure you want to continue?", - "trackDatabaseReset": "Track database has been reset", - "errorResettingDatabase": "Error resetting database: {error}", - "@errorResettingDatabase": { - "placeholders": { - "error": { - "type": "String" - } - } - }, - "noTracksInAlbum": "No tracks in this album", - "playAll": "Play All", - "addToQueue": "Add to Queue", - "noTracksInPlaylist": "No tracks in this playlist", - "noAlbumsFound": "No albums found", - "createOne": "Create One", - "addNewPlaylist": "Add a new playlist", - "newPlaylist": "New Playlist", - "playlistName": "Playlist Name", - "create": "Create", - "noPlaylistsYet": "No playlists yet", - "queue": "Queue", - "appSettings": "App Settings", - "appSettingsDescription": "Configure app-wide settings and preferences.", - "language": "Language", - "languageDescription": "Choose the app language.", - "english": "English", - "chinese": "中文", - "settingsTitle": "Settings", - "tracks": "Tracks", - "albums": "Albums", - "playlists": "Playlists", - "autoScan": "Auto Scan", - "autoScanMusicLibraries": "Auto-scan music libraries", - "autoScanDescription": "Automatically scan music libraries for new music files", - "watchForChanges": "Watch for changes", - "watchForChangesDescription": "Monitor music libraries for file changes", - "musicLibraries": "Music Libraries", - "scanLibraries": "Scan Libraries", - "addMusicLibrary": "Add Music Library", - "addMusicLibraryDescription": "Add folder libraries to index music files. Files will be copied to internal storage for playback.", - "noMusicLibrariesAdded": "No music libraries added yet.", - "remoteProviders": "Remote Providers", - "indexRemoteProviders": "Index Remote Providers", - "addRemoteProvider": "Add Remote Provider", - "remoteProvidersDescription": "Connect to remote media servers like Jellyfin to access your music library.", - "noRemoteProvidersAdded": "No remote providers added yet.", - "playerSettings": "Player Settings", - "playerSettingsDescription": "Configure player behavior and display options.", - "defaultPlayerScreen": "Default Player Screen", - "defaultPlayerScreenDescription": "Choose which screen to show when opening the player.", - "lyricsMode": "Lyrics Mode", - "lyricsModeDescription": "Choose how lyrics are displayed.", - "continuePlaying": "Continue Playing", - "continuePlayingDescription": "Continue playing music after the queue is empty", - "databaseManagement": "Database Management", - "databaseManagementDescription": "Manage your music database and cached files.", - "resetTrackDatabase": "Reset Track Database", - "resetTrackDatabaseDescription": "Remove all tracks from database and delete cached files. This action cannot be undone.", - "addedMusicLibrary": "Added music library: {path}", - "errorAddingLibrary": "Error adding library: {error}", - "librariesScannedSuccessfully": "Libraries scanned successfully", - "errorScanningLibraries": "Error scanning libraries: {error}", - "noActiveRemoteProviders": "No active remote providers to index", - "indexedRemoteProviders": "Indexed {count} remote provider(s)", - "errorIndexingRemoteProviders": "Error indexing remote providers: {error}", - "addedRemoteProvider": "Added remote provider: {url}", - "errorAddingProvider": "Error adding provider: {error}", - "confirmResetTrackDatabase": "This will permanently delete all tracks from the database and remove all cached music files and album art. This action cannot be undone.\n\nAre you sure you want to continue?", - "trackDatabaseReset": "Track database has been reset", - "errorResettingDatabase": "Error resetting database: {error}", - "addRemoteProviderDialog": "Add Remote Provider", - "serverUrl": "Server URL", - "serverUrlHint": "https://your-jellyfin-server.com", - "username": "Username", - "password": "Password", - "add": "Add", - "allFieldsRequired": "All fields are required", - "reset": "Reset", - "imported": "Imported", - "lyricsLines":"lyrics lines for", - "createdAt": "created at", - "matched": "matched", - "notMatched": "not matched", - "deleted": "deleted", - "confirmDelete": "confirm delete", - "thisWillRemoveThemFromYourDevice":"This will remove them from your device.", - "added": "added", - "to":"to", - "unknown":"Unknown", - "noPlaylistsAvailable":"No Playlists available" -} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart deleted file mode 100644 index 1edcb36..0000000 --- a/lib/l10n/app_localizations.dart +++ /dev/null @@ -1,1064 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:intl/intl.dart' as intl; - -import 'app_localizations_en.dart'; -import 'app_localizations_zh.dart'; - -// ignore_for_file: type=lint - -/// Callers can lookup localized strings with an instance of AppLocalizations -/// returned by `AppLocalizations.of(context)`. -/// -/// Applications need to include `AppLocalizations.delegate()` in their app's -/// `localizationDelegates` list, and the locales they support in the app's -/// `supportedLocales` list. For example: -/// -/// ```dart -/// import 'l10n/app_localizations.dart'; -/// -/// return MaterialApp( -/// localizationsDelegates: AppLocalizations.localizationsDelegates, -/// supportedLocales: AppLocalizations.supportedLocales, -/// home: MyApplicationHome(), -/// ); -/// ``` -/// -/// ## Update pubspec.yaml -/// -/// Please make sure to update your pubspec.yaml to include the following -/// packages: -/// -/// ```yaml -/// dependencies: -/// # Internationalization support. -/// flutter_localizations: -/// sdk: flutter -/// intl: any # Use the pinned version from flutter_localizations -/// -/// # Rest of dependencies -/// ``` -/// -/// ## iOS Applications -/// -/// iOS applications define key application metadata, including supported -/// locales, in an Info.plist file that is built into the application bundle. -/// To configure the locales supported by your app, you’ll need to edit this -/// file. -/// -/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. -/// Then, in the Project Navigator, open the Info.plist file under the Runner -/// project’s Runner folder. -/// -/// Next, select the Information Property List item, select Add Item from the -/// Editor menu, then select Localizations from the pop-up menu. -/// -/// Select and expand the newly-created Localizations item then, for each -/// locale your application supports, add a new item and select the locale -/// you wish to add from the pop-up menu in the Value field. This list should -/// be consistent with the languages listed in the AppLocalizations.supportedLocales -/// property. -abstract class AppLocalizations { - AppLocalizations(String locale) - : localeName = intl.Intl.canonicalizedLocale(locale.toString()); - - final String localeName; - - static AppLocalizations? of(BuildContext context) { - return Localizations.of(context, AppLocalizations); - } - - static const LocalizationsDelegate delegate = - _AppLocalizationsDelegate(); - - /// A list of this localizations delegate along with the default localizations - /// delegates. - /// - /// Returns a list of localizations delegates containing this delegate along with - /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, - /// and GlobalWidgetsLocalizations.delegate. - /// - /// Additional delegates can be added by appending to this list in - /// MaterialApp. This list does not have to be used at all if a custom list - /// of delegates is preferred or required. - static const List> localizationsDelegates = - >[ - delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ]; - - /// A list of this localizations delegate's supported locales. - static const List supportedLocales = [ - Locale('en'), - Locale('zh'), - ]; - - /// No description provided for @noMediaSelected. - /// - /// In en, this message translates to: - /// **'No media selected'** - String get noMediaSelected; - - /// No description provided for @noLyricsAvailable. - /// - /// In en, this message translates to: - /// **'No Lyrics Available'** - String get noLyricsAvailable; - - /// No description provided for @fetchLyrics. - /// - /// In en, this message translates to: - /// **'Fetch Lyrics'** - String get fetchLyrics; - - /// No description provided for @searchLyricsWith. - /// - /// In en, this message translates to: - /// **'Search lyrics with {searchTerm}'** - String searchLyricsWith(String searchTerm); - - /// No description provided for @whereToSearchLyrics. - /// - /// In en, this message translates to: - /// **'Where do you want to search lyrics from?'** - String get whereToSearchLyrics; - - /// No description provided for @musixmatch. - /// - /// In en, this message translates to: - /// **'Musixmatch'** - String get musixmatch; - - /// No description provided for @netease. - /// - /// In en, this message translates to: - /// **'NetEase'** - String get netease; - - /// No description provided for @lrclib. - /// - /// In en, this message translates to: - /// **'Lrclib'** - String get lrclib; - - /// No description provided for @manualImport. - /// - /// In en, this message translates to: - /// **'Manual Import'** - String get manualImport; - - /// No description provided for @cancel. - /// - /// In en, this message translates to: - /// **'Cancel'** - String get cancel; - - /// No description provided for @lyricsOptions. - /// - /// In en, this message translates to: - /// **'Lyrics Options'** - String get lyricsOptions; - - /// No description provided for @refetch. - /// - /// In en, this message translates to: - /// **'Re-fetch'** - String get refetch; - - /// No description provided for @clear. - /// - /// In en, this message translates to: - /// **'Clear'** - String get clear; - - /// No description provided for @liveSyncLyrics. - /// - /// In en, this message translates to: - /// **'Live Sync Lyrics'** - String get liveSyncLyrics; - - /// No description provided for @manualOffset. - /// - /// In en, this message translates to: - /// **'Manual Offset'** - String get manualOffset; - - /// No description provided for @adjustLyricsTiming. - /// - /// In en, this message translates to: - /// **'Adjust Lyrics Timing'** - String get adjustLyricsTiming; - - /// No description provided for @enterOffsetMs. - /// - /// In en, this message translates to: - /// **'Enter offset in milliseconds.\nPositive values delay lyrics, negative values advance them.'** - String get enterOffsetMs; - - /// No description provided for @offsetMs. - /// - /// In en, this message translates to: - /// **'Offset (ms)'** - String get offsetMs; - - /// No description provided for @save. - /// - /// In en, this message translates to: - /// **'Save'** - String get save; - - /// No description provided for @liveLyricsSync. - /// - /// In en, this message translates to: - /// **'Live Lyrics Sync'** - String get liveLyricsSync; - - /// No description provided for @offset. - /// - /// In en, this message translates to: - /// **'Offset: {value}ms'** - String offset(int value); - - /// No description provided for @minus100ms. - /// - /// In en, this message translates to: - /// **'-100ms'** - String get minus100ms; - - /// No description provided for @plus10ms. - /// - /// In en, this message translates to: - /// **'+10ms'** - String get plus10ms; - - /// No description provided for @reset. - /// - /// In en, this message translates to: - /// **'Reset'** - String get reset; - - /// No description provided for @plus100ms. - /// - /// In en, this message translates to: - /// **'+100ms'** - String get plus100ms; - - /// No description provided for @fineAdjustment. - /// - /// In en, this message translates to: - /// **'Fine Adjustment'** - String get fineAdjustment; - - /// No description provided for @onlyTimedLyricsCanBeSynced. - /// - /// In en, this message translates to: - /// **'Only timed lyrics can be synced'** - String get onlyTimedLyricsCanBeSynced; - - /// No description provided for @errorLoadingLyrics. - /// - /// In en, this message translates to: - /// **'Error loading lyrics: {error}'** - String errorLoadingLyrics(String error); - - /// No description provided for @errorParsingLyrics. - /// - /// In en, this message translates to: - /// **'Error parsing lyrics: {error}'** - String errorParsingLyrics(String error); - - /// No description provided for @noTracksInQueue. - /// - /// In en, this message translates to: - /// **'No tracks in queue'** - String get noTracksInQueue; - - /// No description provided for @unknownArtist. - /// - /// In en, this message translates to: - /// **'Unknown Artist'** - String get unknownArtist; - - /// No description provided for @showLyrics. - /// - /// In en, this message translates to: - /// **'Show Lyrics'** - String get showLyrics; - - /// No description provided for @showQueue. - /// - /// In en, this message translates to: - /// **'Show Queue'** - String get showQueue; - - /// No description provided for @showCover. - /// - /// In en, this message translates to: - /// **'Show Cover'** - String get showCover; - - /// No description provided for @importedLyricsLines. - /// - /// In en, this message translates to: - /// **'Imported {count} lyrics lines for \"{title}\"'** - String importedLyricsLines(int count, String title); - - /// No description provided for @selected. - /// - /// In en, this message translates to: - /// **'{count} selected'** - String selected(int count); - - /// No description provided for @addToPlaylist. - /// - /// In en, this message translates to: - /// **'Add to Playlist'** - String get addToPlaylist; - - /// No description provided for @delete. - /// - /// In en, this message translates to: - /// **'Delete'** - String get delete; - - /// No description provided for @groovyBox. - /// - /// In en, this message translates to: - /// **'GroovyBox'** - String get groovyBox; - - /// No description provided for @library. - /// - /// In en, this message translates to: - /// **'Library'** - String get library; - - /// No description provided for @importFiles. - /// - /// In en, this message translates to: - /// **'Import Files'** - String get importFiles; - - /// No description provided for @searchTracks. - /// - /// In en, this message translates to: - /// **'Search tracks...'** - String get searchTracks; - - /// No description provided for @searchTracksWithCount. - /// - /// In en, this message translates to: - /// **'Search tracks... ({total} tracks)'** - String searchTracksWithCount(int total); - - /// No description provided for @searchTracksFiltered. - /// - /// In en, this message translates to: - /// **'Search tracks... ({filtered} of {total} tracks)'** - String searchTracksFiltered(int filtered, int total); - - /// No description provided for @error. - /// - /// In en, this message translates to: - /// **'Error: {message}'** - String error(String message); - - /// No description provided for @noTracksYet. - /// - /// In en, this message translates to: - /// **'No tracks yet. Add some!'** - String get noTracksYet; - - /// No description provided for @noTracksMatchSearch. - /// - /// In en, this message translates to: - /// **'No tracks match your search.'** - String get noTracksMatchSearch; - - /// No description provided for @deleteTrack. - /// - /// In en, this message translates to: - /// **'Delete Track?'** - String get deleteTrack; - - /// No description provided for @confirmDeleteTrack. - /// - /// In en, this message translates to: - /// **'Are you sure you want to delete \"{title}\"? This cannot be undone.'** - String confirmDeleteTrack(String title); - - /// No description provided for @deletedTrack. - /// - /// In en, this message translates to: - /// **'Deleted \"{title}\"'** - String deletedTrack(String title); - - /// No description provided for @viewDetails. - /// - /// In en, this message translates to: - /// **'View Details'** - String get viewDetails; - - /// No description provided for @editMetadata. - /// - /// In en, this message translates to: - /// **'Edit Metadata'** - String get editMetadata; - - /// No description provided for @importLyrics. - /// - /// In en, this message translates to: - /// **'Import Lyrics'** - String get importLyrics; - - /// No description provided for @noPlaylistsAvailable. - /// - /// In en, this message translates to: - /// **'No Playlists available'** - String get noPlaylistsAvailable; - - /// No description provided for @addedToPlaylist. - /// - /// In en, this message translates to: - /// **'Added to {name}'** - String addedToPlaylist(String name); - - /// No description provided for @trackDetails. - /// - /// In en, this message translates to: - /// **'Track Details'** - String get trackDetails; - - /// No description provided for @close. - /// - /// In en, this message translates to: - /// **'Close'** - String get close; - - /// No description provided for @title. - /// - /// In en, this message translates to: - /// **'Title'** - String get title; - - /// No description provided for @artist. - /// - /// In en, this message translates to: - /// **'Artist'** - String get artist; - - /// No description provided for @album. - /// - /// In en, this message translates to: - /// **'Album'** - String get album; - - /// No description provided for @duration. - /// - /// In en, this message translates to: - /// **'Duration'** - String get duration; - - /// No description provided for @fileSize. - /// - /// In en, this message translates to: - /// **'File Size'** - String get fileSize; - - /// No description provided for @filePath. - /// - /// In en, this message translates to: - /// **'File Path'** - String get filePath; - - /// No description provided for @dateAdded. - /// - /// In en, this message translates to: - /// **'Date Added'** - String get dateAdded; - - /// No description provided for @albumArt. - /// - /// In en, this message translates to: - /// **'Album Art'** - String get albumArt; - - /// No description provided for @present. - /// - /// In en, this message translates to: - /// **'Present'** - String get present; - - /// No description provided for @editTrack. - /// - /// In en, this message translates to: - /// **'Edit Track'** - String get editTrack; - - /// No description provided for @addedTracksToPlaylist. - /// - /// In en, this message translates to: - /// **'Added {count} tracks to {name}'** - String addedTracksToPlaylist(int count, String name); - - /// No description provided for @deleteTracks. - /// - /// In en, this message translates to: - /// **'Delete Tracks?'** - String get deleteTracks; - - /// No description provided for @confirmDeleteTracks. - /// - /// In en, this message translates to: - /// **'Are you sure you want to delete {count} tracks? This will remove them from your device.'** - String confirmDeleteTracks(int count); - - /// No description provided for @deletedTracks. - /// - /// In en, this message translates to: - /// **'Deleted {count} tracks'** - String deletedTracks(int count); - - /// No description provided for @batchImportComplete. - /// - /// In en, this message translates to: - /// **'Batch import complete: {matched} matched, {notMatched} not matched'** - String batchImportComplete(int matched, int notMatched); - - /// No description provided for @settings. - /// - /// In en, this message translates to: - /// **'Settings'** - String get settings; - - /// No description provided for @autoScan. - /// - /// In en, this message translates to: - /// **'Auto Scan'** - String get autoScan; - - /// No description provided for @autoScanMusicLibraries. - /// - /// In en, this message translates to: - /// **'Auto-scan music libraries'** - String get autoScanMusicLibraries; - - /// No description provided for @autoScanDescription. - /// - /// In en, this message translates to: - /// **'Automatically scan music libraries for new music files'** - String get autoScanDescription; - - /// No description provided for @watchForChanges. - /// - /// In en, this message translates to: - /// **'Watch for changes'** - String get watchForChanges; - - /// No description provided for @watchForChangesDescription. - /// - /// In en, this message translates to: - /// **'Monitor music libraries for file changes'** - String get watchForChangesDescription; - - /// No description provided for @musicLibraries. - /// - /// In en, this message translates to: - /// **'Music Libraries'** - String get musicLibraries; - - /// No description provided for @scanLibraries. - /// - /// In en, this message translates to: - /// **'Scan Libraries'** - String get scanLibraries; - - /// No description provided for @addMusicLibrary. - /// - /// In en, this message translates to: - /// **'Add Music Library'** - String get addMusicLibrary; - - /// No description provided for @addMusicLibraryDescription. - /// - /// In en, this message translates to: - /// **'Add folder libraries to index music files. Files will be copied to internal storage for playback.'** - String get addMusicLibraryDescription; - - /// No description provided for @noMusicLibrariesAdded. - /// - /// In en, this message translates to: - /// **'No music libraries added yet.'** - String get noMusicLibrariesAdded; - - /// No description provided for @errorLoadingLibraries. - /// - /// In en, this message translates to: - /// **'Error loading libraries: {error}'** - String errorLoadingLibraries(String error); - - /// No description provided for @remoteProviders. - /// - /// In en, this message translates to: - /// **'Remote Providers'** - String get remoteProviders; - - /// No description provided for @indexRemoteProviders. - /// - /// In en, this message translates to: - /// **'Index Remote Providers'** - String get indexRemoteProviders; - - /// No description provided for @addRemoteProvider. - /// - /// In en, this message translates to: - /// **'Add Remote Provider'** - String get addRemoteProvider; - - /// No description provided for @remoteProvidersDescription. - /// - /// In en, this message translates to: - /// **'Connect to remote media servers like Jellyfin to access your music library.'** - String get remoteProvidersDescription; - - /// No description provided for @noRemoteProvidersAdded. - /// - /// In en, this message translates to: - /// **'No remote providers added yet.'** - String get noRemoteProvidersAdded; - - /// No description provided for @errorLoadingProviders. - /// - /// In en, this message translates to: - /// **'Error loading providers: {error}'** - String errorLoadingProviders(String error); - - /// No description provided for @playerSettings. - /// - /// In en, this message translates to: - /// **'Player Settings'** - String get playerSettings; - - /// No description provided for @playerSettingsDescription. - /// - /// In en, this message translates to: - /// **'Configure player behavior and display options.'** - String get playerSettingsDescription; - - /// No description provided for @defaultPlayerScreen. - /// - /// In en, this message translates to: - /// **'Default Player Screen'** - String get defaultPlayerScreen; - - /// No description provided for @defaultPlayerScreenDescription. - /// - /// In en, this message translates to: - /// **'Choose which screen to show when opening the player.'** - String get defaultPlayerScreenDescription; - - /// No description provided for @lyricsMode. - /// - /// In en, this message translates to: - /// **'Lyrics Mode'** - String get lyricsMode; - - /// No description provided for @lyricsModeDescription. - /// - /// In en, this message translates to: - /// **'Choose how lyrics are displayed.'** - String get lyricsModeDescription; - - /// No description provided for @continuePlaying. - /// - /// In en, this message translates to: - /// **'Continue Playing'** - String get continuePlaying; - - /// No description provided for @continuePlayingDescription. - /// - /// In en, this message translates to: - /// **'Continue playing music after the queue is empty'** - String get continuePlayingDescription; - - /// No description provided for @databaseManagement. - /// - /// In en, this message translates to: - /// **'Database Management'** - String get databaseManagement; - - /// No description provided for @databaseManagementDescription. - /// - /// In en, this message translates to: - /// **'Manage your music database and cached files.'** - String get databaseManagementDescription; - - /// No description provided for @resetTrackDatabase. - /// - /// In en, this message translates to: - /// **'Reset Track Database'** - String get resetTrackDatabase; - - /// No description provided for @resetTrackDatabaseDescription. - /// - /// In en, this message translates to: - /// **'Remove all tracks from database and delete cached files. This action cannot be undone.'** - String get resetTrackDatabaseDescription; - - /// No description provided for @errorLoadingSettings. - /// - /// In en, this message translates to: - /// **'Error loading settings: {error}'** - String errorLoadingSettings(String error); - - /// No description provided for @addedMusicLibrary. - /// - /// In en, this message translates to: - /// **'Added music library: {path}'** - String addedMusicLibrary(String path); - - /// No description provided for @errorAddingLibrary. - /// - /// In en, this message translates to: - /// **'Error adding library: {error}'** - String errorAddingLibrary(String error); - - /// No description provided for @librariesScannedSuccessfully. - /// - /// In en, this message translates to: - /// **'Libraries scanned successfully'** - String get librariesScannedSuccessfully; - - /// No description provided for @errorScanningLibraries. - /// - /// In en, this message translates to: - /// **'Error scanning libraries: {error}'** - String errorScanningLibraries(String error); - - /// No description provided for @noActiveRemoteProviders. - /// - /// In en, this message translates to: - /// **'No active remote providers to index'** - String get noActiveRemoteProviders; - - /// No description provided for @indexedRemoteProviders. - /// - /// In en, this message translates to: - /// **'Indexed {count} remote provider(s)'** - String indexedRemoteProviders(int count); - - /// No description provided for @errorIndexingRemoteProviders. - /// - /// In en, this message translates to: - /// **'Error indexing remote providers: {error}'** - String errorIndexingRemoteProviders(String error); - - /// No description provided for @serverUrl. - /// - /// In en, this message translates to: - /// **'Server URL'** - String get serverUrl; - - /// No description provided for @serverUrlHint. - /// - /// In en, this message translates to: - /// **'https://your-jellyfin-server.com'** - String get serverUrlHint; - - /// No description provided for @username. - /// - /// In en, this message translates to: - /// **'Username'** - String get username; - - /// No description provided for @password. - /// - /// In en, this message translates to: - /// **'Password'** - String get password; - - /// No description provided for @add. - /// - /// In en, this message translates to: - /// **'Add'** - String get add; - - /// No description provided for @allFieldsRequired. - /// - /// In en, this message translates to: - /// **'All fields are required'** - String get allFieldsRequired; - - /// No description provided for @addedRemoteProvider. - /// - /// In en, this message translates to: - /// **'Added remote provider: {url}'** - String addedRemoteProvider(String url); - - /// No description provided for @errorAddingProvider. - /// - /// In en, this message translates to: - /// **'Error adding provider: {error}'** - String errorAddingProvider(String error); - - /// No description provided for @confirmResetTrackDatabase. - /// - /// In en, this message translates to: - /// **'This will permanently delete all tracks from the database and remove all cached music files and album art. This action cannot be undone.\n\nAre you sure you want to continue?'** - String get confirmResetTrackDatabase; - - /// No description provided for @trackDatabaseReset. - /// - /// In en, this message translates to: - /// **'Track database has been reset'** - String get trackDatabaseReset; - - /// No description provided for @errorResettingDatabase. - /// - /// In en, this message translates to: - /// **'Error resetting database: {error}'** - String errorResettingDatabase(String error); - - /// No description provided for @noTracksInAlbum. - /// - /// In en, this message translates to: - /// **'No tracks in this album'** - String get noTracksInAlbum; - - /// No description provided for @playAll. - /// - /// In en, this message translates to: - /// **'Play All'** - String get playAll; - - /// No description provided for @addToQueue. - /// - /// In en, this message translates to: - /// **'Add to Queue'** - String get addToQueue; - - /// No description provided for @noTracksInPlaylist. - /// - /// In en, this message translates to: - /// **'No tracks in this playlist'** - String get noTracksInPlaylist; - - /// No description provided for @noAlbumsFound. - /// - /// In en, this message translates to: - /// **'No albums found'** - String get noAlbumsFound; - - /// No description provided for @createOne. - /// - /// In en, this message translates to: - /// **'Create One'** - String get createOne; - - /// No description provided for @addNewPlaylist. - /// - /// In en, this message translates to: - /// **'Add a new playlist'** - String get addNewPlaylist; - - /// No description provided for @newPlaylist. - /// - /// In en, this message translates to: - /// **'New Playlist'** - String get newPlaylist; - - /// No description provided for @playlistName. - /// - /// In en, this message translates to: - /// **'Playlist Name'** - String get playlistName; - - /// No description provided for @create. - /// - /// In en, this message translates to: - /// **'Create'** - String get create; - - /// No description provided for @noPlaylistsYet. - /// - /// In en, this message translates to: - /// **'No playlists yet'** - String get noPlaylistsYet; - - /// No description provided for @queue. - /// - /// In en, this message translates to: - /// **'Queue'** - String get queue; - - /// No description provided for @appSettings. - /// - /// In en, this message translates to: - /// **'App Settings'** - String get appSettings; - - /// No description provided for @appSettingsDescription. - /// - /// In en, this message translates to: - /// **'Configure app-wide settings and preferences.'** - String get appSettingsDescription; - - /// No description provided for @language. - /// - /// In en, this message translates to: - /// **'Language'** - String get language; - - /// No description provided for @languageDescription. - /// - /// In en, this message translates to: - /// **'Choose the app language.'** - String get languageDescription; - - /// No description provided for @english. - /// - /// In en, this message translates to: - /// **'English'** - String get english; - - /// No description provided for @chinese. - /// - /// In en, this message translates to: - /// **'中文'** - String get chinese; - - /// No description provided for @settingsTitle. - /// - /// In en, this message translates to: - /// **'Settings'** - String get settingsTitle; - - /// No description provided for @tracks. - /// - /// In en, this message translates to: - /// **'Tracks'** - String get tracks; - - /// No description provided for @albums. - /// - /// In en, this message translates to: - /// **'Albums'** - String get albums; - - /// No description provided for @playlists. - /// - /// In en, this message translates to: - /// **'Playlists'** - String get playlists; - - /// No description provided for @addRemoteProviderDialog. - /// - /// In en, this message translates to: - /// **'Add Remote Provider'** - String get addRemoteProviderDialog; - - /// No description provided for @imported. - /// - /// In en, this message translates to: - /// **'Imported'** - String get imported; - - /// No description provided for @lyricsLines. - /// - /// In en, this message translates to: - /// **'lyrics lines for'** - String get lyricsLines; - - /// No description provided for @createdAt. - /// - /// In en, this message translates to: - /// **'created at'** - String get createdAt; - - /// No description provided for @matched. - /// - /// In en, this message translates to: - /// **'matched'** - String get matched; - - /// No description provided for @notMatched. - /// - /// In en, this message translates to: - /// **'not matched'** - String get notMatched; - - /// No description provided for @deleted. - /// - /// In en, this message translates to: - /// **'deleted'** - String get deleted; - - /// No description provided for @confirmDelete. - /// - /// In en, this message translates to: - /// **'confirm delete'** - String get confirmDelete; - - /// No description provided for @thisWillRemoveThemFromYourDevice. - /// - /// In en, this message translates to: - /// **'This will remove them from your device.'** - String get thisWillRemoveThemFromYourDevice; - - /// No description provided for @added. - /// - /// In en, this message translates to: - /// **'added'** - String get added; - - /// No description provided for @to. - /// - /// In en, this message translates to: - /// **'to'** - String get to; - - /// No description provided for @unknown. - /// - /// In en, this message translates to: - /// **'Unknown'** - String get unknown; -} - -class _AppLocalizationsDelegate - extends LocalizationsDelegate { - const _AppLocalizationsDelegate(); - - @override - Future load(Locale locale) { - return SynchronousFuture(lookupAppLocalizations(locale)); - } - - @override - bool isSupported(Locale locale) => - ['en', 'zh'].contains(locale.languageCode); - - @override - bool shouldReload(_AppLocalizationsDelegate old) => false; -} - -AppLocalizations lookupAppLocalizations(Locale locale) { - // Lookup logic when only language code is specified. - switch (locale.languageCode) { - case 'en': - return AppLocalizationsEn(); - case 'zh': - return AppLocalizationsZh(); - } - - throw FlutterError( - 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' - 'an issue with the localizations generation tool. Please file an issue ' - 'on GitHub with a reproducible sample app and the gen-l10n configuration ' - 'that was used.', - ); -} diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart deleted file mode 100644 index 5608739..0000000 --- a/lib/l10n/app_localizations_en.dart +++ /dev/null @@ -1,542 +0,0 @@ -// ignore: unused_import -import 'package:intl/intl.dart' as intl; -import 'app_localizations.dart'; - -// ignore_for_file: type=lint - -/// The translations for English (`en`). -class AppLocalizationsEn extends AppLocalizations { - AppLocalizationsEn([String locale = 'en']) : super(locale); - - @override - String get noMediaSelected => 'No media selected'; - - @override - String get noLyricsAvailable => 'No Lyrics Available'; - - @override - String get fetchLyrics => 'Fetch Lyrics'; - - @override - String searchLyricsWith(String searchTerm) { - return 'Search lyrics with $searchTerm'; - } - - @override - String get whereToSearchLyrics => 'Where do you want to search lyrics from?'; - - @override - String get musixmatch => 'Musixmatch'; - - @override - String get netease => 'NetEase'; - - @override - String get lrclib => 'Lrclib'; - - @override - String get manualImport => 'Manual Import'; - - @override - String get cancel => 'Cancel'; - - @override - String get lyricsOptions => 'Lyrics Options'; - - @override - String get refetch => 'Re-fetch'; - - @override - String get clear => 'Clear'; - - @override - String get liveSyncLyrics => 'Live Sync Lyrics'; - - @override - String get manualOffset => 'Manual Offset'; - - @override - String get adjustLyricsTiming => 'Adjust Lyrics Timing'; - - @override - String get enterOffsetMs => - 'Enter offset in milliseconds.\nPositive values delay lyrics, negative values advance them.'; - - @override - String get offsetMs => 'Offset (ms)'; - - @override - String get save => 'Save'; - - @override - String get liveLyricsSync => 'Live Lyrics Sync'; - - @override - String offset(int value) { - return 'Offset: ${value}ms'; - } - - @override - String get minus100ms => '-100ms'; - - @override - String get plus10ms => '+10ms'; - - @override - String get reset => 'Reset'; - - @override - String get plus100ms => '+100ms'; - - @override - String get fineAdjustment => 'Fine Adjustment'; - - @override - String get onlyTimedLyricsCanBeSynced => 'Only timed lyrics can be synced'; - - @override - String errorLoadingLyrics(String error) { - return 'Error loading lyrics: $error'; - } - - @override - String errorParsingLyrics(String error) { - return 'Error parsing lyrics: $error'; - } - - @override - String get noTracksInQueue => 'No tracks in queue'; - - @override - String get unknownArtist => 'Unknown Artist'; - - @override - String get showLyrics => 'Show Lyrics'; - - @override - String get showQueue => 'Show Queue'; - - @override - String get showCover => 'Show Cover'; - - @override - String importedLyricsLines(int count, String title) { - return 'Imported $count lyrics lines for \"$title\"'; - } - - @override - String selected(int count) { - return '$count selected'; - } - - @override - String get addToPlaylist => 'Add to Playlist'; - - @override - String get delete => 'Delete'; - - @override - String get groovyBox => 'GroovyBox'; - - @override - String get library => 'Library'; - - @override - String get importFiles => 'Import Files'; - - @override - String get searchTracks => 'Search tracks...'; - - @override - String searchTracksWithCount(int total) { - return 'Search tracks... ($total tracks)'; - } - - @override - String searchTracksFiltered(int filtered, int total) { - return 'Search tracks... ($filtered of $total tracks)'; - } - - @override - String error(String message) { - return 'Error: $message'; - } - - @override - String get noTracksYet => 'No tracks yet. Add some!'; - - @override - String get noTracksMatchSearch => 'No tracks match your search.'; - - @override - String get deleteTrack => 'Delete Track?'; - - @override - String confirmDeleteTrack(String title) { - return 'Are you sure you want to delete \"$title\"? This cannot be undone.'; - } - - @override - String deletedTrack(String title) { - return 'Deleted \"$title\"'; - } - - @override - String get viewDetails => 'View Details'; - - @override - String get editMetadata => 'Edit Metadata'; - - @override - String get importLyrics => 'Import Lyrics'; - - @override - String get noPlaylistsAvailable => 'No Playlists available'; - - @override - String addedToPlaylist(String name) { - return 'Added to $name'; - } - - @override - String get trackDetails => 'Track Details'; - - @override - String get close => 'Close'; - - @override - String get title => 'Title'; - - @override - String get artist => 'Artist'; - - @override - String get album => 'Album'; - - @override - String get duration => 'Duration'; - - @override - String get fileSize => 'File Size'; - - @override - String get filePath => 'File Path'; - - @override - String get dateAdded => 'Date Added'; - - @override - String get albumArt => 'Album Art'; - - @override - String get present => 'Present'; - - @override - String get editTrack => 'Edit Track'; - - @override - String addedTracksToPlaylist(int count, String name) { - return 'Added $count tracks to $name'; - } - - @override - String get deleteTracks => 'Delete Tracks?'; - - @override - String confirmDeleteTracks(int count) { - return 'Are you sure you want to delete $count tracks? This will remove them from your device.'; - } - - @override - String deletedTracks(int count) { - return 'Deleted $count tracks'; - } - - @override - String batchImportComplete(int matched, int notMatched) { - return 'Batch import complete: $matched matched, $notMatched not matched'; - } - - @override - String get settings => 'Settings'; - - @override - String get autoScan => 'Auto Scan'; - - @override - String get autoScanMusicLibraries => 'Auto-scan music libraries'; - - @override - String get autoScanDescription => - 'Automatically scan music libraries for new music files'; - - @override - String get watchForChanges => 'Watch for changes'; - - @override - String get watchForChangesDescription => - 'Monitor music libraries for file changes'; - - @override - String get musicLibraries => 'Music Libraries'; - - @override - String get scanLibraries => 'Scan Libraries'; - - @override - String get addMusicLibrary => 'Add Music Library'; - - @override - String get addMusicLibraryDescription => - 'Add folder libraries to index music files. Files will be copied to internal storage for playback.'; - - @override - String get noMusicLibrariesAdded => 'No music libraries added yet.'; - - @override - String errorLoadingLibraries(String error) { - return 'Error loading libraries: $error'; - } - - @override - String get remoteProviders => 'Remote Providers'; - - @override - String get indexRemoteProviders => 'Index Remote Providers'; - - @override - String get addRemoteProvider => 'Add Remote Provider'; - - @override - String get remoteProvidersDescription => - 'Connect to remote media servers like Jellyfin to access your music library.'; - - @override - String get noRemoteProvidersAdded => 'No remote providers added yet.'; - - @override - String errorLoadingProviders(String error) { - return 'Error loading providers: $error'; - } - - @override - String get playerSettings => 'Player Settings'; - - @override - String get playerSettingsDescription => - 'Configure player behavior and display options.'; - - @override - String get defaultPlayerScreen => 'Default Player Screen'; - - @override - String get defaultPlayerScreenDescription => - 'Choose which screen to show when opening the player.'; - - @override - String get lyricsMode => 'Lyrics Mode'; - - @override - String get lyricsModeDescription => 'Choose how lyrics are displayed.'; - - @override - String get continuePlaying => 'Continue Playing'; - - @override - String get continuePlayingDescription => - 'Continue playing music after the queue is empty'; - - @override - String get databaseManagement => 'Database Management'; - - @override - String get databaseManagementDescription => - 'Manage your music database and cached files.'; - - @override - String get resetTrackDatabase => 'Reset Track Database'; - - @override - String get resetTrackDatabaseDescription => - 'Remove all tracks from database and delete cached files. This action cannot be undone.'; - - @override - String errorLoadingSettings(String error) { - return 'Error loading settings: $error'; - } - - @override - String addedMusicLibrary(String path) { - return 'Added music library: $path'; - } - - @override - String errorAddingLibrary(String error) { - return 'Error adding library: $error'; - } - - @override - String get librariesScannedSuccessfully => 'Libraries scanned successfully'; - - @override - String errorScanningLibraries(String error) { - return 'Error scanning libraries: $error'; - } - - @override - String get noActiveRemoteProviders => 'No active remote providers to index'; - - @override - String indexedRemoteProviders(int count) { - return 'Indexed $count remote provider(s)'; - } - - @override - String errorIndexingRemoteProviders(String error) { - return 'Error indexing remote providers: $error'; - } - - @override - String get serverUrl => 'Server URL'; - - @override - String get serverUrlHint => 'https://your-jellyfin-server.com'; - - @override - String get username => 'Username'; - - @override - String get password => 'Password'; - - @override - String get add => 'Add'; - - @override - String get allFieldsRequired => 'All fields are required'; - - @override - String addedRemoteProvider(String url) { - return 'Added remote provider: $url'; - } - - @override - String errorAddingProvider(String error) { - return 'Error adding provider: $error'; - } - - @override - String get confirmResetTrackDatabase => - 'This will permanently delete all tracks from the database and remove all cached music files and album art. This action cannot be undone.\n\nAre you sure you want to continue?'; - - @override - String get trackDatabaseReset => 'Track database has been reset'; - - @override - String errorResettingDatabase(String error) { - return 'Error resetting database: $error'; - } - - @override - String get noTracksInAlbum => 'No tracks in this album'; - - @override - String get playAll => 'Play All'; - - @override - String get addToQueue => 'Add to Queue'; - - @override - String get noTracksInPlaylist => 'No tracks in this playlist'; - - @override - String get noAlbumsFound => 'No albums found'; - - @override - String get createOne => 'Create One'; - - @override - String get addNewPlaylist => 'Add a new playlist'; - - @override - String get newPlaylist => 'New Playlist'; - - @override - String get playlistName => 'Playlist Name'; - - @override - String get create => 'Create'; - - @override - String get noPlaylistsYet => 'No playlists yet'; - - @override - String get queue => 'Queue'; - - @override - String get appSettings => 'App Settings'; - - @override - String get appSettingsDescription => - 'Configure app-wide settings and preferences.'; - - @override - String get language => 'Language'; - - @override - String get languageDescription => 'Choose the app language.'; - - @override - String get english => 'English'; - - @override - String get chinese => '中文'; - - @override - String get settingsTitle => 'Settings'; - - @override - String get tracks => 'Tracks'; - - @override - String get albums => 'Albums'; - - @override - String get playlists => 'Playlists'; - - @override - String get addRemoteProviderDialog => 'Add Remote Provider'; - - @override - String get imported => 'Imported'; - - @override - String get lyricsLines => 'lyrics lines for'; - - @override - String get createdAt => 'created at'; - - @override - String get matched => 'matched'; - - @override - String get notMatched => 'not matched'; - - @override - String get deleted => 'deleted'; - - @override - String get confirmDelete => 'confirm delete'; - - @override - String get thisWillRemoveThemFromYourDevice => - 'This will remove them from your device.'; - - @override - String get added => 'added'; - - @override - String get to => 'to'; - - @override - String get unknown => 'Unknown'; -} diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart deleted file mode 100644 index 1530cf6..0000000 --- a/lib/l10n/app_localizations_zh.dart +++ /dev/null @@ -1,530 +0,0 @@ -// ignore: unused_import -import 'package:intl/intl.dart' as intl; -import 'app_localizations.dart'; - -// ignore_for_file: type=lint - -/// The translations for Chinese (`zh`). -class AppLocalizationsZh extends AppLocalizations { - AppLocalizationsZh([String locale = 'zh']) : super(locale); - - @override - String get noMediaSelected => '未选择媒体'; - - @override - String get noLyricsAvailable => '无歌词可用'; - - @override - String get fetchLyrics => '获取歌词'; - - @override - String searchLyricsWith(String searchTerm) { - return '使用 $searchTerm 搜索歌词'; - } - - @override - String get whereToSearchLyrics => '您想从哪里搜索歌词?'; - - @override - String get musixmatch => 'Musixmatch'; - - @override - String get netease => '网易云音乐'; - - @override - String get lrclib => 'Lrclib'; - - @override - String get manualImport => '手动导入'; - - @override - String get cancel => '取消'; - - @override - String get lyricsOptions => '歌词选项'; - - @override - String get refetch => '重新获取'; - - @override - String get clear => '清除'; - - @override - String get liveSyncLyrics => '实时同步歌词'; - - @override - String get manualOffset => '手动偏移'; - - @override - String get adjustLyricsTiming => '调整歌词时间'; - - @override - String get enterOffsetMs => '输入偏移量(毫秒)。\n正值延迟歌词显示,负值提前显示。'; - - @override - String get offsetMs => '偏移量(毫秒)'; - - @override - String get save => '保存'; - - @override - String get liveLyricsSync => '实时歌词同步'; - - @override - String offset(int value) { - return '偏移量:$value毫秒'; - } - - @override - String get minus100ms => '-100毫秒'; - - @override - String get plus10ms => '+10毫秒'; - - @override - String get reset => '重置'; - - @override - String get plus100ms => '+100毫秒'; - - @override - String get fineAdjustment => '精细调整'; - - @override - String get onlyTimedLyricsCanBeSynced => '只有带时间戳的歌词才能同步'; - - @override - String errorLoadingLyrics(String error) { - return '加载歌词时出错:$error'; - } - - @override - String errorParsingLyrics(String error) { - return '解析歌词时出错:$error'; - } - - @override - String get noTracksInQueue => '队列中没有曲目'; - - @override - String get unknownArtist => '未知艺术家'; - - @override - String get showLyrics => '显示歌词'; - - @override - String get showQueue => '显示队列'; - - @override - String get showCover => '显示封面'; - - @override - String importedLyricsLines(int count, String title) { - return '为\"$title\"导入了 $count 行歌词'; - } - - @override - String selected(int count) { - return '已选择 $count 项'; - } - - @override - String get addToPlaylist => '添加到播放列表'; - - @override - String get delete => '删除'; - - @override - String get groovyBox => 'GroovyBox'; - - @override - String get library => '音乐库'; - - @override - String get importFiles => '导入文件'; - - @override - String get searchTracks => '搜索曲目...'; - - @override - String searchTracksWithCount(int total) { - return '搜索曲目...(共 $total 首)'; - } - - @override - String searchTracksFiltered(int filtered, int total) { - return '搜索曲目...($filtered / $total 首)'; - } - - @override - String error(String message) { - return '错误:$message'; - } - - @override - String get noTracksYet => '还没有曲目,请添加一些!'; - - @override - String get noTracksMatchSearch => '没有匹配搜索的曲目。'; - - @override - String get deleteTrack => '删除曲目?'; - - @override - String confirmDeleteTrack(String title) { - return '您确定要删除\"$title\"吗?此操作无法撤销。'; - } - - @override - String deletedTrack(String title) { - return '已删除\"$title\"'; - } - - @override - String get viewDetails => '查看详情'; - - @override - String get editMetadata => '编辑元数据'; - - @override - String get importLyrics => '导入歌词'; - - @override - String get noPlaylistsAvailable => '没有可用的播放列表,请先创建一个!'; - - @override - String addedToPlaylist(String name) { - return '已添加到 $name'; - } - - @override - String get trackDetails => '曲目详情'; - - @override - String get close => '关闭'; - - @override - String get title => '标题'; - - @override - String get artist => '艺术家'; - - @override - String get album => '专辑'; - - @override - String get duration => '时长'; - - @override - String get fileSize => '文件大小'; - - @override - String get filePath => '文件路径'; - - @override - String get dateAdded => '添加日期'; - - @override - String get albumArt => '专辑封面'; - - @override - String get present => '存在'; - - @override - String get editTrack => '编辑曲目'; - - @override - String addedTracksToPlaylist(int count, String name) { - return '已将 $count 首曲目添加到 $name'; - } - - @override - String get deleteTracks => '删除曲目?'; - - @override - String confirmDeleteTracks(int count) { - return '您确定要删除 $count 首曲目吗?这将从您的设备中移除它们。'; - } - - @override - String deletedTracks(int count) { - return '已删除 $count 首曲目'; - } - - @override - String batchImportComplete(int matched, int notMatched) { - return '批量导入完成:$matched 匹配,$notMatched 不匹配'; - } - - @override - String get settings => '设置'; - - @override - String get autoScan => '自动扫描'; - - @override - String get autoScanMusicLibraries => '自动扫描音乐库'; - - @override - String get autoScanDescription => '自动扫描音乐库中的新音乐文件'; - - @override - String get watchForChanges => '监视更改'; - - @override - String get watchForChangesDescription => '监视音乐库的文件更改'; - - @override - String get musicLibraries => '音乐库'; - - @override - String get scanLibraries => '扫描库'; - - @override - String get addMusicLibrary => '添加音乐库'; - - @override - String get addMusicLibraryDescription => '添加文件夹库来索引音乐文件。文件将被复制到内部存储以供播放。'; - - @override - String get noMusicLibrariesAdded => '尚未添加音乐库。'; - - @override - String errorLoadingLibraries(String error) { - return '加载库时出错:$error'; - } - - @override - String get remoteProviders => '远程提供商'; - - @override - String get indexRemoteProviders => '索引远程提供商'; - - @override - String get addRemoteProvider => '添加远程提供商'; - - @override - String get remoteProvidersDescription => '连接到远程媒体服务器,如Jellyfin,来访问您的音乐库。'; - - @override - String get noRemoteProvidersAdded => '尚未添加远程提供商。'; - - @override - String errorLoadingProviders(String error) { - return '加载提供商时出错:$error'; - } - - @override - String get playerSettings => '播放器设置'; - - @override - String get playerSettingsDescription => '配置播放器行为和显示选项。'; - - @override - String get defaultPlayerScreen => '默认播放器屏幕'; - - @override - String get defaultPlayerScreenDescription => '选择打开播放器时显示的屏幕。'; - - @override - String get lyricsMode => '歌词模式'; - - @override - String get lyricsModeDescription => '选择歌词的显示方式。'; - - @override - String get continuePlaying => '继续播放'; - - @override - String get continuePlayingDescription => '队列为空后继续播放音乐'; - - @override - String get databaseManagement => '数据库管理'; - - @override - String get databaseManagementDescription => '管理您的音乐数据库和缓存文件。'; - - @override - String get resetTrackDatabase => '重置曲目数据库'; - - @override - String get resetTrackDatabaseDescription => '从数据库中移除所有曲目并删除缓存文件。此操作无法撤销。'; - - @override - String errorLoadingSettings(String error) { - return '加载设置时出错:$error'; - } - - @override - String addedMusicLibrary(String path) { - return '已添加音乐库:$path'; - } - - @override - String errorAddingLibrary(String error) { - return '添加库时出错:$error'; - } - - @override - String get librariesScannedSuccessfully => '库扫描成功'; - - @override - String errorScanningLibraries(String error) { - return '扫描库时出错:$error'; - } - - @override - String get noActiveRemoteProviders => '没有活动的远程提供商可索引'; - - @override - String indexedRemoteProviders(int count) { - return '已索引 $count 个远程提供商'; - } - - @override - String errorIndexingRemoteProviders(String error) { - return '索引远程提供商时出错:$error'; - } - - @override - String get serverUrl => '服务器URL'; - - @override - String get serverUrlHint => 'https://your-jellyfin-server.com'; - - @override - String get username => '用户名'; - - @override - String get password => '密码'; - - @override - String get add => '添加'; - - @override - String get allFieldsRequired => '所有字段都是必填的'; - - @override - String addedRemoteProvider(String url) { - return '已添加远程提供商:$url'; - } - - @override - String errorAddingProvider(String error) { - return '添加提供商时出错:$error'; - } - - @override - String get confirmResetTrackDatabase => - '这将永久删除数据库中的所有曲目,并移除所有缓存的音乐文件和专辑封面。此操作无法撤销。\n\n您确定要继续吗?'; - - @override - String get trackDatabaseReset => '曲目数据库已重置'; - - @override - String errorResettingDatabase(String error) { - return '重置数据库时出错:$error'; - } - - @override - String get noTracksInAlbum => '此专辑中没有曲目'; - - @override - String get playAll => '播放全部'; - - @override - String get addToQueue => '添加到队列'; - - @override - String get noTracksInPlaylist => '此播放列表中没有曲目'; - - @override - String get noAlbumsFound => '未找到专辑'; - - @override - String get createOne => '创建一个'; - - @override - String get addNewPlaylist => '添加新播放列表'; - - @override - String get newPlaylist => '新播放列表'; - - @override - String get playlistName => '播放列表名称'; - - @override - String get create => '创建'; - - @override - String get noPlaylistsYet => '还没有播放列表'; - - @override - String get queue => '队列'; - - @override - String get appSettings => '应用设置'; - - @override - String get appSettingsDescription => '配置应用范围的设置和偏好。'; - - @override - String get language => '语言'; - - @override - String get languageDescription => '选择应用语言。'; - - @override - String get english => 'English'; - - @override - String get chinese => '中文'; - - @override - String get settingsTitle => '设置'; - - @override - String get tracks => '曲目'; - - @override - String get albums => '专辑'; - - @override - String get playlists => '播放列表'; - - @override - String get addRemoteProviderDialog => '添加远程提供商'; - - @override - String get imported => '已导入'; - - @override - String get lyricsLines => '歌词'; - - @override - String get createdAt => '创建于'; - - @override - String get matched => '匹配的'; - - @override - String get notMatched => '未匹配的'; - - @override - String get deleted => '已删除'; - - @override - String get confirmDelete => '确认删除'; - - @override - String get thisWillRemoveThemFromYourDevice => '这将从您的设备上删除。'; - - @override - String get added => '已添加'; - - @override - String get to => '至'; - - @override - String get unknown => '未知'; -} diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb deleted file mode 100644 index e231ffe..0000000 --- a/lib/l10n/app_zh.arb +++ /dev/null @@ -1,404 +0,0 @@ -{ - "noMediaSelected": "未选择媒体", - "noLyricsAvailable": "无歌词可用", - "fetchLyrics": "获取歌词", - "searchLyricsWith": "使用 {searchTerm} 搜索歌词", - "@searchLyricsWith": { - "placeholders": { - "searchTerm": { - "type": "String" - } - } - }, - "whereToSearchLyrics": "您想从哪里搜索歌词?", - "musixmatch": "Musixmatch", - "netease": "网易云音乐", - "lrclib": "Lrclib", - "manualImport": "手动导入", - "cancel": "取消", - "lyricsOptions": "歌词选项", - "refetch": "重新获取", - "clear": "清除", - "liveSyncLyrics": "实时同步歌词", - "manualOffset": "手动偏移", - "adjustLyricsTiming": "调整歌词时间", - "enterOffsetMs": "输入偏移量(毫秒)。\n正值延迟歌词显示,负值提前显示。", - "offsetMs": "偏移量(毫秒)", - "save": "保存", - "liveLyricsSync": "实时歌词同步", - "offset": "偏移量:{value}毫秒", - "@offset": { - "placeholders": { - "value": { - "type": "int" - } - } - }, - "minus100ms": "-100毫秒", - "plus10ms": "+10毫秒", - "reset": "重置", - "plus100ms": "+100毫秒", - "fineAdjustment": "精细调整", - "onlyTimedLyricsCanBeSynced": "只有带时间戳的歌词才能同步", - "errorLoadingLyrics": "加载歌词时出错:{error}", - "@errorLoadingLyrics": { - "placeholders": { - "error": { - "type": "String" - } - } - }, - "errorParsingLyrics": "解析歌词时出错:{error}", - "@errorParsingLyrics": { - "placeholders": { - "error": { - "type": "String" - } - } - }, - "noTracksInQueue": "队列中没有曲目", - "unknownArtist": "未知艺术家", - "showLyrics": "显示歌词", - "showQueue": "显示队列", - "showCover": "显示封面", - "importedLyricsLines": "为\"{title}\"导入了 {count} 行歌词", - "@importedLyricsLines": { - "placeholders": { - "count": { - "type": "int" - }, - "title": { - "type": "String" - } - } - }, - "selected": "已选择 {count} 项", - "@selected": { - "placeholders": { - "count": { - "type": "int" - } - } - }, - "addToPlaylist": "添加到播放列表", - "delete": "删除", - "groovyBox": "GroovyBox", - "library": "音乐库", - "importFiles": "导入文件", - "searchTracks": "搜索曲目...", - "searchTracksWithCount": "搜索曲目...(共 {total} 首)", - "@searchTracksWithCount": { - "placeholders": { - "total": { - "type": "int" - } - } - }, - "searchTracksFiltered": "搜索曲目...({filtered} / {total} 首)", - "@searchTracksFiltered": { - "placeholders": { - "filtered": { - "type": "int" - }, - "total": { - "type": "int" - } - } - }, - "error": "错误:{message}", - "@error": { - "placeholders": { - "message": { - "type": "String" - } - } - }, - "noTracksYet": "还没有曲目,请添加一些!", - "noTracksMatchSearch": "没有匹配搜索的曲目。", - "deleteTrack": "删除曲目?", - "confirmDeleteTrack": "您确定要删除\"{title}\"吗?此操作无法撤销。", - "@confirmDeleteTrack": { - "placeholders": { - "title": { - "type": "String" - } - } - }, - "deletedTrack": "已删除\"{title}\"", - "@deletedTrack": { - "placeholders": { - "title": { - "type": "String" - } - } - }, - "viewDetails": "查看详情", - "editMetadata": "编辑元数据", - "importLyrics": "导入歌词", - "noPlaylistsAvailable": "没有可用的播放列表,请先创建一个!", - "addedToPlaylist": "已添加到 {name}", - "@addedToPlaylist": { - "placeholders": { - "name": { - "type": "String" - } - } - }, - "trackDetails": "曲目详情", - "close": "关闭", - "title": "标题", - "artist": "艺术家", - "album": "专辑", - "duration": "时长", - "fileSize": "文件大小", - "filePath": "文件路径", - "dateAdded": "添加日期", - "albumArt": "专辑封面", - "present": "存在", - "editTrack": "编辑曲目", - "addedTracksToPlaylist": "已将 {count} 首曲目添加到 {name}", - "@addedTracksToPlaylist": { - "placeholders": { - "count": { - "type": "int" - }, - "name": { - "type": "String" - } - } - }, - "deleteTracks": "删除曲目?", - "confirmDeleteTracks": "您确定要删除 {count} 首曲目吗?这将从您的设备中移除它们。", - "@confirmDeleteTracks": { - "placeholders": { - "count": { - "type": "int" - } - } - }, - "deletedTracks": "已删除 {count} 首曲目", - "@deletedTracks": { - "placeholders": { - "count": { - "type": "int" - } - } - }, - "batchImportComplete": "批量导入完成:{matched} 匹配,{notMatched} 不匹配", - "@batchImportComplete": { - "placeholders": { - "matched": { - "type": "int" - }, - "notMatched": { - "type": "int" - } - } - }, - "settings": "设置", - "autoScan": "自动扫描", - "autoScanMusicLibraries": "自动扫描音乐库", - "autoScanDescription": "自动扫描音乐库中的新音乐文件", - "watchForChanges": "监视更改", - "watchForChangesDescription": "监视音乐库的文件更改", - "musicLibraries": "音乐库", - "scanLibraries": "扫描库", - "addMusicLibrary": "添加音乐库", - "addMusicLibraryDescription": "添加文件夹库来索引音乐文件。文件将被复制到内部存储以供播放。", - "noMusicLibrariesAdded": "尚未添加音乐库。", - "errorLoadingLibraries": "加载库时出错:{error}", - "@errorLoadingLibraries": { - "placeholders": { - "error": { - "type": "String" - } - } - }, - "remoteProviders": "远程提供商", - "indexRemoteProviders": "索引远程提供商", - "addRemoteProvider": "添加远程提供商", - "remoteProvidersDescription": "连接到远程媒体服务器,如Jellyfin,来访问您的音乐库。", - "noRemoteProvidersAdded": "尚未添加远程提供商。", - "errorLoadingProviders": "加载提供商时出错:{error}", - "@errorLoadingProviders": { - "placeholders": { - "error": { - "type": "String" - } - } - }, - "playerSettings": "播放器设置", - "playerSettingsDescription": "配置播放器行为和显示选项。", - "defaultPlayerScreen": "默认播放器屏幕", - "defaultPlayerScreenDescription": "选择打开播放器时显示的屏幕。", - "lyricsMode": "歌词模式", - "lyricsModeDescription": "选择歌词的显示方式。", - "continuePlaying": "继续播放", - "continuePlayingDescription": "队列为空后继续播放音乐", - "databaseManagement": "数据库管理", - "databaseManagementDescription": "管理您的音乐数据库和缓存文件。", - "resetTrackDatabase": "重置曲目数据库", - "resetTrackDatabaseDescription": "从数据库中移除所有曲目并删除缓存文件。此操作无法撤销。", - "errorLoadingSettings": "加载设置时出错:{error}", - "@errorLoadingSettings": { - "placeholders": { - "error": { - "type": "String" - } - } - }, - "addedMusicLibrary": "已添加音乐库:{path}", - "@addedMusicLibrary": { - "placeholders": { - "path": { - "type": "String" - } - } - }, - "errorAddingLibrary": "添加库时出错:{error}", - "@errorAddingLibrary": { - "placeholders": { - "error": { - "type": "String" - } - } - }, - "librariesScannedSuccessfully": "库扫描成功", - "errorScanningLibraries": "扫描库时出错:{error}", - "@errorScanningLibraries": { - "placeholders": { - "error": { - "type": "String" - } - } - }, - "noActiveRemoteProviders": "没有活动的远程提供商可索引", - "indexedRemoteProviders": "已索引 {count} 个远程提供商", - "@indexedRemoteProviders": { - "placeholders": { - "count": { - "type": "int" - } - } - }, - "errorIndexingRemoteProviders": "索引远程提供商时出错:{error}", - "@errorIndexingRemoteProviders": { - "placeholders": { - "error": { - "type": "String" - } - } - }, - "serverUrl": "服务器URL", - "serverUrlHint": "https://your-jellyfin-server.com", - "username": "用户名", - "password": "密码", - "add": "添加", - "allFieldsRequired": "所有字段都是必填的", - "addedRemoteProvider": "已添加远程提供商:{url}", - "@addedRemoteProvider": { - "placeholders": { - "url": { - "type": "String" - } - } - }, - "errorAddingProvider": "添加提供商时出错:{error}", - "@errorAddingProvider": { - "placeholders": { - "error": { - "type": "String" - } - } - }, - "confirmResetTrackDatabase": "这将永久删除数据库中的所有曲目,并移除所有缓存的音乐文件和专辑封面。此操作无法撤销。\n\n您确定要继续吗?", - "trackDatabaseReset": "曲目数据库已重置", - "errorResettingDatabase": "重置数据库时出错:{error}", - "@errorResettingDatabase": { - "placeholders": { - "error": { - "type": "String" - } - } - }, - "noTracksInAlbum": "此专辑中没有曲目", - "playAll": "播放全部", - "addToQueue": "添加到队列", - "noTracksInPlaylist": "此播放列表中没有曲目", - "noAlbumsFound": "未找到专辑", - "createOne": "创建一个", - "addNewPlaylist": "添加新播放列表", - "newPlaylist": "新播放列表", - "playlistName": "播放列表名称", - "create": "创建", - "noPlaylistsYet": "还没有播放列表", - "queue": "队列", - "appSettings": "应用设置", - "appSettingsDescription": "配置应用范围的设置和偏好。", - "language": "语言", - "languageDescription": "选择应用语言。", - "english": "English", - "chinese": "中文", - "settingsTitle": "设置", - "tracks": "曲目", - "albums": "专辑", - "playlists": "播放列表", - "autoScan": "自动扫描", - "autoScanMusicLibraries": "自动扫描音乐库", - "autoScanDescription": "自动扫描音乐库中的新音乐文件", - "watchForChanges": "监视更改", - "watchForChangesDescription": "监视音乐库的文件更改", - "musicLibraries": "音乐库", - "scanLibraries": "扫描库", - "addMusicLibrary": "添加音乐库", - "addMusicLibraryDescription": "添加文件夹库来索引音乐文件。文件将被复制到内部存储以供播放。", - "noMusicLibrariesAdded": "尚未添加音乐库。", - "remoteProviders": "远程提供商", - "indexRemoteProviders": "索引远程提供商", - "addRemoteProvider": "添加远程提供商", - "remoteProvidersDescription": "连接到远程媒体服务器,如Jellyfin,来访问您的音乐库。", - "noRemoteProvidersAdded": "尚未添加远程提供商。", - "playerSettings": "播放器设置", - "playerSettingsDescription": "配置播放器行为和显示选项。", - "defaultPlayerScreen": "默认播放器屏幕", - "defaultPlayerScreenDescription": "选择打开播放器时显示的屏幕。", - "lyricsMode": "歌词模式", - "lyricsModeDescription": "选择歌词的显示方式。", - "continuePlaying": "继续播放", - "continuePlayingDescription": "队列为空后继续播放音乐", - "databaseManagement": "数据库管理", - "databaseManagementDescription": "管理您的音乐数据库和缓存文件。", - "resetTrackDatabase": "重置曲目数据库", - "resetTrackDatabaseDescription": "从数据库中移除所有曲目并删除缓存文件。此操作无法撤销。", - "addedMusicLibrary": "已添加音乐库:{path}", - "errorAddingLibrary": "添加库时出错:{error}", - "librariesScannedSuccessfully": "库扫描成功", - "errorScanningLibraries": "扫描库时出错:{error}", - "noActiveRemoteProviders": "没有活动的远程提供商可索引", - "indexedRemoteProviders": "已索引 {count} 个远程提供商", - "errorIndexingRemoteProviders": "索引远程提供商时出错:{error}", - "addedRemoteProvider": "已添加远程提供商:{url}", - "errorAddingProvider": "添加提供商时出错:{error}", - "confirmResetTrackDatabase": "这将永久删除数据库中的所有曲目,并移除所有缓存的音乐文件和专辑封面。此操作无法撤销。\n\n您确定要继续吗?", - "trackDatabaseReset": "曲目数据库已重置", - "errorResettingDatabase": "重置数据库时出错:{error}", - "addRemoteProviderDialog": "添加远程提供商", - "serverUrl": "服务器URL", - "serverUrlHint": "https://your-jellyfin-server.com", - "username": "用户名", - "password": "密码", - "add": "添加", - "allFieldsRequired": "所有字段都是必填的", - "reset": "重置", - "imported": "已导入", - "lyricsLines": "歌词", - "createdAt":"创建于", - "matched": "匹配的", - "notMatched": "未匹配的", - "deleted": "已删除", - "confirmDelete": "确认删除", - "thisWillRemoveThemFromYourDevice":"这将从您的设备上删除。", - "added": "已添加", - "to": "至", - "unknown": "未知" -} From e1f81bbdf5608bb1d6841df91fc6a8fd2317070d Mon Sep 17 00:00:00 2001 From: liang-work Date: Tue, 6 Jan 2026 22:39:23 +0800 Subject: [PATCH 09/16] =?UTF-8?q?=E2=9E=95=20Add=20dependency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pubspec.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pubspec.yaml b/pubspec.yaml index be7ae76..526cf44 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,6 +65,7 @@ dependencies: material_symbols_icons: ^4.2892.0 window_manager: ^0.5.1 go_router: ^17.0.1 + easy_localization: ^3.0.8 dev_dependencies: flutter_test: @@ -96,6 +97,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - assets/images/ + - assets/locales/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images From e8863caad5d9564443dff7728058627937a1d9e9 Mon Sep 17 00:00:00 2001 From: liang-work Date: Tue, 6 Jan 2026 22:40:23 +0800 Subject: [PATCH 10/16] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Modify=20the=20local?= =?UTF-8?q?ization=20implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/locales/en.json | 156 ++++++++++++++++++++ assets/locales/zh.json | 157 +++++++++++++++++++++ lib/main.dart | 40 +++--- lib/providers/locale_provider.dart | 30 ++-- lib/ui/screens/album_detail_screen.dart | 11 +- lib/ui/screens/library_screen.dart | 142 +++++++++---------- lib/ui/screens/player_screen.dart | 77 +++++----- lib/ui/screens/playlist_detail_screen.dart | 11 +- lib/ui/screens/settings_screen.dart | 115 +++++++-------- lib/ui/tabs/albums_tab.dart | 7 +- lib/ui/tabs/playlists_tab.dart | 21 +-- lib/ui/widgets/mini_player.dart | 17 ++- lib/ui/widgets/track_tile.dart | 6 +- 13 files changed, 554 insertions(+), 236 deletions(-) create mode 100644 assets/locales/en.json create mode 100644 assets/locales/zh.json diff --git a/assets/locales/en.json b/assets/locales/en.json new file mode 100644 index 0000000..decce04 --- /dev/null +++ b/assets/locales/en.json @@ -0,0 +1,156 @@ +{ + "noMediaSelected": "No media selected", + "noLyricsAvailable": "No Lyrics Available", + "fetchLyrics": "Fetch Lyrics", + "searchLyricsWith": "Search lyrics with {searchTerm}", + "whereToSearchLyrics": "Where do you want to search lyrics from?", + "musixmatch": "Musixmatch", + "netease": "NetEase", + "lrclib": "Lrclib", + "manualImport": "Manual Import", + "cancel": "Cancel", + "lyricsOptions": "Lyrics Options", + "refetch": "Re-fetch", + "clear": "Clear", + "liveSyncLyrics": "Live Sync Lyrics", + "manualOffset": "Manual Offset", + "adjustLyricsTiming": "Adjust Lyrics Timing", + "enterOffsetMs": "Enter offset in milliseconds.\nPositive values delay lyrics, negative values advance them.", + "offsetMs": "Offset (ms)", + "save": "Save", + "liveLyricsSync": "Live Lyrics Sync", + "offset": "Offset: {value}ms", + "minus100ms": "-100ms", + "plus10ms": "+10ms", + "reset": "Reset", + "plus100ms": "+100ms", + "fineAdjustment": "Fine Adjustment", + "onlyTimedLyricsCanBeSynced": "Only timed lyrics can be synced", + "errorLoadingLyrics": "Error loading lyrics: {error}", + "errorParsingLyrics": "Error parsing lyrics: {error}", + "noTracksInQueue": "No tracks in queue", + "unknownArtist": "Unknown Artist", + "showLyrics": "Show Lyrics", + "showQueue": "Show Queue", + "showCover": "Show Cover", + "importedLyricsLines": "Imported {count} lyrics lines for \"{title}\"", + "selected": "{count} selected", + "addToPlaylist": "Add to Playlist", + "delete": "Delete", + "groovyBox": "GroovyBox", + "library": "Library", + "importFiles": "Import Files", + "searchTracks": "Search tracks...", + "searchTracksWithCount": "Search tracks... ({total} tracks)", + "searchTracksFiltered": "Search tracks... ({filtered} of {total} tracks)", + "error": "Error: {message}", + "noTracksYet": "No tracks yet. Add some!", + "noTracksMatchSearch": "No tracks match your search.", + "deleteTrack": "Delete Track?", + "confirmDeleteTrack": "Are you sure you want to delete \"{title}\"? This cannot be undone.", + "deletedTrack": "Deleted \"{title}\"", + "viewDetails": "View Details", + "editMetadata": "Edit Metadata", + "importLyrics": "Import Lyrics", + "addedToPlaylist": "Added to {name}", + "trackDetails": "Track Details", + "close": "Close", + "title": "Title", + "artist": "Artist", + "album": "Album", + "duration": "Duration", + "fileSize": "File Size", + "filePath": "File Path", + "dateAdded": "Date Added", + "albumArt": "Album Art", + "present": "Present", + "editTrack": "Edit Track", + "addedTracksToPlaylist": "Added {count} tracks to {name}", + "deleteTracks": "Delete Tracks?", + "confirmDeleteTracks": "Are you sure you want to delete {count} tracks? This will remove them from your device.", + "deletedTracks": "Deleted {count} tracks", + "batchImportComplete": "Batch import complete: {matched} matched, {notMatched} not matched", + "settings": "Settings", + "autoScan": "Auto Scan", + "autoScanMusicLibraries": "Auto-scan music libraries", + "autoScanDescription": "Automatically scan music libraries for new music files", + "watchForChanges": "Watch for changes", + "watchForChangesDescription": "Monitor music libraries for file changes", + "musicLibraries": "Music Libraries", + "scanLibraries": "Scan Libraries", + "addMusicLibrary": "Add Music Library", + "addMusicLibraryDescription": "Add folder libraries to index music files. Files will be copied to internal storage for playback.", + "noMusicLibrariesAdded": "No music libraries added yet.", + "errorLoadingLibraries": "Error loading libraries: {error}", + "remoteProviders": "Remote Providers", + "indexRemoteProviders": "Index Remote Providers", + "addRemoteProvider": "Add Remote Provider", + "remoteProvidersDescription": "Connect to remote media servers like Jellyfin to access your music library.", + "noRemoteProvidersAdded": "No remote providers added yet.", + "errorLoadingProviders": "Error loading providers: {error}", + "playerSettings": "Player Settings", + "playerSettingsDescription": "Configure player behavior and display options.", + "defaultPlayerScreen": "Default Player Screen", + "defaultPlayerScreenDescription": "Choose which screen to show when opening the player.", + "lyricsMode": "Lyrics Mode", + "lyricsModeDescription": "Choose how lyrics are displayed.", + "continuePlaying": "Continue Playing", + "continuePlayingDescription": "Continue playing music after the queue is empty", + "databaseManagement": "Database Management", + "databaseManagementDescription": "Manage your music database and cached files.", + "resetTrackDatabase": "Reset Track Database", + "resetTrackDatabaseDescription": "Remove all tracks from database and delete cached files. This action cannot be undone.", + "errorLoadingSettings": "Error loading settings: {error}", + "playAll": "Play All", + "addToQueue": "Add to Queue", + "noTracksInPlaylist": "No tracks in this playlist", + "noAlbumsFound": "No albums found", + "createOne": "Create One", + "addNewPlaylist": "Add a new playlist", + "newPlaylist": "New Playlist", + "playlistName": "Playlist Name", + "create": "Create", + "noPlaylistsYet": "No playlists yet", + "queue": "Queue", + "appSettings": "App Settings", + "appSettingsDescription": "Configure app-wide settings and preferences.", + "language": "Language", + "languageDescription": "Choose the app language.", + "english": "English", + "chinese": "中文", + "settingsTitle": "Settings", + "tracks": "Tracks", + "albums": "Albums", + "playlists": "Playlists", + "addedMusicLibrary": "Added music library: {path}", + "errorAddingLibrary": "Error adding library: {error}", + "librariesScannedSuccessfully": "Libraries scanned successfully", + "errorScanningLibraries": "Error scanning libraries: {error}", + "noActiveRemoteProviders": "No active remote providers to index", + "indexedRemoteProviders": "Indexed {count} remote provider(s)", + "errorIndexingRemoteProviders": "Error indexing remote providers: {error}", + "addedRemoteProvider": "Added remote provider: {url}", + "errorAddingProvider": "Error adding provider: {error}", + "confirmResetTrackDatabase": "This will permanently delete all tracks from the database and remove all cached music files and album art. This action cannot be undone.\n\nAre you sure you want to continue?", + "trackDatabaseReset": "Track database has been reset", + "errorResettingDatabase": "Error resetting database: {error}", + "addRemoteProviderDialog": "Add Remote Provider", + "serverUrl": "Server URL", + "serverUrlHint": "https://your-jellyfin-server.com", + "username": "Username", + "password": "Password", + "add": "Add", + "allFieldsRequired": "All fields are required", + "imported": "Imported", + "lyricsLines":"lyrics lines for", + "createdAt": "created at", + "matched": "matched", + "notMatched": "not matched", + "deleted": "deleted", + "confirmDelete": "confirm delete", + "thisWillRemoveThemFromYourDevice":"This will remove them from your device.", + "added": "added", + "to":"to", + "unknown":"Unknown", + "noPlaylistsAvailable":"No Playlists available" +} diff --git a/assets/locales/zh.json b/assets/locales/zh.json new file mode 100644 index 0000000..beea5f5 --- /dev/null +++ b/assets/locales/zh.json @@ -0,0 +1,157 @@ +{ + "noMediaSelected": "未选择媒体", + "noLyricsAvailable": "无歌词可用", + "fetchLyrics": "获取歌词", + "searchLyricsWith": "使用 {searchTerm} 搜索歌词", + "whereToSearchLyrics": "您想从哪里搜索歌词?", + "musixmatch": "Musixmatch", + "netease": "网易云音乐", + "lrclib": "Lrclib", + "manualImport": "手动导入", + "cancel": "取消", + "lyricsOptions": "歌词选项", + "refetch": "重新获取", + "clear": "清除", + "liveSyncLyrics": "实时同步歌词", + "manualOffset": "手动偏移", + "adjustLyricsTiming": "调整歌词时间", + "enterOffsetMs": "输入偏移量(毫秒)。\n正值延迟歌词显示,负值提前显示。", + "offsetMs": "偏移量(毫秒)", + "save": "保存", + "liveLyricsSync": "实时歌词同步", + "offset": "偏移量:{value}毫秒", + "minus100ms": "-100毫秒", + "plus10ms": "+10毫秒", + "reset": "重置", + "plus100ms": "+100毫秒", + "fineAdjustment": "精细调整", + "onlyTimedLyricsCanBeSynced": "只有带时间戳的歌词才能同步", + "errorLoadingLyrics": "加载歌词时出错:{error}", + "errorParsingLyrics": "解析歌词时出错:{error}", + "noTracksInQueue": "队列中没有曲目", + "unknownArtist": "未知艺术家", + "showLyrics": "显示歌词", + "showQueue": "显示队列", + "showCover": "显示封面", + "importedLyricsLines": "为\"{title}\"导入了 {count} 行歌词", + "selected": "已选择 {count} 项", + "addToPlaylist": "添加到播放列表", + "delete": "删除", + "groovyBox": "GroovyBox", + "library": "音乐库", + "importFiles": "导入文件", + "searchTracks": "搜索曲目...", + "searchTracksWithCount": "搜索曲目...(共 {total} 首)", + "searchTracksFiltered": "搜索曲目...({filtered} / {total} 首)", + "error": "错误:{message}", + "noTracksYet": "还没有曲目,请添加一些!", + "noTracksMatchSearch": "没有匹配搜索的曲目。", + "deleteTrack": "删除曲目?", + "confirmDeleteTrack": "您确定要删除\"{title}\"吗?此操作无法撤销。", + "deletedTrack": "已删除\"{title}\"", + "viewDetails": "查看详情", + "editMetadata": "编辑元数据", + "importLyrics": "导入歌词", + "noPlaylistsAvailable": "没有可用的播放列表,请先创建一个!", + "addedToPlaylist": "已添加到 {name}", + "trackDetails": "曲目详情", + "close": "关闭", + "title": "标题", + "artist": "艺术家", + "album": "专辑", + "duration": "时长", + "fileSize": "文件大小", + "filePath": "文件路径", + "dateAdded": "添加日期", + "albumArt": "专辑封面", + "present": "存在", + "editTrack": "编辑曲目", + "addedTracksToPlaylist": "已将 {count} 首曲目添加到 {name}", + "deleteTracks": "删除曲目?", + "confirmDeleteTracks": "您确定要删除 {count} 首曲目吗?这将从您的设备中移除它们。", + "deletedTracks": "已删除 {count} 首曲目", + "batchImportComplete": "批量导入完成:{matched} 匹配,{notMatched} 不匹配", + "settings": "设置", + "autoScan": "自动扫描", + "autoScanMusicLibraries": "自动扫描音乐库", + "autoScanDescription": "自动扫描音乐库中的新音乐文件", + "watchForChanges": "监视更改", + "watchForChangesDescription": "监视音乐库的文件更改", + "musicLibraries": "音乐库", + "scanLibraries": "扫描库", + "addMusicLibrary": "添加音乐库", + "addMusicLibraryDescription": "添加文件夹库来索引音乐文件。文件将被复制到内部存储以供播放。", + "noMusicLibrariesAdded": "尚未添加音乐库。", + "errorLoadingLibraries": "加载库时出错:{error}", + "remoteProviders": "远程提供商", + "indexRemoteProviders": "索引远程提供商", + "addRemoteProvider": "添加远程提供商", + "remoteProvidersDescription": "连接到远程媒体服务器,如Jellyfin,来访问您的音乐库。", + "noRemoteProvidersAdded": "尚未添加远程提供商。", + "errorLoadingProviders": "加载提供商时出错:{error}", + "playerSettings": "播放器设置", + "playerSettingsDescription": "配置播放器行为和显示选项。", + "defaultPlayerScreen": "默认播放器屏幕", + "defaultPlayerScreenDescription": "选择打开播放器时显示的屏幕。", + "lyricsMode": "歌词模式", + "lyricsModeDescription": "选择歌词的显示方式。", + "continuePlaying": "继续播放", + "continuePlayingDescription": "队列为空后继续播放音乐", + "databaseManagement": "数据库管理", + "databaseManagementDescription": "管理您的音乐数据库和缓存文件。", + "resetTrackDatabase": "重置曲目数据库", + "resetTrackDatabaseDescription": "从数据库中移除所有曲目并删除缓存文件。此操作无法撤销。", + "errorLoadingSettings": "加载设置时出错:{error}", + "noTracksInAlbum": "此专辑中没有曲目", + "playAll": "播放全部", + "addToQueue": "添加到队列", + "noTracksInPlaylist": "此播放列表中没有曲目", + "noAlbumsFound": "未找到专辑", + "createOne": "创建一个", + "addNewPlaylist": "添加新播放列表", + "newPlaylist": "新播放列表", + "playlistName": "播放列表名称", + "create": "创建", + "noPlaylistsYet": "还没有播放列表", + "queue": "队列", + "appSettings": "应用设置", + "appSettingsDescription": "配置应用范围的设置和偏好。", + "language": "语言", + "languageDescription": "选择应用语言。", + "english": "English", + "chinese": "中文", + "settingsTitle": "设置", + "tracks": "曲目", + "albums": "专辑", + "playlists": "播放列表", + "addedMusicLibrary": "已添加音乐库:{path}", + "errorAddingLibrary": "添加库时出错:{error}", + "librariesScannedSuccessfully": "库扫描成功", + "errorScanningLibraries": "扫描库时出错:{error}", + "noActiveRemoteProviders": "没有活动的远程提供商可索引", + "indexedRemoteProviders": "已索引 {count} 个远程提供商", + "errorIndexingRemoteProviders": "索引远程提供商时出错:{error}", + "addedRemoteProvider": "已添加远程提供商:{url}", + "errorAddingProvider": "添加提供商时出错:{error}", + "confirmResetTrackDatabase": "这将永久删除数据库中的所有曲目,并移除所有缓存的音乐文件和专辑封面。此操作无法撤销。\n\n您确定要继续吗?", + "trackDatabaseReset": "曲目数据库已重置", + "errorResettingDatabase": "重置数据库时出错:{error}", + "addRemoteProviderDialog": "添加远程提供商", + "serverUrl": "服务器URL", + "serverUrlHint": "https://your-jellyfin-server.com", + "username": "用户名", + "password": "密码", + "add": "添加", + "allFieldsRequired": "所有字段都是必填的", + "imported": "已导入", + "lyricsLines": "歌词", + "createdAt":"创建于", + "matched": "匹配的", + "notMatched": "未匹配的", + "deleted": "已删除", + "confirmDelete": "确认删除", + "thisWillRemoveThemFromYourDevice":"这将从您的设备上删除。", + "added": "已添加", + "to": "至", + "unknown": "未知" +} diff --git a/lib/main.dart b/lib/main.dart index 35be7ff..7eb52e5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,5 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:groovybox/l10n/app_localizations.dart'; import 'package:groovybox/logic/audio_handler.dart'; import 'package:groovybox/logic/window_helpers.dart'; import 'package:groovybox/providers/audio_provider.dart'; @@ -17,6 +16,9 @@ Future main() async { WidgetsFlutterBinding.ensureInitialized(); MediaKit.ensureInitialized(); + // Initialize EasyLocalization + await EasyLocalization.ensureInitialized(); + // Initialize window manager for desktop platforms if (isDesktopPlatform()) { await initializeWindowManager(); @@ -36,14 +38,19 @@ Future main() async { setAudioHandler(_audioHandler); runApp( - ProviderScope( - child: Builder( - builder: (context) { - // Get the provider container and set it on the audio handler - final container = ProviderScope.containerOf(context); - _audioHandler.setProviderContainer(container); - return GroovyApp(); - }, + EasyLocalization( + supportedLocales: const [Locale('en'), Locale('zh')], + path: 'assets/locales', + fallbackLocale: const Locale('en'), + child: ProviderScope( + child: Builder( + builder: (context) { + // Get the provider container and set it on the audio handler + final container = ProviderScope.containerOf(context); + _audioHandler.setProviderContainer(container); + return GroovyApp(); + }, + ), ), ), ); @@ -81,16 +88,9 @@ class _GroovyAppState extends ConsumerState { themeMode: themeMode, locale: locale, routerConfig: router, - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [ - Locale('en'), // English - Locale('zh'), // Chinese - ], + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, ); } } + diff --git a/lib/providers/locale_provider.dart b/lib/providers/locale_provider.dart index 49973d7..d12f291 100644 --- a/lib/providers/locale_provider.dart +++ b/lib/providers/locale_provider.dart @@ -1,39 +1,29 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; part 'locale_provider.g.dart'; @Riverpod(keepAlive: true) class LocaleNotifier extends _$LocaleNotifier { - static const String _localeKey = 'locale'; - @override Locale build() { - // Load saved locale asynchronously - _loadLocale(); return const Locale('en'); // Default to English } - Future _loadLocale() async { - final prefs = await SharedPreferences.getInstance(); - final localeString = prefs.getString(_localeKey); - if (localeString == 'zh') { - state = const Locale('zh'); - } - } - - Future setLocale(Locale locale) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setString(_localeKey, locale.languageCode); + Future setLocale(BuildContext context, Locale locale) async { + await context.setLocale(locale); state = locale; } - Future setEnglish() async { - await setLocale(const Locale('en')); + Future setEnglish(BuildContext context) async { + await setLocale(context, const Locale('en')); } - Future setChinese() async { - await setLocale(const Locale('zh')); + Future setChinese(BuildContext context) async { + await setLocale(context, const Locale('zh')); } } + + + diff --git a/lib/ui/screens/album_detail_screen.dart b/lib/ui/screens/album_detail_screen.dart index d290f91..be948d1 100644 --- a/lib/ui/screens/album_detail_screen.dart +++ b/lib/ui/screens/album_detail_screen.dart @@ -1,10 +1,11 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:gap/gap.dart'; import 'package:groovybox/data/db.dart'; import 'package:groovybox/data/playlist_repository.dart'; -import 'package:groovybox/l10n/app_localizations.dart'; + import 'package:groovybox/providers/audio_provider.dart'; import 'package:groovybox/ui/widgets/track_tile.dart'; import 'package:groovybox/ui/widgets/universal_image.dart'; @@ -76,7 +77,7 @@ class AlbumDetailScreen extends HookConsumerWidget { if (tracks.isEmpty) { return SizedBox( height: 200, - child: Center(child: Text(AppLocalizations.of(context)!.noTracksInAlbum)), + child: Center(child: Text(context.tr('noTracksInAlbum'))), ); } @@ -97,7 +98,7 @@ class AlbumDetailScreen extends HookConsumerWidget { _playAlbum(ref, tracks); }, icon: const Icon(Symbols.play_arrow), - label: Text(AppLocalizations.of(context)!.playAll), + label: Text(context.tr('playAll')), ), ), SizedBox( @@ -107,7 +108,7 @@ class AlbumDetailScreen extends HookConsumerWidget { _addToQueue(ref, tracks); }, icon: const Icon(Symbols.queue_music), - label: Text(AppLocalizations.of(context)!.addToQueue), + label: Text(context.tr('addToQueue')), ), ), ], @@ -224,3 +225,5 @@ class AlbumDetailScreen extends HookConsumerWidget { ); } } + + diff --git a/lib/ui/screens/library_screen.dart b/lib/ui/screens/library_screen.dart index f6eb500..55e5051 100644 --- a/lib/ui/screens/library_screen.dart +++ b/lib/ui/screens/library_screen.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; @@ -6,7 +7,7 @@ import 'package:gap/gap.dart'; import 'package:groovybox/data/db.dart'; import 'package:groovybox/data/playlist_repository.dart'; import 'package:groovybox/data/track_repository.dart'; -import 'package:groovybox/l10n/app_localizations.dart'; + import 'package:groovybox/logic/lyrics_parser.dart'; import 'package:groovybox/logic/window_helpers.dart'; import 'package:groovybox/providers/audio_provider.dart'; @@ -83,14 +84,14 @@ class LibraryScreen extends HookConsumerWidget { onPressed: clearSelection, ), title: Text( - AppLocalizations.of(context)!.selected(selectedTrackIds.value.length), + context.tr('selected', args: [selectedTrackIds.value.length.toString()]), ).textColor(Theme.of(context).colorScheme.onPrimary), backgroundColor: Theme.of(context).colorScheme.primary, actions: [ IconButton( icon: const Icon(Symbols.playlist_add), color: Theme.of(context).colorScheme.onPrimary, - tooltip: AppLocalizations.of(context)!.addToPlaylist, + tooltip: context.tr('addToPlaylist'), onPressed: () { _batchAddToPlaylist( context, @@ -103,7 +104,7 @@ class LibraryScreen extends HookConsumerWidget { IconButton( icon: const Icon(Symbols.delete), color: Theme.of(context).colorScheme.onPrimary, - tooltip: AppLocalizations.of(context)!.delete, + tooltip: context.tr('delete'), onPressed: () { _batchDelete( context, @@ -140,7 +141,7 @@ class LibraryScreen extends HookConsumerWidget { ), ], ) - : Text(AppLocalizations.of(context)!.library), + : Text(context.tr('library')), actions: [ IconButton( onPressed: () { @@ -150,7 +151,7 @@ class LibraryScreen extends HookConsumerWidget { ), IconButton( icon: const Icon(Symbols.add_circle_outline), - tooltip: AppLocalizations.of(context)!.importFiles, + tooltip: context.tr('importFiles'), onPressed: () async { final result = await FilePicker.platform.pickFiles( type: FileType.any, @@ -211,15 +212,15 @@ class LibraryScreen extends HookConsumerWidget { destinations: [ NavigationRailDestination( icon: Icon(Symbols.audiotrack), - label: Text(AppLocalizations.of(context)!.tracks), + label: Text(context.tr('tracks')), ), NavigationRailDestination( icon: Icon(Symbols.album), - label: Text(AppLocalizations.of(context)!.albums), + label: Text(context.tr('albums')), ), NavigationRailDestination( icon: Icon(Symbols.queue_music), - label: Text(AppLocalizations.of(context)!.playlists), + label: Text(context.tr('playlists')), ), ], ), @@ -258,13 +259,13 @@ class LibraryScreen extends HookConsumerWidget { onPressed: clearSelection, ), title: Text( - AppLocalizations.of(context)!.selected(selectedTrackIds.value.length), + context.tr('selected', args: [selectedTrackIds.value.length.toString()]), ).textColor(Theme.of(context).colorScheme.onPrimary), backgroundColor: Theme.of(context).colorScheme.primary, actions: [ IconButton( icon: const Icon(Symbols.playlist_add), - tooltip: AppLocalizations.of(context)!.addToPlaylist, + tooltip: context.tr('addToPlaylist'), color: Theme.of(context).colorScheme.onPrimary, onPressed: () { _batchAddToPlaylist( @@ -277,7 +278,7 @@ class LibraryScreen extends HookConsumerWidget { ), IconButton( icon: const Icon(Symbols.delete), - tooltip: AppLocalizations.of(context)!.delete, + tooltip: context.tr('delete'), color: Theme.of(context).colorScheme.onPrimary, onPressed: () { _batchDelete( @@ -293,12 +294,12 @@ class LibraryScreen extends HookConsumerWidget { ) : AppBar( centerTitle: true, - title: Text(AppLocalizations.of(context)!.library), + title: Text(context.tr('library')), bottom: TabBar( tabs: [ - Tab(text: AppLocalizations.of(context)!.tracks, icon: Icon(Symbols.audiotrack)), - Tab(text: AppLocalizations.of(context)!.albums, icon: Icon(Symbols.album)), - Tab(text: AppLocalizations.of(context)!.playlists, icon: Icon(Symbols.queue_music)), + Tab(text: context.tr('tracks'), icon: Icon(Symbols.audiotrack)), + Tab(text: context.tr('albums'), icon: Icon(Symbols.album)), + Tab(text: context.tr('playlists'), icon: Icon(Symbols.queue_music)), ], ), actions: [ @@ -310,7 +311,7 @@ class LibraryScreen extends HookConsumerWidget { ), IconButton( icon: const Icon(Symbols.add_circle_outline), - tooltip: AppLocalizations.of(context)!.importFiles, + tooltip: context.tr('importFiles'), onPressed: () async { final result = await FilePicker.platform.pickFiles( type: FileType.any, @@ -392,12 +393,12 @@ class LibraryScreen extends HookConsumerWidget { // Calculate hintText String hintText; if (!snapshot.hasData || snapshot.hasError) { - hintText = AppLocalizations.of(context)!.searchTracks; + hintText = context.tr('searchTracks'); } else { final tracks = snapshot.data!; final totalTracks = tracks.length; if (searchQuery.value.isEmpty) { - hintText = '${AppLocalizations.of(context)!.searchTracks} ($totalTracks ${AppLocalizations.of(context)!.tracks})'; + hintText = '${context.tr('searchTracks')} ($totalTracks ${context.tr('tracks')})'; } else { final query = searchQuery.value.toLowerCase(); final filteredCount = tracks.where((track) { @@ -421,7 +422,7 @@ class LibraryScreen extends HookConsumerWidget { return false; }).length; hintText = - '${AppLocalizations.of(context)!.searchTracks}... ($filteredCount of $totalTracks ${AppLocalizations.of(context)!.tracks})'; + '${context.tr('searchTracks')}... ($filteredCount of $totalTracks ${context.tr('tracks')})'; } } @@ -434,7 +435,7 @@ class LibraryScreen extends HookConsumerWidget { } else { final tracks = snapshot.data!; if (tracks.isEmpty) { - mainContent = Center(child: Text(AppLocalizations.of(context)!.noTracksYet)); + mainContent = Center(child: Text(context.tr('noTracksYet'))); } else { List filteredTracks; if (searchQuery.value.isEmpty) { @@ -465,7 +466,7 @@ class LibraryScreen extends HookConsumerWidget { if (filteredTracks.isEmpty && searchQuery.value.isNotEmpty) { mainContent = Center( - child: Text(AppLocalizations.of(context)!.noTracksMatchSearch), + child: Text(context.tr('noTracksMatchSearch')), ); } else { mainContent = ListView.builder( @@ -492,7 +493,7 @@ class LibraryScreen extends HookConsumerWidget { overflow: TextOverflow.ellipsis, ), subtitle: Text( - '${track.artist ?? AppLocalizations.of(context)!.unknownArtist} • ${_formatDuration(track.duration)}', + '${track.artist ?? context.tr('unknownArtist')} ?${_formatDuration(track.duration)}', maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -514,16 +515,15 @@ class LibraryScreen extends HookConsumerWidget { context: context, builder: (context) { return AlertDialog( - title: Text(AppLocalizations.of(context)!.deleteTrack), + title: Text(context.tr('deleteTrack')), content: Text( - AppLocalizations.of(context)! - .confirmDeleteTrack(track.title), + context.tr('confirmDeleteTrack', args: [track.title]), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), - child: Text(AppLocalizations.of(context)!.cancel), + child: Text(context.tr('cancel')), ), TextButton( onPressed: () => @@ -531,7 +531,7 @@ class LibraryScreen extends HookConsumerWidget { style: TextButton.styleFrom( foregroundColor: Colors.red, ), - child: Text(AppLocalizations.of(context)!.delete), + child: Text(context.tr('delete')), ), ], ); @@ -545,8 +545,7 @@ class LibraryScreen extends HookConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - AppLocalizations.of(context)! - .deletedTrack(track.title), + context.tr('deletedTrack', args: [track.title]), ), ), ); @@ -642,7 +641,7 @@ class LibraryScreen extends HookConsumerWidget { children: [ ListTile( leading: const Icon(Symbols.playlist_add), - title: Text(AppLocalizations.of(context)!.addToPlaylist), + title: Text(context.tr('addToPlaylist')), onTap: () { Navigator.pop(context); _showAddToPlaylistDialog(context, ref, track); @@ -650,7 +649,7 @@ class LibraryScreen extends HookConsumerWidget { ), ListTile( leading: const Icon(Symbols.info), - title: Text(AppLocalizations.of(context)!.viewDetails), + title: Text(context.tr('viewDetails')), onTap: () { Navigator.pop(context); _showTrackDetails(context, ref, track); @@ -658,7 +657,7 @@ class LibraryScreen extends HookConsumerWidget { ), ListTile( leading: const Icon(Symbols.edit), - title: Text(AppLocalizations.of(context)!.editMetadata), + title: Text(context.tr('editMetadata')), onTap: () { Navigator.pop(context); _showEditDialog(context, ref, track); @@ -666,7 +665,7 @@ class LibraryScreen extends HookConsumerWidget { ), ListTile( leading: const Icon(Symbols.lyrics), - title: Text(AppLocalizations.of(context)!.importLyrics), + title: Text(context.tr('importLyrics')), onTap: () { Navigator.pop(context); _importLyricsForTrack(context, ref, track); @@ -675,7 +674,7 @@ class LibraryScreen extends HookConsumerWidget { ListTile( leading: const Icon(Symbols.delete, color: Colors.red), title: Text( - AppLocalizations.of(context)!.deleteTrack, + context.tr('deleteTrack'), style: TextStyle(color: Colors.red), ), onTap: () { @@ -706,7 +705,7 @@ class LibraryScreen extends HookConsumerWidget { // For simplicity, we'll assume the user wants to pick from *current* playlists. // Or we can use a Consumer widget inside the dialog. return AlertDialog( - title: Text(AppLocalizations.of(context)!.addToPlaylist), + title: Text(context.tr('addToPlaylist')), content: ConstrainedBox( constraints: BoxConstraints( maxWidth: screenSize.width * 0.8, @@ -726,7 +725,7 @@ class LibraryScreen extends HookConsumerWidget { final playlists = snapshot.data!; if (playlists.isEmpty) { return Text( - AppLocalizations.of(context)!.noPlaylistsAvailable, + context.tr('noPlaylistsAvailable'), ); } @@ -744,8 +743,7 @@ class LibraryScreen extends HookConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - AppLocalizations.of(context)! - .addedToPlaylist(playlist.name), + context.tr('addedToPlaylist', args: [playlist.name]), ), ), ); @@ -762,7 +760,7 @@ class LibraryScreen extends HookConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.cancel), + child: Text(context.tr('cancel')), ), ], ); @@ -776,9 +774,9 @@ class LibraryScreen extends HookConsumerWidget { Track track, ) async { // Try to get file info - String fileSize = AppLocalizations.of(context)!.unknown; - String libraryName = AppLocalizations.of(context)!.unknown; - String dateAdded = AppLocalizations.of(context)!.unknown; + String fileSize = context.tr('unknown'); + String libraryName = context.tr('unknown'); + String dateAdded = context.tr('unknown'); try { final file = File(track.path); @@ -812,7 +810,7 @@ class LibraryScreen extends HookConsumerWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: Text(AppLocalizations.of(context)!.trackDetails), + title: Text(context.tr('trackDetails')), content: ConstrainedBox( constraints: BoxConstraints(maxWidth: screenSize.width * 0.8), child: SingleChildScrollView( @@ -820,16 +818,16 @@ class LibraryScreen extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - _buildDetailRow(AppLocalizations.of(context)!.title, track.title), - _buildDetailRow(AppLocalizations.of(context)!.artist, track.artist ?? 'Unknown'), - _buildDetailRow(AppLocalizations.of(context)!.album, track.album ?? 'Unknown'), - _buildDetailRow(AppLocalizations.of(context)!.duration, _formatDuration(track.duration)), - _buildDetailRow(AppLocalizations.of(context)!.fileSize, fileSize), - _buildDetailRow(AppLocalizations.of(context)!.library, libraryName), - _buildDetailRow(AppLocalizations.of(context)!.filePath, track.path), - _buildDetailRow(AppLocalizations.of(context)!.dateAdded, dateAdded), + _buildDetailRow(context.tr('title'), track.title), + _buildDetailRow(context.tr('artist'), track.artist ?? 'Unknown'), + _buildDetailRow(context.tr('album'), track.album ?? 'Unknown'), + _buildDetailRow(context.tr('duration'), _formatDuration(track.duration)), + _buildDetailRow(context.tr('fileSize'), fileSize), + _buildDetailRow(context.tr('library'), libraryName), + _buildDetailRow(context.tr('filePath'), track.path), + _buildDetailRow(context.tr('dateAdded'), dateAdded), if (track.artUri != null) - _buildDetailRow(AppLocalizations.of(context)!.albumArt, 'Present'), + _buildDetailRow(context.tr('albumArt'), 'Present'), ], ), ), @@ -837,7 +835,7 @@ class LibraryScreen extends HookConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.close), + child: Text(context.tr('close')), ), ], ), @@ -874,7 +872,7 @@ class LibraryScreen extends HookConsumerWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: Text(AppLocalizations.of(context)!.editTrack), + title: Text(context.tr('editTrack')), content: ConstrainedBox( constraints: BoxConstraints(maxWidth: screenSize.width * 0.8), child: Column( @@ -883,15 +881,15 @@ class LibraryScreen extends HookConsumerWidget { children: [ TextField( controller: titleController, - decoration: InputDecoration(labelText: AppLocalizations.of(context)!.title), + decoration: InputDecoration(labelText: context.tr('title')), ), TextField( controller: artistController, - decoration: InputDecoration(labelText: AppLocalizations.of(context)!.artist), + decoration: InputDecoration(labelText: context.tr('artist')), ), TextField( controller: albumController, - decoration: InputDecoration(labelText: AppLocalizations.of(context)!.album), + decoration: InputDecoration(labelText: context.tr('album')), ), ], ), @@ -899,7 +897,7 @@ class LibraryScreen extends HookConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.cancel), + child: Text(context.tr('cancel')), ), TextButton( onPressed: () { @@ -913,7 +911,7 @@ class LibraryScreen extends HookConsumerWidget { ); Navigator.pop(context); }, - child: Text(AppLocalizations.of(context)!.save), + child: Text(context.tr('save')), ), ], ), @@ -939,7 +937,7 @@ class LibraryScreen extends HookConsumerWidget { context: context, builder: (context) { return AlertDialog( - title: Text(AppLocalizations.of(context)!.addToPlaylist), + title: Text(context.tr('addToPlaylist')), content: ConstrainedBox( constraints: BoxConstraints( maxWidth: screenSize.width * 0.8, @@ -958,7 +956,7 @@ class LibraryScreen extends HookConsumerWidget { } final playlists = snapshot.data!; if (playlists.isEmpty) { - return Text(AppLocalizations.of(context)!.noPlaylistsAvailable); + return Text(context.tr('noPlaylistsAvailable')); } return SingleChildScrollView( @@ -980,7 +978,7 @@ class LibraryScreen extends HookConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - '${AppLocalizations.of(context)!.added} ${trackIds.length} ${AppLocalizations.of(context)!.tracks} ${AppLocalizations.of(context)!.to} ${playlist.name}', + '${context.tr('added')} ${trackIds.length} ${context.tr('tracks')} ${context.tr('to')} ${playlist.name}', ), ), ); @@ -997,7 +995,7 @@ class LibraryScreen extends HookConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.cancel), + child: Text(context.tr('cancel')), ), ], ); @@ -1014,20 +1012,20 @@ class LibraryScreen extends HookConsumerWidget { final confirm = await showDialog( context: context, builder: (context) => AlertDialog( - title: Text(AppLocalizations.of(context)!.deleteTracks), + title: Text(context.tr('deleteTracks')), content: Text( - '${AppLocalizations.of(context)!.confirmDelete} ${trackIds.length} ${AppLocalizations.of(context)!.tracks}? ' - '${AppLocalizations.of(context)!.thisWillRemoveThemFromYourDevice}', + '${context.tr('confirmDelete')} ${trackIds.length} ${context.tr('tracks')}? ' + '${context.tr('thisWillRemoveThemFromYourDevice')}', ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: Text(AppLocalizations.of(context)!.cancel), + child: Text(context.tr('cancel')), ), TextButton( onPressed: () => Navigator.pop(context, true), style: TextButton.styleFrom(foregroundColor: Colors.red), - child: Text(AppLocalizations.of(context)!.delete), + child: Text(context.tr('delete')), ), ], ), @@ -1043,7 +1041,7 @@ class LibraryScreen extends HookConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - '${AppLocalizations.of(context)!.deleted} ${trackIds.length} ${AppLocalizations.of(context)!.tracks}', + '${context.tr('deleted')} ${trackIds.length} ${context.tr('tracks')}', ), ), ); @@ -1077,7 +1075,7 @@ class LibraryScreen extends HookConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - '${AppLocalizations.of(context)!.imported} ${lyricsData.lines.length} ${AppLocalizations.of(context)!.lyricsLines} for "${track.title}"', + '${context.tr('imported')} ${lyricsData.lines.length} ${context.tr('lyricsLines')} for "${track.title}"', ), ), ); @@ -1128,7 +1126,7 @@ class LibraryScreen extends HookConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - '${AppLocalizations.of(context)!.batchImportComplete} $matched ${AppLocalizations.of(context)!.matched}, $notMatched ${AppLocalizations.of(context)!.notMatched}', + '${context.tr('batchImportComplete')} $matched ${context.tr('matched')}, $notMatched ${context.tr('notMatched')}', ), ), ); diff --git a/lib/ui/screens/player_screen.dart b/lib/ui/screens/player_screen.dart index 1a5fad3..565fd2d 100644 --- a/lib/ui/screens/player_screen.dart +++ b/lib/ui/screens/player_screen.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'dart:io'; import 'dart:math' as math; import 'dart:ui'; @@ -11,7 +12,7 @@ import 'package:drift/drift.dart' as drift; import 'package:gap/gap.dart'; import 'package:groovybox/data/db.dart' as db; import 'package:groovybox/data/track_repository.dart'; -import 'package:groovybox/l10n/app_localizations.dart'; + import 'package:groovybox/logic/lrc_providers.dart'; import 'package:groovybox/logic/lyrics_parser.dart'; import 'package:groovybox/logic/metadata_service.dart'; @@ -69,7 +70,7 @@ class PlayerScreen extends HookConsumerWidget { final index = snapshot.data?.index ?? 0; final medias = snapshot.data?.medias ?? []; if (medias.isEmpty || index < 0 || index >= medias.length) { - return Center(child: Text(AppLocalizations.of(context)!.noMediaSelected)); + return Center(child: Text(context.tr('noMediaSelected'))); } final media = medias[index]; @@ -615,7 +616,7 @@ class _PlayerLyrics extends HookConsumerWidget { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text(AppLocalizations.of(context)!.noLyricsAvailable), + Text(context.tr('noLyricsAvailable')), const SizedBox(height: 16), if (lyricsFetcher.isLoading) @@ -626,7 +627,7 @@ class _PlayerLyrics extends HookConsumerWidget { else ElevatedButton.icon( icon: const Icon(Symbols.download), - label: Text(AppLocalizations.of(context)!.fetchLyrics), + label: Text(context.tr('fetchLyrics')), onPressed: () => _showFetchLyricsDialog( context, ref, @@ -711,7 +712,7 @@ class _PlayerLyrics extends HookConsumerWidget { } } } catch (e) { - return Center(child: Text(AppLocalizations.of(context)!.errorLoadingLyrics(e.toString()))); + return Center(child: Text(context.tr('errorLoadingLyrics', args: [e.toString()]))); } } } @@ -743,7 +744,7 @@ class _FetchLyricsDialog extends StatelessWidget { .trim(); return AlertDialog( - title: Text(AppLocalizations.of(context)!.fetchLyrics), + title: Text(context.tr('fetchLyrics')), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -753,7 +754,7 @@ class _FetchLyricsDialog extends StatelessWidget { text: TextSpan( style: TextStyle(color: Theme.of(context).colorScheme.onSurface), children: [ - TextSpan(text: AppLocalizations.of(context)!.searchLyricsWith(searchTerm.split(' ').first)), + TextSpan(text: context.tr('searchLyricsWith', args: [searchTerm.split(' ').first])), TextSpan( text: ' $searchTerm', style: const TextStyle(fontWeight: FontWeight.bold), @@ -761,7 +762,7 @@ class _FetchLyricsDialog extends StatelessWidget { ], ), ), - Text(AppLocalizations.of(context)!.whereToSearchLyrics), + Text(context.tr('whereToSearchLyrics')), Card( child: Column( mainAxisSize: MainAxisSize.min, @@ -769,7 +770,7 @@ class _FetchLyricsDialog extends StatelessWidget { ListTile( dense: true, leading: const Icon(Symbols.library_music), - title: Text(AppLocalizations.of(context)!.musixmatch), + title: Text(context.tr('musixmatch')), shape: RoundedRectangleBorder( borderRadius: const BorderRadius.all(Radius.circular(12)), ), @@ -788,7 +789,7 @@ class _FetchLyricsDialog extends StatelessWidget { ListTile( dense: true, leading: const Icon(Symbols.music_video), - title: Text(AppLocalizations.of(context)!.netease), + title: Text(context.tr('netease')), shape: RoundedRectangleBorder( borderRadius: const BorderRadius.all(Radius.circular(12)), ), @@ -807,7 +808,7 @@ class _FetchLyricsDialog extends StatelessWidget { ListTile( dense: true, leading: const Icon(Symbols.library_books), - title: Text(AppLocalizations.of(context)!.lrclib), + title: Text(context.tr('lrclib')), shape: RoundedRectangleBorder( borderRadius: const BorderRadius.all(Radius.circular(12)), ), @@ -826,7 +827,7 @@ class _FetchLyricsDialog extends StatelessWidget { ListTile( dense: true, leading: const Icon(Symbols.file_upload), - title: Text(AppLocalizations.of(context)!.manualImport), + title: Text(context.tr('manualImport')), shape: RoundedRectangleBorder( borderRadius: const BorderRadius.all(Radius.circular(12)), ), @@ -843,7 +844,7 @@ class _FetchLyricsDialog extends StatelessWidget { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: Text(AppLocalizations.of(context)!.cancel), + child: Text(context.tr('cancel')), ), ], ); @@ -910,7 +911,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { return IconButton( icon: const Icon(Symbols.settings_applications), iconSize: 24, - tooltip: AppLocalizations.of(context)!.adjustLyricsTiming, + tooltip: context.tr('adjustLyricsTiming'), onPressed: () => _showLyricsRefreshDialog( context, ref, @@ -974,7 +975,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: Text(AppLocalizations.of(context)!.lyricsOptions), + title: Text(context.tr('lyricsOptions')), content: Column( spacing: 8, mainAxisSize: MainAxisSize.min, @@ -988,7 +989,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { Expanded( child: ElevatedButton.icon( icon: const Icon(Symbols.refresh), - label: Text(AppLocalizations.of(context)!.refetch), + label: Text(context.tr('refetch')), onPressed: () { Navigator.of(context).pop(); final metadata = metadataAsync.value; @@ -1008,7 +1009,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { Expanded( child: ElevatedButton.icon( icon: const Icon(Symbols.clear), - label: Text(AppLocalizations.of(context)!.clear), + label: Text(context.tr('clear')), style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, @@ -1060,7 +1061,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { width: double.infinity, child: ElevatedButton.icon( icon: const Icon(Symbols.sync), - label: Text(AppLocalizations.of(context)!.liveLyricsSync), + label: Text(context.tr('liveLyricsSync')), onPressed: () { Navigator.of(context).pop(); _showLiveLyricsSyncDialog( @@ -1077,7 +1078,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { width: double.infinity, child: ElevatedButton.icon( icon: const Icon(Symbols.tune), - label: Text(AppLocalizations.of(context)!.manualOffset), + label: Text(context.tr('manualOffset')), onPressed: () { Navigator.of(context).pop(); _showLyricsOffsetDialog( @@ -1096,7 +1097,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: Text(AppLocalizations.of(context)!.cancel), + child: Text(context.tr('cancel')), ), ], ), @@ -1116,17 +1117,17 @@ class _LyricsAdjustButton extends HookConsumerWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: Text(AppLocalizations.of(context)!.adjustLyricsTiming), + title: Text(context.tr('adjustLyricsTiming')), content: Column( mainAxisSize: MainAxisSize.min, children: [ Text( - AppLocalizations.of(context)!.enterOffsetMs, + context.tr('enterOffsetMs'), ), const SizedBox(height: 16), TextField( controller: offsetController, - decoration: InputDecoration(labelText: AppLocalizations.of(context)!.offsetMs), + decoration: InputDecoration(labelText: context.tr('offsetMs')), keyboardType: TextInputType.number, ), ], @@ -1134,7 +1135,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: Text(AppLocalizations.of(context)!.cancel), + child: Text(context.tr('cancel')), ), TextButton( onPressed: () async { @@ -1196,11 +1197,11 @@ class _ViewToggleButton extends StatelessWidget { String getTooltip() { switch (viewMode.value) { case ViewMode.cover: - return AppLocalizations.of(context)!.showLyrics; + return context.tr('showLyrics'); case ViewMode.lyrics: - return AppLocalizations.of(context)!.showQueue; + return context.tr('showQueue'); case ViewMode.queue: - return AppLocalizations.of(context)!.showCover; + return context.tr('showCover'); } } @@ -1251,7 +1252,7 @@ class _QueueView extends HookConsumerWidget { builder: (context, snapshot) { final playlist = snapshot.data; if (playlist == null || playlist.medias.isEmpty) { - return Center(child: Text(AppLocalizations.of(context)!.noTracksInQueue)); + return Center(child: Text(context.tr('noTracksInQueue'))); } return ReorderableListView.builder( @@ -1919,7 +1920,7 @@ class _LiveLyricsSyncDialog extends HookConsumerWidget { return Scaffold( appBar: AppBar( - title: Text(AppLocalizations.of(context)!.liveLyricsSync), + title: Text(context.tr('liveLyricsSync')), leading: IconButton( icon: const Icon(Symbols.close), onPressed: () => Navigator.of(context).pop(), @@ -1977,7 +1978,7 @@ class _LiveLyricsSyncDialog extends HookConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text(AppLocalizations.of(context)!.offset(tempOffset.value)), + Text(context.tr('offset', args: [tempOffset.value.toString()])), ], ), ), @@ -1993,30 +1994,30 @@ class _LiveLyricsSyncDialog extends HookConsumerWidget { children: [ ElevatedButton.icon( icon: const Icon(Symbols.fast_rewind), - label: Text(AppLocalizations.of(context)!.minus100ms), + label: Text(context.tr('minus100ms')), onPressed: () => tempOffset.value = (tempOffset.value - 100), ), ElevatedButton.icon( icon: const Icon(Symbols.skip_previous), - label: Text(AppLocalizations.of(context)!.plus10ms), + label: Text(context.tr('plus10ms')), onPressed: () => tempOffset.value = (tempOffset.value - 10), ), ElevatedButton.icon( icon: const Icon(Symbols.refresh), - label: Text(AppLocalizations.of(context)!.reset), + label: Text(context.tr('reset')), onPressed: () => tempOffset.value = 0, ), ElevatedButton.icon( icon: const Icon(Symbols.skip_next), - label: Text(AppLocalizations.of(context)!.plus10ms), + label: Text(context.tr('plus10ms')), onPressed: () => tempOffset.value = (tempOffset.value + 10), ), ElevatedButton.icon( icon: const Icon(Symbols.fast_forward), - label: Text(AppLocalizations.of(context)!.plus100ms), + label: Text(context.tr('plus100ms')), onPressed: () => tempOffset.value = (tempOffset.value + 100), ), @@ -2029,7 +2030,7 @@ class _LiveLyricsSyncDialog extends HookConsumerWidget { padding: const EdgeInsets.all(16.0), child: Column( children: [ - Text(AppLocalizations.of(context)!.fineAdjustment), + Text(context.tr('fineAdjustment')), Slider( value: tempOffset.value.toDouble().clamp(-5000.0, 5000.0), min: -5000, @@ -2173,7 +2174,7 @@ class _LiveLyricsPreview extends HookConsumerWidget { final lyricsData = LyricsData.fromJsonString(track.lyrics!); if (lyricsData.type != 'timed') { - return Center(child: Text(AppLocalizations.of(context)!.onlyTimedLyricsCanBeSynced)); + return Center(child: Text(context.tr('onlyTimedLyricsCanBeSynced'))); } return StreamBuilder( @@ -2254,7 +2255,7 @@ class _LiveLyricsPreview extends HookConsumerWidget { }, ); } catch (e) { - return Center(child: Text(AppLocalizations.of(context)!.errorLoadingLyrics(e.toString()))); + return Center(child: Text(context.tr('errorLoadingLyrics', args: [e.toString()]))); } } } diff --git a/lib/ui/screens/playlist_detail_screen.dart b/lib/ui/screens/playlist_detail_screen.dart index 4bbe8bf..b867d82 100644 --- a/lib/ui/screens/playlist_detail_screen.dart +++ b/lib/ui/screens/playlist_detail_screen.dart @@ -1,10 +1,11 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:gap/gap.dart'; import 'package:groovybox/data/db.dart'; import 'package:groovybox/data/playlist_repository.dart'; -import 'package:groovybox/l10n/app_localizations.dart'; + import 'package:groovybox/providers/audio_provider.dart'; import 'package:groovybox/ui/widgets/track_tile.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -52,7 +53,7 @@ class PlaylistDetailScreen extends HookConsumerWidget { return SizedBox( height: 200, child: Center( - child: Text(AppLocalizations.of(context)!.noTracksInPlaylist), + child: Text(context.tr('noTracksInPlaylist')), ), ); } @@ -74,7 +75,7 @@ class PlaylistDetailScreen extends HookConsumerWidget { _playPlaylist(ref, tracks); }, icon: const Icon(Symbols.play_arrow), - label: Text(AppLocalizations.of(context)!.playAll), + label: Text(context.tr('playAll')), ), ), SizedBox( @@ -84,7 +85,7 @@ class PlaylistDetailScreen extends HookConsumerWidget { _addToQueue(ref, tracks); }, icon: const Icon(Symbols.queue_music), - label: Text(AppLocalizations.of(context)!.addToQueue), + label: Text(context.tr('addToQueue')), ), ), ], @@ -205,3 +206,5 @@ class PlaylistDetailScreen extends HookConsumerWidget { ); } } + + diff --git a/lib/ui/screens/settings_screen.dart b/lib/ui/screens/settings_screen.dart index 954e7bf..9429dcd 100644 --- a/lib/ui/screens/settings_screen.dart +++ b/lib/ui/screens/settings_screen.dart @@ -1,7 +1,8 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:groovybox/data/track_repository.dart'; -import 'package:groovybox/l10n/app_localizations.dart'; + import 'package:groovybox/providers/locale_provider.dart'; import 'package:groovybox/providers/settings_provider.dart'; import 'package:groovybox/providers/watch_folder_provider.dart'; @@ -21,7 +22,7 @@ class SettingsScreen extends ConsumerWidget { final remoteProvidersAsync = ref.watch(remoteProvidersProvider); return Scaffold( - appBar: AppBar(title: Text(AppLocalizations.of(context)!.settingsTitle)), + appBar: AppBar(title: Text(context.tr('settingsTitle'))), body: settingsAsync.when( data: (settings) => Align( alignment: Alignment.topCenter, @@ -40,16 +41,16 @@ class SettingsScreen extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - AppLocalizations.of(context)!.autoScan, + context.tr('autoScan'), style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ).padding(horizontal: 16, top: 16), SwitchListTile( - title: Text(AppLocalizations.of(context)!.autoScanMusicLibraries), + title: Text(context.tr('autoScanMusicLibraries')), subtitle: Text( - AppLocalizations.of(context)!.autoScanDescription, + context.tr('autoScanDescription'), ), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(8)), @@ -60,9 +61,9 @@ class SettingsScreen extends ConsumerWidget { }, ), SwitchListTile( - title: Text(AppLocalizations.of(context)!.watchForChanges), + title: Text(context.tr('watchForChanges')), subtitle: Text( - AppLocalizations.of(context)!.watchForChangesDescription, + context.tr('watchForChangesDescription'), ), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(8)), @@ -92,7 +93,7 @@ class SettingsScreen extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - AppLocalizations.of(context)!.musicLibraries, + context.tr('musicLibraries'), style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -104,7 +105,7 @@ class SettingsScreen extends ConsumerWidget { onPressed: () => _scanLibraries(context, ref), icon: const Icon(Symbols.refresh), - tooltip: AppLocalizations.of(context)!.scanLibraries, + tooltip: context.tr('scanLibraries'), visualDensity: const VisualDensity( horizontal: -4, vertical: -4, @@ -114,7 +115,7 @@ class SettingsScreen extends ConsumerWidget { onPressed: () => _addMusicLibrary(context, ref), icon: const Icon(Symbols.add), - tooltip: AppLocalizations.of(context)!.addMusicLibrary, + tooltip: context.tr('addMusicLibrary'), visualDensity: const VisualDensity( horizontal: -4, vertical: -4, @@ -125,7 +126,7 @@ class SettingsScreen extends ConsumerWidget { ], ), Text( - AppLocalizations.of(context)!.addMusicLibraryDescription, + context.tr('addMusicLibraryDescription'), style: const TextStyle( color: Colors.grey, fontSize: 14, @@ -136,7 +137,7 @@ class SettingsScreen extends ConsumerWidget { watchFoldersAsync.when( data: (folders) => folders.isEmpty ? Text( - AppLocalizations.of(context)!.noMusicLibrariesAdded, + context.tr('noMusicLibrariesAdded'), style: const TextStyle( color: Colors.grey, fontSize: 14, @@ -210,7 +211,7 @@ class SettingsScreen extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - AppLocalizations.of(context)!.remoteProviders, + context.tr('remoteProviders'), style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -222,7 +223,7 @@ class SettingsScreen extends ConsumerWidget { onPressed: () => _indexRemoteProviders(context, ref), icon: const Icon(Symbols.refresh), - tooltip: AppLocalizations.of(context)!.indexRemoteProviders, + tooltip: context.tr('indexRemoteProviders'), visualDensity: const VisualDensity( horizontal: -4, vertical: -4, @@ -232,7 +233,7 @@ class SettingsScreen extends ConsumerWidget { onPressed: () => _addRemoteProvider(context, ref), icon: const Icon(Symbols.add), - tooltip: AppLocalizations.of(context)!.addRemoteProvider, + tooltip: context.tr('addRemoteProvider'), visualDensity: const VisualDensity( horizontal: -4, vertical: -4, @@ -243,7 +244,7 @@ class SettingsScreen extends ConsumerWidget { ], ), Text( - AppLocalizations.of(context)!.remoteProvidersDescription, + context.tr('remoteProvidersDescription'), style: const TextStyle( color: Colors.grey, fontSize: 14, @@ -254,7 +255,7 @@ class SettingsScreen extends ConsumerWidget { remoteProvidersAsync.when( data: (providers) => providers.isEmpty ? Text( - AppLocalizations.of(context)!.noRemoteProvidersAdded, + context.tr('noRemoteProvidersAdded'), style: const TextStyle( color: Colors.grey, fontSize: 14, @@ -322,20 +323,20 @@ class SettingsScreen extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - AppLocalizations.of(context)!.playerSettings, + context.tr('playerSettings'), style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ).padding(horizontal: 16, top: 16), Text( - AppLocalizations.of(context)!.playerSettingsDescription, + context.tr('playerSettingsDescription'), style: const TextStyle(color: Colors.grey, fontSize: 14), ).padding(horizontal: 16, bottom: 8), ListTile( - title: Text(AppLocalizations.of(context)!.defaultPlayerScreen), + title: Text(context.tr('defaultPlayerScreen')), subtitle: Text( - AppLocalizations.of(context)!.defaultPlayerScreenDescription, + context.tr('defaultPlayerScreenDescription'), ), trailing: DropdownButtonHideUnderline( child: DropdownButton( @@ -359,9 +360,9 @@ class SettingsScreen extends ConsumerWidget { ), ), ListTile( - title: Text(AppLocalizations.of(context)!.lyricsMode), + title: Text(context.tr('lyricsMode')), subtitle: Text( - AppLocalizations.of(context)!.lyricsModeDescription, + context.tr('lyricsModeDescription'), ), trailing: DropdownButtonHideUnderline( child: DropdownButton( @@ -383,9 +384,9 @@ class SettingsScreen extends ConsumerWidget { ), ), SwitchListTile( - title: Text(AppLocalizations.of(context)!.continuePlaying), + title: Text(context.tr('continuePlaying')), subtitle: Text( - AppLocalizations.of(context)!.continuePlayingDescription, + context.tr('continuePlayingDescription'), ), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(8)), @@ -409,27 +410,27 @@ class SettingsScreen extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - AppLocalizations.of(context)!.appSettings, + context.tr('appSettings'), style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ).padding(horizontal: 16, top: 16), Text( - AppLocalizations.of(context)!.appSettingsDescription, + context.tr('appSettingsDescription'), style: const TextStyle(color: Colors.grey, fontSize: 14), ).padding(horizontal: 16, bottom: 8), ListTile( - title: Text(AppLocalizations.of(context)!.language), + title: Text(context.tr('language')), subtitle: Text( - AppLocalizations.of(context)!.languageDescription, + context.tr('languageDescription'), ), trailing: DropdownButtonHideUnderline( child: DropdownButton( value: ref.watch(localeProvider), onChanged: (Locale? value) { if (value != null) { - ref.read(localeProvider.notifier).setLocale(value); + ref.read(localeProvider.notifier).setLocale(context, value); } }, items: const [ @@ -457,20 +458,20 @@ class SettingsScreen extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - AppLocalizations.of(context)!.databaseManagement, + context.tr('databaseManagement'), style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ).padding(horizontal: 16, top: 16), Text( - AppLocalizations.of(context)!.databaseManagementDescription, + context.tr('databaseManagementDescription'), style: const TextStyle(color: Colors.grey, fontSize: 14), ).padding(horizontal: 16, bottom: 8), ListTile( - title: Text(AppLocalizations.of(context)!.resetTrackDatabase), + title: Text(context.tr('resetTrackDatabase')), subtitle: Text( - AppLocalizations.of(context)!.resetTrackDatabaseDescription, + context.tr('resetTrackDatabaseDescription'), ), trailing: ElevatedButton( onPressed: () => _resetTrackDatabase(context, ref), @@ -478,7 +479,7 @@ class SettingsScreen extends ConsumerWidget { backgroundColor: Colors.red, foregroundColor: Colors.white, ), - child: Text(AppLocalizations.of(context)!.reset), + child: Text(context.tr('reset')), ), ), const SizedBox(height: 8), @@ -507,14 +508,14 @@ class SettingsScreen extends ConsumerWidget { await service.addWatchFolder(path, recursive: true); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(AppLocalizations.of(context)!.addedMusicLibrary(path))), + SnackBar(content: Text(context.tr('addedMusicLibrary', args: [path]))), ); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of( context, - ).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.errorAddingLibrary(e.toString())))); + ).showSnackBar(SnackBar(content: Text(context.tr('errorAddingLibrary', args: [e.toString()])))); } } } @@ -527,14 +528,14 @@ class SettingsScreen extends ConsumerWidget { await service.scanWatchFolders(); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(AppLocalizations.of(context)!.librariesScannedSuccessfully)), + SnackBar(content: Text(context.tr('librariesScannedSuccessfully'))), ); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of( context, - ).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.errorScanningLibraries(e.toString())))); + ).showSnackBar(SnackBar(content: Text(context.tr('errorScanningLibraries', args: [e.toString()])))); } } } @@ -552,7 +553,7 @@ class SettingsScreen extends ConsumerWidget { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(AppLocalizations.of(context)!.noActiveRemoteProviders), + content: Text(context.tr('noActiveRemoteProviders')), ), ); } @@ -571,7 +572,7 @@ class SettingsScreen extends ConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - AppLocalizations.of(context)!.indexedRemoteProviders(activeProviders.length), + context.tr('indexedRemoteProviders', args: [activeProviders.length.toString()]), ), ), ); @@ -605,27 +606,27 @@ class SettingsScreen extends ConsumerWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: Text(AppLocalizations.of(context)!.addRemoteProviderDialog), + title: Text(context.tr('addRemoteProviderDialog')), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: serverUrlController, decoration: InputDecoration( - labelText: AppLocalizations.of(context)!.serverUrl, - hintText: AppLocalizations.of(context)!.serverUrlHint, + labelText: context.tr('serverUrl'), + hintText: context.tr('serverUrlHint'), ), keyboardType: TextInputType.url, ), const SizedBox(height: 16), TextField( controller: usernameController, - decoration: InputDecoration(labelText: AppLocalizations.of(context)!.username), + decoration: InputDecoration(labelText: context.tr('username')), ), const SizedBox(height: 16), TextField( controller: passwordController, - decoration: InputDecoration(labelText: AppLocalizations.of(context)!.password), + decoration: InputDecoration(labelText: context.tr('password')), obscureText: true, ), ], @@ -633,7 +634,7 @@ class SettingsScreen extends ConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: Text(AppLocalizations.of(context)!.cancel), + child: Text(context.tr('cancel')), ), TextButton( onPressed: () async { @@ -643,7 +644,7 @@ class SettingsScreen extends ConsumerWidget { if (serverUrl.isEmpty || username.isEmpty || password.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(AppLocalizations.of(context)!.allFieldsRequired)), + SnackBar(content: Text(context.tr('allFieldsRequired'))), ); return; } @@ -655,19 +656,19 @@ class SettingsScreen extends ConsumerWidget { Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(AppLocalizations.of(context)!.addedRemoteProvider(serverUrl)), + content: Text(context.tr('addedRemoteProvider', args: [serverUrl])), ), ); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(AppLocalizations.of(context)!.errorAddingProvider(e.toString()))), + SnackBar(content: Text(context.tr('errorAddingProvider', args: [e.toString()]))), ); } } }, - child: Text(AppLocalizations.of(context)!.add), + child: Text(context.tr('add')), ), ], ), @@ -678,14 +679,14 @@ class SettingsScreen extends ConsumerWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: Text(AppLocalizations.of(context)!.resetTrackDatabase), + title: Text(context.tr('resetTrackDatabase')), content: Text( - AppLocalizations.of(context)!.confirmResetTrackDatabase, + context.tr('confirmResetTrackDatabase'), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: Text(AppLocalizations.of(context)!.cancel), + child: Text(context.tr('cancel')), ), TextButton( onPressed: () async { @@ -698,20 +699,20 @@ class SettingsScreen extends ConsumerWidget { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(AppLocalizations.of(context)!.trackDatabaseReset), + content: Text(context.tr('trackDatabaseReset')), ), ); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(AppLocalizations.of(context)!.errorResettingDatabase(e.toString()))), + SnackBar(content: Text(context.tr('errorResettingDatabase', args: [e.toString()]))), ); } } }, style: TextButton.styleFrom(foregroundColor: Colors.red), - child: Text(AppLocalizations.of(context)!.reset), + child: Text(context.tr('reset')), ), ], ), diff --git a/lib/ui/tabs/albums_tab.dart b/lib/ui/tabs/albums_tab.dart index 56e5396..adb3b64 100644 --- a/lib/ui/tabs/albums_tab.dart +++ b/lib/ui/tabs/albums_tab.dart @@ -1,7 +1,8 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:animations/animations.dart'; import 'package:flutter/material.dart'; import 'package:groovybox/data/playlist_repository.dart'; -import 'package:groovybox/l10n/app_localizations.dart'; + import 'package:groovybox/ui/screens/album_detail_screen.dart'; import 'package:groovybox/ui/widgets/universal_image.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -23,7 +24,7 @@ class AlbumsTab extends HookConsumerWidget { final albums = snapshot.data!; if (albums.isEmpty) { - return Center(child: Text(AppLocalizations.of(context)!.noAlbumsFound)); + return Center(child: Text(context.tr('noAlbumsFound'))); } return GridView.builder( @@ -88,3 +89,5 @@ class AlbumsTab extends HookConsumerWidget { ); } } + + diff --git a/lib/ui/tabs/playlists_tab.dart b/lib/ui/tabs/playlists_tab.dart index 9548007..879053b 100644 --- a/lib/ui/tabs/playlists_tab.dart +++ b/lib/ui/tabs/playlists_tab.dart @@ -1,7 +1,8 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:groovybox/data/db.dart'; import 'package:groovybox/data/playlist_repository.dart'; -import 'package:groovybox/l10n/app_localizations.dart'; + import 'package:groovybox/ui/screens/playlist_detail_screen.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -20,30 +21,30 @@ class PlaylistsTab extends HookConsumerWidget { ListTile( leading: const Icon(Symbols.add), trailing: const Icon(Symbols.chevron_right).padding(right: 8), - title: Text(AppLocalizations.of(context)!.createOne), - subtitle: Text(AppLocalizations.of(context)!.addNewPlaylist), + title: Text(context.tr('createOne')), + subtitle: Text(context.tr('addNewPlaylist')), onTap: () async { final nameController = TextEditingController(); final name = await showDialog( context: context, builder: (context) => AlertDialog( - title: Text(AppLocalizations.of(context)!.newPlaylist), + title: Text(context.tr('newPlaylist')), content: TextField( controller: nameController, decoration: InputDecoration( - labelText: AppLocalizations.of(context)!.playlistName, + labelText: context.tr('playlistName'), ), autofocus: true, ), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.cancel), + child: Text(context.tr('cancel')), ), TextButton( onPressed: () => Navigator.pop(context, nameController.text), - child: Text(AppLocalizations.of(context)!.create), + child: Text(context.tr('create')), ), ], ), @@ -64,7 +65,7 @@ class PlaylistsTab extends HookConsumerWidget { final playlists = snapshot.data!; if (playlists.isEmpty) { - return Center(child: Text(AppLocalizations.of(context)!.noPlaylistsYet)); + return Center(child: Text(context.tr('noPlaylistsYet'))); } return ListView.builder( @@ -75,7 +76,7 @@ class PlaylistsTab extends HookConsumerWidget { leading: const Icon(Symbols.queue_music), title: Text(playlist.name), subtitle: Text( - '${AppLocalizations.of(context)!.createdAt} ${playlist.createdAt.day}/${playlist.createdAt.month}/${playlist.createdAt.year}', + '${context.tr('createdAt')} ${playlist.createdAt.day}/${playlist.createdAt.month}/${playlist.createdAt.year}', ), trailing: IconButton( icon: const Icon(Symbols.delete), @@ -101,3 +102,5 @@ class PlaylistsTab extends HookConsumerWidget { ); } } + + diff --git a/lib/ui/widgets/mini_player.dart b/lib/ui/widgets/mini_player.dart index c99d225..775be89 100644 --- a/lib/ui/widgets/mini_player.dart +++ b/lib/ui/widgets/mini_player.dart @@ -1,7 +1,8 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:groovybox/data/db.dart' as db; -import 'package:groovybox/l10n/app_localizations.dart'; + import 'package:groovybox/providers/audio_provider.dart'; import 'package:groovybox/ui/screens/player_screen.dart'; import 'package:groovybox/ui/widgets/track_tile.dart'; @@ -174,7 +175,7 @@ class _MobileMiniPlayer extends HookConsumerWidget { overflow: TextOverflow.ellipsis, ), Text( - currentMetadata?.artist ?? AppLocalizations.of(context)!.unknownArtist, + currentMetadata?.artist ?? context.tr('unknownArtist'), style: Theme.of(context).textTheme.bodySmall, maxLines: 1, overflow: TextOverflow.ellipsis, @@ -408,7 +409,7 @@ class _DesktopMiniPlayer extends HookConsumerWidget { overflow: TextOverflow.ellipsis, ), Text( - currentMetadata?.artist ?? AppLocalizations.of(context)!.unknownArtist, + currentMetadata?.artist ?? context.tr('unknownArtist'), style: Theme.of( context, ).textTheme.bodySmall, @@ -604,7 +605,7 @@ class _DesktopMiniPlayer extends HookConsumerWidget { padding: const EdgeInsets.fromLTRB(24, 12, 24, 8), child: Row( children: [ - Text(AppLocalizations.of(context)!.queue, style: TextStyle(fontSize: 20)), + Text(context.tr('queue'), style: TextStyle(fontSize: 20)), const Spacer(), IconButton( icon: const Icon(Symbols.close), @@ -621,7 +622,7 @@ class _DesktopMiniPlayer extends HookConsumerWidget { builder: (context, snapshot) { final playlist = snapshot.data; if (playlist == null || playlist.medias.isEmpty) { - return Center(child: Text(AppLocalizations.of(context)!.noTracksInQueue)); + return Center(child: Text(context.tr('noTracksInQueue'))); } return ReorderableListView.builder( @@ -678,7 +679,7 @@ class _DesktopMiniPlayer extends HookConsumerWidget { title: Uri.parse(media.uri).pathSegments.last, artist: media.extras?['artist'] as String? ?? - AppLocalizations.of(context)!.unknownArtist, + context.tr('unknownArtist'), album: media.extras?['album'] as String?, duration: null, artUri: null, @@ -727,7 +728,7 @@ class _DesktopMiniPlayer extends HookConsumerWidget { ).pathSegments.last, artist: media.extras?['artist'] as String? ?? - AppLocalizations.of(context)!.unknownArtist, + context.tr('unknownArtist'), album: media.extras?['album'] as String?, duration: null, artUri: null, @@ -756,3 +757,5 @@ class _DesktopMiniPlayer extends HookConsumerWidget { ); } } + + diff --git a/lib/ui/widgets/track_tile.dart b/lib/ui/widgets/track_tile.dart index 75d1f6a..4fb1fe6 100644 --- a/lib/ui/widgets/track_tile.dart +++ b/lib/ui/widgets/track_tile.dart @@ -1,6 +1,6 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:groovybox/data/db.dart' as db; -import 'package:groovybox/l10n/app_localizations.dart'; import 'package:groovybox/ui/widgets/universal_image.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -49,7 +49,7 @@ class TrackTile extends StatelessWidget { leading: Row( mainAxisSize: MainAxisSize.min, children: [ - ?leading, + if (leading != null) leading!, AspectRatio( aspectRatio: 1, child: UniversalImage( @@ -74,7 +74,7 @@ class TrackTile extends StatelessWidget { ), ), subtitle: Text( - '${track.artist ?? AppLocalizations.of(context)!.unknownArtist} • ${_formatDuration(track.duration)}', + '${track.artist ?? context.tr('unknownArtist')} ?${_formatDuration(track.duration)}', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( From 6fcac1cf12086bea92b84090209df426f7ea2caa Mon Sep 17 00:00:00 2001 From: liang-work Date: Thu, 8 Jan 2026 22:04:38 +0800 Subject: [PATCH 11/16] =?UTF-8?q?=F0=9F=90=9B=20Fix=20the=20issue=20where?= =?UTF-8?q?=20build=5Frunner=20cannot=20run?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main.dart | 14 -------------- lib/providers/locale_provider.g.dart | 2 +- lib/ui/screens/library_screen.dart | 4 ++-- lib/ui/widgets/track_tile.dart | 4 ++-- 4 files changed, 5 insertions(+), 19 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 7eb52e5..271dab1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:groovybox/logic/audio_handler.dart'; import 'package:groovybox/logic/window_helpers.dart'; import 'package:groovybox/providers/audio_provider.dart'; -import 'package:groovybox/providers/locale_provider.dart'; import 'package:groovybox/providers/theme_provider.dart'; import 'package:groovybox/router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -67,30 +66,17 @@ class _GroovyAppState extends ConsumerState { @override Widget build(BuildContext context) { final themeMode = ref.watch(themeProvider); - final locale = ref.watch(localeProvider); final router = ref.watch(routerProvider); - // Listen to locale changes and force rebuild when locale changes - ref.listen(localeProvider, (previous, next) { - if (previous != next) { - // Force rebuild of the entire app when locale changes - WidgetsBinding.instance.addPostFrameCallback((_) { - setState(() {}); - }); - } - }); - return MaterialApp.router( title: 'GroovyBox', debugShowCheckedModeBanner: false, theme: ref.watch(lightThemeProvider), darkTheme: ref.watch(darkThemeProvider), themeMode: themeMode, - locale: locale, routerConfig: router, localizationsDelegates: context.localizationDelegates, supportedLocales: context.supportedLocales, ); } } - diff --git a/lib/providers/locale_provider.g.dart b/lib/providers/locale_provider.g.dart index a25e06d..26c47a9 100644 --- a/lib/providers/locale_provider.g.dart +++ b/lib/providers/locale_provider.g.dart @@ -41,7 +41,7 @@ final class LocaleNotifierProvider } } -String _$localeNotifierHash() => r'838dee78faf52f7e5a7a9c2dc6d666f49e9553ee'; +String _$localeNotifierHash() => r'60db537ea3a9685b0eb20df4d15da07c9c024f3b'; abstract class _$LocaleNotifier extends $Notifier { Locale build(); diff --git a/lib/ui/screens/library_screen.dart b/lib/ui/screens/library_screen.dart index 55e5051..30670e2 100644 --- a/lib/ui/screens/library_screen.dart +++ b/lib/ui/screens/library_screen.dart @@ -493,7 +493,7 @@ class LibraryScreen extends HookConsumerWidget { overflow: TextOverflow.ellipsis, ), subtitle: Text( - '${track.artist ?? context.tr('unknownArtist')} ?${_formatDuration(track.duration)}', + '${track.artist ?? context.tr('unknownArtist')} - ${_formatDuration(track.duration)}', maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -1131,4 +1131,4 @@ class LibraryScreen extends HookConsumerWidget { ), ); } -} +} \ No newline at end of file diff --git a/lib/ui/widgets/track_tile.dart b/lib/ui/widgets/track_tile.dart index 4fb1fe6..03c72f1 100644 --- a/lib/ui/widgets/track_tile.dart +++ b/lib/ui/widgets/track_tile.dart @@ -74,7 +74,7 @@ class TrackTile extends StatelessWidget { ), ), subtitle: Text( - '${track.artist ?? context.tr('unknownArtist')} ?${_formatDuration(track.duration)}', + '${track.artist ?? context.tr('unknownArtist')} - ${_formatDuration(track.duration)}', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( @@ -97,4 +97,4 @@ class TrackTile extends StatelessWidget { ), ); } -} +} \ No newline at end of file From f8a6205d3e2ff40ef2190fa033befad37f2d1f21 Mon Sep 17 00:00:00 2001 From: liang-work Date: Thu, 8 Jan 2026 22:37:54 +0800 Subject: [PATCH 12/16] =?UTF-8?q?=F0=9F=90=9B=20Fix=20the=20issue=20where?= =?UTF-8?q?=20the=20currently=20set=20language=20resets=20after=20restarti?= =?UTF-8?q?ng=20the=20app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/logic/audio_handler.dart | 2 +- lib/providers/locale_provider.dart | 23 +++++++------- lib/providers/locale_provider.g.dart | 22 +++++--------- lib/ui/screens/settings_screen.dart | 45 +++++++++++++++------------- lib/ui/widgets/track_tile.dart | 4 +-- 5 files changed, 48 insertions(+), 48 deletions(-) diff --git a/lib/logic/audio_handler.dart b/lib/logic/audio_handler.dart index 4ea95a8..c9819cd 100644 --- a/lib/logic/audio_handler.dart +++ b/lib/logic/audio_handler.dart @@ -54,7 +54,7 @@ class AudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { .when( data: (settings) => settings.continuePlays, loading: () => false, - error: (_, __) => false, + error: (_, _) => false, ); if (continuePlays && _queueIndex == _queue.length - 1) { diff --git a/lib/providers/locale_provider.dart b/lib/providers/locale_provider.dart index d12f291..9f68d79 100644 --- a/lib/providers/locale_provider.dart +++ b/lib/providers/locale_provider.dart @@ -1,27 +1,30 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; part 'locale_provider.g.dart'; @Riverpod(keepAlive: true) class LocaleNotifier extends _$LocaleNotifier { @override - Locale build() { - return const Locale('en'); // Default to English + Future build() async { + final prefs = await SharedPreferences.getInstance(); + final localeString = prefs.getString('locale') ?? 'en'; + return Locale(localeString); } - Future setLocale(BuildContext context, Locale locale) async { - await context.setLocale(locale); - state = locale; + Future setLocale(Locale locale) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('locale', locale.languageCode); + ref.invalidateSelf(); } - Future setEnglish(BuildContext context) async { - await setLocale(context, const Locale('en')); + Future setEnglish() async { + await setLocale(const Locale('en')); } - Future setChinese(BuildContext context) async { - await setLocale(context, const Locale('zh')); + Future setChinese() async { + await setLocale(const Locale('zh')); } } diff --git a/lib/providers/locale_provider.g.dart b/lib/providers/locale_provider.g.dart index 26c47a9..8ec1993 100644 --- a/lib/providers/locale_provider.g.dart +++ b/lib/providers/locale_provider.g.dart @@ -13,7 +13,7 @@ part of 'locale_provider.dart'; final localeProvider = LocaleNotifierProvider._(); final class LocaleNotifierProvider - extends $NotifierProvider { + extends $AsyncNotifierProvider { LocaleNotifierProvider._() : super( from: null, @@ -31,29 +31,21 @@ final class LocaleNotifierProvider @$internal @override LocaleNotifier create() => LocaleNotifier(); - - /// {@macro riverpod.override_with_value} - Override overrideWithValue(Locale value) { - return $ProviderOverride( - origin: this, - providerOverride: $SyncValueProvider(value), - ); - } } -String _$localeNotifierHash() => r'60db537ea3a9685b0eb20df4d15da07c9c024f3b'; +String _$localeNotifierHash() => r'1da62d4650ef0dd2f6a331033cf991796e673035'; -abstract class _$LocaleNotifier extends $Notifier { - Locale build(); +abstract class _$LocaleNotifier extends $AsyncNotifier { + FutureOr build(); @$mustCallSuper @override void runBuild() { - final ref = this.ref as $Ref; + final ref = this.ref as $Ref, Locale>; final element = ref.element as $ClassProviderElement< - AnyNotifier, - Locale, + AnyNotifier, Locale>, + AsyncValue, Object?, Object? >; diff --git a/lib/ui/screens/settings_screen.dart b/lib/ui/screens/settings_screen.dart index 9429dcd..607a5ee 100644 --- a/lib/ui/screens/settings_screen.dart +++ b/lib/ui/screens/settings_screen.dart @@ -420,31 +420,36 @@ class SettingsScreen extends ConsumerWidget { context.tr('appSettingsDescription'), style: const TextStyle(color: Colors.grey, fontSize: 14), ).padding(horizontal: 16, bottom: 8), - ListTile( - title: Text(context.tr('language')), - subtitle: Text( - context.tr('languageDescription'), - ), - trailing: DropdownButtonHideUnderline( - child: DropdownButton( - value: ref.watch(localeProvider), - onChanged: (Locale? value) { + ref.watch(localeProvider).when( + data: (locale) => ListTile( + title: Text(context.tr('language')), + subtitle: Text( + context.tr('languageDescription'), + ), + trailing: DropdownButtonHideUnderline( + child: DropdownButton( + value: locale, + onChanged: (Locale? value) async { if (value != null) { - ref.read(localeProvider.notifier).setLocale(context, value); + await context.setLocale(value); + await ref.read(localeProvider.notifier).setLocale(value); } }, - items: const [ - DropdownMenuItem( - value: Locale('en'), - child: Text('English'), - ), - DropdownMenuItem( - value: Locale('zh'), - child: Text('中文'), - ), - ], + items: const [ + DropdownMenuItem( + value: Locale('en'), + child: Text('English'), + ), + DropdownMenuItem( + value: Locale('zh'), + child: Text('中文'), + ), + ], + ), ), ), + loading: () => const CircularProgressIndicator(), + error: (error, _) => Text('Error loading locale: $error'), ), const SizedBox(height: 8), ], diff --git a/lib/ui/widgets/track_tile.dart b/lib/ui/widgets/track_tile.dart index 03c72f1..bdc0612 100644 --- a/lib/ui/widgets/track_tile.dart +++ b/lib/ui/widgets/track_tile.dart @@ -40,7 +40,7 @@ class TrackTile extends StatelessWidget { return Container( decoration: BoxDecoration( color: isPlaying - ? Theme.of(context).colorScheme.primary.withOpacity(0.2) + ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.2) : Colors.transparent, borderRadius: BorderRadius.circular(8), ), @@ -78,7 +78,7 @@ class TrackTile extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7), ), ), trailing: showTrailingIcon From f4037eee632bd025d94509134677811faccdfe82 Mon Sep 17 00:00:00 2001 From: liang-work Date: Fri, 9 Jan 2026 19:37:01 +0800 Subject: [PATCH 13/16] =?UTF-8?q?=F0=9F=94=A5=20delete=20old=20files=20abo?= =?UTF-8?q?ut=20locale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/providers/locale_provider.dart | 32 ----------------- lib/providers/locale_provider.g.dart | 54 ---------------------------- 2 files changed, 86 deletions(-) delete mode 100644 lib/providers/locale_provider.dart delete mode 100644 lib/providers/locale_provider.g.dart diff --git a/lib/providers/locale_provider.dart b/lib/providers/locale_provider.dart deleted file mode 100644 index 9f68d79..0000000 --- a/lib/providers/locale_provider.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -part 'locale_provider.g.dart'; - -@Riverpod(keepAlive: true) -class LocaleNotifier extends _$LocaleNotifier { - @override - Future build() async { - final prefs = await SharedPreferences.getInstance(); - final localeString = prefs.getString('locale') ?? 'en'; - return Locale(localeString); - } - - Future setLocale(Locale locale) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setString('locale', locale.languageCode); - ref.invalidateSelf(); - } - - Future setEnglish() async { - await setLocale(const Locale('en')); - } - - Future setChinese() async { - await setLocale(const Locale('zh')); - } -} - - - diff --git a/lib/providers/locale_provider.g.dart b/lib/providers/locale_provider.g.dart deleted file mode 100644 index 8ec1993..0000000 --- a/lib/providers/locale_provider.g.dart +++ /dev/null @@ -1,54 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'locale_provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint, type=warning - -@ProviderFor(LocaleNotifier) -final localeProvider = LocaleNotifierProvider._(); - -final class LocaleNotifierProvider - extends $AsyncNotifierProvider { - LocaleNotifierProvider._() - : super( - from: null, - argument: null, - retry: null, - name: r'localeProvider', - isAutoDispose: false, - dependencies: null, - $allTransitiveDependencies: null, - ); - - @override - String debugGetCreateSourceHash() => _$localeNotifierHash(); - - @$internal - @override - LocaleNotifier create() => LocaleNotifier(); -} - -String _$localeNotifierHash() => r'1da62d4650ef0dd2f6a331033cf991796e673035'; - -abstract class _$LocaleNotifier extends $AsyncNotifier { - FutureOr build(); - @$mustCallSuper - @override - void runBuild() { - final ref = this.ref as $Ref, Locale>; - final element = - ref.element - as $ClassProviderElement< - AnyNotifier, Locale>, - AsyncValue, - Object?, - Object? - >; - element.handleCreate(ref, build); - } -} From 433623b1c567d79c68c7f263a54272b7e2fbc737 Mon Sep 17 00:00:00 2001 From: liang-work Date: Fri, 9 Jan 2026 19:41:33 +0800 Subject: [PATCH 14/16] Merge locale_provider.dart into the settings provider --- lib/ui/screens/settings_screen.dart | 43 +++++++++++++---------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/lib/ui/screens/settings_screen.dart b/lib/ui/screens/settings_screen.dart index 607a5ee..e2d649f 100644 --- a/lib/ui/screens/settings_screen.dart +++ b/lib/ui/screens/settings_screen.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:groovybox/data/track_repository.dart'; -import 'package:groovybox/providers/locale_provider.dart'; + import 'package:groovybox/providers/settings_provider.dart'; import 'package:groovybox/providers/watch_folder_provider.dart'; import 'package:groovybox/providers/remote_provider.dart'; @@ -420,36 +420,31 @@ class SettingsScreen extends ConsumerWidget { context.tr('appSettingsDescription'), style: const TextStyle(color: Colors.grey, fontSize: 14), ).padding(horizontal: 16, bottom: 8), - ref.watch(localeProvider).when( - data: (locale) => ListTile( - title: Text(context.tr('language')), - subtitle: Text( - context.tr('languageDescription'), - ), - trailing: DropdownButtonHideUnderline( - child: DropdownButton( - value: locale, + ListTile( + title: Text(context.tr('language')), + subtitle: Text( + context.tr('languageDescription'), + ), + trailing: DropdownButtonHideUnderline( + child: DropdownButton( + value: context.locale, onChanged: (Locale? value) async { if (value != null) { await context.setLocale(value); - await ref.read(localeProvider.notifier).setLocale(value); } }, - items: const [ - DropdownMenuItem( - value: Locale('en'), - child: Text('English'), - ), - DropdownMenuItem( - value: Locale('zh'), - child: Text('中文'), - ), - ], - ), + items: const [ + DropdownMenuItem( + value: Locale('en'), + child: Text('English'), + ), + DropdownMenuItem( + value: Locale('zh'), + child: Text('中文'), + ), + ], ), ), - loading: () => const CircularProgressIndicator(), - error: (error, _) => Text('Error loading locale: $error'), ), const SizedBox(height: 8), ], From 193f18a84f5784d15f638ba65c08e0c8ae7d6c62 Mon Sep 17 00:00:00 2001 From: liang-work Date: Fri, 9 Jan 2026 23:16:05 +0800 Subject: [PATCH 15/16] =?UTF-8?q?=F0=9F=90=9BFix=20some=20localization=20i?= =?UTF-8?q?ssues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/locales/en.json | 54 ++++++++++++++--------------- assets/locales/zh.json | 54 ++++++++++++++--------------- lib/main.dart | 2 ++ lib/ui/screens/library_screen.dart | 7 ++-- lib/ui/screens/settings_screen.dart | 22 ++++++------ 5 files changed, 70 insertions(+), 69 deletions(-) diff --git a/assets/locales/en.json b/assets/locales/en.json index decce04..56f972b 100644 --- a/assets/locales/en.json +++ b/assets/locales/en.json @@ -2,7 +2,7 @@ "noMediaSelected": "No media selected", "noLyricsAvailable": "No Lyrics Available", "fetchLyrics": "Fetch Lyrics", - "searchLyricsWith": "Search lyrics with {searchTerm}", + "searchLyricsWith": "Search lyrics with {}", "whereToSearchLyrics": "Where do you want to search lyrics from?", "musixmatch": "Musixmatch", "netease": "NetEase", @@ -19,40 +19,40 @@ "offsetMs": "Offset (ms)", "save": "Save", "liveLyricsSync": "Live Lyrics Sync", - "offset": "Offset: {value}ms", + "offset": "Offset: {}ms", "minus100ms": "-100ms", "plus10ms": "+10ms", "reset": "Reset", "plus100ms": "+100ms", "fineAdjustment": "Fine Adjustment", "onlyTimedLyricsCanBeSynced": "Only timed lyrics can be synced", - "errorLoadingLyrics": "Error loading lyrics: {error}", - "errorParsingLyrics": "Error parsing lyrics: {error}", + "errorLoadingLyrics": "Error loading lyrics: {}", + "errorParsingLyrics": "Error parsing lyrics: {}", "noTracksInQueue": "No tracks in queue", "unknownArtist": "Unknown Artist", "showLyrics": "Show Lyrics", "showQueue": "Show Queue", "showCover": "Show Cover", - "importedLyricsLines": "Imported {count} lyrics lines for \"{title}\"", - "selected": "{count} selected", + "importedLyricsLines": "Imported {} lyrics lines for \"{}\"", + "selected": "{} selected", "addToPlaylist": "Add to Playlist", "delete": "Delete", "groovyBox": "GroovyBox", "library": "Library", "importFiles": "Import Files", "searchTracks": "Search tracks...", - "searchTracksWithCount": "Search tracks... ({total} tracks)", - "searchTracksFiltered": "Search tracks... ({filtered} of {total} tracks)", - "error": "Error: {message}", + "searchTracksWithCount": "Search tracks... ({} tracks)", + "searchTracksFiltered": "Search tracks... ({} of {} tracks)", + "error": "Error: {}", "noTracksYet": "No tracks yet. Add some!", "noTracksMatchSearch": "No tracks match your search.", "deleteTrack": "Delete Track?", - "confirmDeleteTrack": "Are you sure you want to delete \"{title}\"? This cannot be undone.", - "deletedTrack": "Deleted \"{title}\"", + "confirmDeleteTrack": "Are you sure you want to delete \"{}\" ? This cannot be undone.", + "deletedTrack": "Deleted \"{}\"", "viewDetails": "View Details", "editMetadata": "Edit Metadata", "importLyrics": "Import Lyrics", - "addedToPlaylist": "Added to {name}", + "addedToPlaylist": "Added to {}", "trackDetails": "Track Details", "close": "Close", "title": "Title", @@ -65,11 +65,11 @@ "albumArt": "Album Art", "present": "Present", "editTrack": "Edit Track", - "addedTracksToPlaylist": "Added {count} tracks to {name}", + "addedTracksToPlaylist": "Added {} tracks to {}", "deleteTracks": "Delete Tracks?", - "confirmDeleteTracks": "Are you sure you want to delete {count} tracks? This will remove them from your device.", - "deletedTracks": "Deleted {count} tracks", - "batchImportComplete": "Batch import complete: {matched} matched, {notMatched} not matched", + "confirmDeleteTracks": "Are you sure you want to delete {} tracks? This will remove them from your device.", + "deletedTracks": "Deleted {} tracks", + "batchImportComplete": "Batch import complete: {} matched, {} not matched", "settings": "Settings", "autoScan": "Auto Scan", "autoScanMusicLibraries": "Auto-scan music libraries", @@ -81,13 +81,13 @@ "addMusicLibrary": "Add Music Library", "addMusicLibraryDescription": "Add folder libraries to index music files. Files will be copied to internal storage for playback.", "noMusicLibrariesAdded": "No music libraries added yet.", - "errorLoadingLibraries": "Error loading libraries: {error}", + "errorLoadingLibraries": "Error loading libraries: {}", "remoteProviders": "Remote Providers", "indexRemoteProviders": "Index Remote Providers", "addRemoteProvider": "Add Remote Provider", "remoteProvidersDescription": "Connect to remote media servers like Jellyfin to access your music library.", "noRemoteProvidersAdded": "No remote providers added yet.", - "errorLoadingProviders": "Error loading providers: {error}", + "errorLoadingProviders": "Error loading providers: {}", "playerSettings": "Player Settings", "playerSettingsDescription": "Configure player behavior and display options.", "defaultPlayerScreen": "Default Player Screen", @@ -100,7 +100,7 @@ "databaseManagementDescription": "Manage your music database and cached files.", "resetTrackDatabase": "Reset Track Database", "resetTrackDatabaseDescription": "Remove all tracks from database and delete cached files. This action cannot be undone.", - "errorLoadingSettings": "Error loading settings: {error}", + "errorLoadingSettings": "Error loading settings: {}", "playAll": "Play All", "addToQueue": "Add to Queue", "noTracksInPlaylist": "No tracks in this playlist", @@ -122,18 +122,18 @@ "tracks": "Tracks", "albums": "Albums", "playlists": "Playlists", - "addedMusicLibrary": "Added music library: {path}", - "errorAddingLibrary": "Error adding library: {error}", + "addedMusicLibrary": "Added music library: {}", + "errorAddingLibrary": "Error adding library: {}", "librariesScannedSuccessfully": "Libraries scanned successfully", - "errorScanningLibraries": "Error scanning libraries: {error}", + "errorScanningLibraries": "Error scanning libraries: {}", "noActiveRemoteProviders": "No active remote providers to index", - "indexedRemoteProviders": "Indexed {count} remote provider(s)", - "errorIndexingRemoteProviders": "Error indexing remote providers: {error}", - "addedRemoteProvider": "Added remote provider: {url}", - "errorAddingProvider": "Error adding provider: {error}", + "indexedRemoteProviders": "Indexed {} remote provider(s)", + "errorIndexingRemoteProviders": "Error indexing remote providers: {}", + "addedRemoteProvider": "Added remote provider: {}", + "errorAddingProvider": "Error adding provider: {}", "confirmResetTrackDatabase": "This will permanently delete all tracks from the database and remove all cached music files and album art. This action cannot be undone.\n\nAre you sure you want to continue?", "trackDatabaseReset": "Track database has been reset", - "errorResettingDatabase": "Error resetting database: {error}", + "errorResettingDatabase": "Error resetting database: {}", "addRemoteProviderDialog": "Add Remote Provider", "serverUrl": "Server URL", "serverUrlHint": "https://your-jellyfin-server.com", diff --git a/assets/locales/zh.json b/assets/locales/zh.json index beea5f5..72edd1a 100644 --- a/assets/locales/zh.json +++ b/assets/locales/zh.json @@ -2,7 +2,7 @@ "noMediaSelected": "未选择媒体", "noLyricsAvailable": "无歌词可用", "fetchLyrics": "获取歌词", - "searchLyricsWith": "使用 {searchTerm} 搜索歌词", + "searchLyricsWith": "使用 {} 搜索歌词", "whereToSearchLyrics": "您想从哪里搜索歌词?", "musixmatch": "Musixmatch", "netease": "网易云音乐", @@ -19,41 +19,41 @@ "offsetMs": "偏移量(毫秒)", "save": "保存", "liveLyricsSync": "实时歌词同步", - "offset": "偏移量:{value}毫秒", + "offset": "偏移量:{}毫秒", "minus100ms": "-100毫秒", "plus10ms": "+10毫秒", "reset": "重置", "plus100ms": "+100毫秒", "fineAdjustment": "精细调整", "onlyTimedLyricsCanBeSynced": "只有带时间戳的歌词才能同步", - "errorLoadingLyrics": "加载歌词时出错:{error}", - "errorParsingLyrics": "解析歌词时出错:{error}", + "errorLoadingLyrics": "加载歌词时出错:{}", + "errorParsingLyrics": "解析歌词时出错:{}", "noTracksInQueue": "队列中没有曲目", "unknownArtist": "未知艺术家", "showLyrics": "显示歌词", "showQueue": "显示队列", "showCover": "显示封面", - "importedLyricsLines": "为\"{title}\"导入了 {count} 行歌词", - "selected": "已选择 {count} 项", + "importedLyricsLines": "为\"{}\"导入了 {} 行歌词", + "selected": "已选择 {} 项", "addToPlaylist": "添加到播放列表", "delete": "删除", "groovyBox": "GroovyBox", "library": "音乐库", "importFiles": "导入文件", "searchTracks": "搜索曲目...", - "searchTracksWithCount": "搜索曲目...(共 {total} 首)", - "searchTracksFiltered": "搜索曲目...({filtered} / {total} 首)", - "error": "错误:{message}", + "searchTracksWithCount": "搜索曲目...(共 {} 首)", + "searchTracksFiltered": "搜索曲目...({} / {} 首)", + "error": "错误:{}", "noTracksYet": "还没有曲目,请添加一些!", "noTracksMatchSearch": "没有匹配搜索的曲目。", "deleteTrack": "删除曲目?", - "confirmDeleteTrack": "您确定要删除\"{title}\"吗?此操作无法撤销。", - "deletedTrack": "已删除\"{title}\"", + "confirmDeleteTrack": "您确定要删除\"{}\"吗?此操作无法撤销。", + "deletedTrack": "已删除\"{}\"", "viewDetails": "查看详情", "editMetadata": "编辑元数据", "importLyrics": "导入歌词", "noPlaylistsAvailable": "没有可用的播放列表,请先创建一个!", - "addedToPlaylist": "已添加到 {name}", + "addedToPlaylist": "已添加到 {}", "trackDetails": "曲目详情", "close": "关闭", "title": "标题", @@ -66,11 +66,11 @@ "albumArt": "专辑封面", "present": "存在", "editTrack": "编辑曲目", - "addedTracksToPlaylist": "已将 {count} 首曲目添加到 {name}", + "addedTracksToPlaylist": "已将 {} 首曲目添加到 {}", "deleteTracks": "删除曲目?", - "confirmDeleteTracks": "您确定要删除 {count} 首曲目吗?这将从您的设备中移除它们。", - "deletedTracks": "已删除 {count} 首曲目", - "batchImportComplete": "批量导入完成:{matched} 匹配,{notMatched} 不匹配", + "confirmDeleteTracks": "您确定要删除 {} 首曲目吗?这将从您的设备中移除它们。", + "deletedTracks": "已删除 {} 首曲目", + "batchImportComplete": "批量导入完成:{} 匹配,{} 不匹配", "settings": "设置", "autoScan": "自动扫描", "autoScanMusicLibraries": "自动扫描音乐库", @@ -82,13 +82,13 @@ "addMusicLibrary": "添加音乐库", "addMusicLibraryDescription": "添加文件夹库来索引音乐文件。文件将被复制到内部存储以供播放。", "noMusicLibrariesAdded": "尚未添加音乐库。", - "errorLoadingLibraries": "加载库时出错:{error}", + "errorLoadingLibraries": "加载库时出错:{}", "remoteProviders": "远程提供商", "indexRemoteProviders": "索引远程提供商", "addRemoteProvider": "添加远程提供商", "remoteProvidersDescription": "连接到远程媒体服务器,如Jellyfin,来访问您的音乐库。", "noRemoteProvidersAdded": "尚未添加远程提供商。", - "errorLoadingProviders": "加载提供商时出错:{error}", + "errorLoadingProviders": "加载提供商时出错:{}", "playerSettings": "播放器设置", "playerSettingsDescription": "配置播放器行为和显示选项。", "defaultPlayerScreen": "默认播放器屏幕", @@ -101,7 +101,7 @@ "databaseManagementDescription": "管理您的音乐数据库和缓存文件。", "resetTrackDatabase": "重置曲目数据库", "resetTrackDatabaseDescription": "从数据库中移除所有曲目并删除缓存文件。此操作无法撤销。", - "errorLoadingSettings": "加载设置时出错:{error}", + "errorLoadingSettings": "加载设置时出错:{}", "noTracksInAlbum": "此专辑中没有曲目", "playAll": "播放全部", "addToQueue": "添加到队列", @@ -124,18 +124,18 @@ "tracks": "曲目", "albums": "专辑", "playlists": "播放列表", - "addedMusicLibrary": "已添加音乐库:{path}", - "errorAddingLibrary": "添加库时出错:{error}", + "addedMusicLibrary": "已添加音乐库:{}", + "errorAddingLibrary": "添加库时出错:{}", "librariesScannedSuccessfully": "库扫描成功", - "errorScanningLibraries": "扫描库时出错:{error}", + "errorScanningLibraries": "扫描库时出错:{}", "noActiveRemoteProviders": "没有活动的远程提供商可索引", - "indexedRemoteProviders": "已索引 {count} 个远程提供商", - "errorIndexingRemoteProviders": "索引远程提供商时出错:{error}", - "addedRemoteProvider": "已添加远程提供商:{url}", - "errorAddingProvider": "添加提供商时出错:{error}", + "indexedRemoteProviders": "已索引 {} 个远程提供商", + "errorIndexingRemoteProviders": "索引远程提供商时出错:{}", + "addedRemoteProvider": "已添加远程提供商:{}", + "errorAddingProvider": "添加提供商时出错:{}", "confirmResetTrackDatabase": "这将永久删除数据库中的所有曲目,并移除所有缓存的音乐文件和专辑封面。此操作无法撤销。\n\n您确定要继续吗?", "trackDatabaseReset": "曲目数据库已重置", - "errorResettingDatabase": "重置数据库时出错:{error}", + "errorResettingDatabase": "重置数据库时出错:{}", "addRemoteProviderDialog": "添加远程提供商", "serverUrl": "服务器URL", "serverUrlHint": "https://your-jellyfin-server.com", diff --git a/lib/main.dart b/lib/main.dart index 271dab1..09273f6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:groovybox/providers/audio_provider.dart'; import 'package:groovybox/providers/theme_provider.dart'; import 'package:groovybox/router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:media_kit/media_kit.dart'; import 'package:audio_service/audio_service.dart' as audio_service; @@ -75,6 +76,7 @@ class _GroovyAppState extends ConsumerState { darkTheme: ref.watch(darkThemeProvider), themeMode: themeMode, routerConfig: router, + locale: context.locale, localizationsDelegates: context.localizationDelegates, supportedLocales: context.supportedLocales, ); diff --git a/lib/ui/screens/library_screen.dart b/lib/ui/screens/library_screen.dart index 30670e2..6a3eb70 100644 --- a/lib/ui/screens/library_screen.dart +++ b/lib/ui/screens/library_screen.dart @@ -398,7 +398,7 @@ class LibraryScreen extends HookConsumerWidget { final tracks = snapshot.data!; final totalTracks = tracks.length; if (searchQuery.value.isEmpty) { - hintText = '${context.tr('searchTracks')} ($totalTracks ${context.tr('tracks')})'; + hintText = context.tr('searchTracksWithCount', args: [totalTracks.toString()]); } else { final query = searchQuery.value.toLowerCase(); final filteredCount = tracks.where((track) { @@ -421,8 +421,7 @@ class LibraryScreen extends HookConsumerWidget { } return false; }).length; - hintText = - '${context.tr('searchTracks')}... ($filteredCount of $totalTracks ${context.tr('tracks')})'; + hintText = context.tr('searchTracksFiltered', args: [filteredCount.toString(), totalTracks.toString()]); } } @@ -1131,4 +1130,4 @@ class LibraryScreen extends HookConsumerWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/ui/screens/settings_screen.dart b/lib/ui/screens/settings_screen.dart index e2d649f..0033c42 100644 --- a/lib/ui/screens/settings_screen.dart +++ b/lib/ui/screens/settings_screen.dart @@ -12,7 +12,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:styled_widget/styled_widget.dart'; -class SettingsScreen extends ConsumerWidget { +class SettingsScreen extends HookConsumerWidget { const SettingsScreen({super.key}); @override @@ -428,19 +428,21 @@ class SettingsScreen extends ConsumerWidget { trailing: DropdownButtonHideUnderline( child: DropdownButton( value: context.locale, - onChanged: (Locale? value) async { + onChanged: (Locale? value) { if (value != null) { - await context.setLocale(value); + EasyLocalization.of(context)!.setLocale(value); + } else { + EasyLocalization.of(context)!.resetLocale(); } }, - items: const [ + items: [ DropdownMenuItem( - value: Locale('en'), - child: Text('English'), + value: const Locale('en'), + child: Text(context.tr('english')), ), DropdownMenuItem( - value: Locale('zh'), - child: Text('中文'), + value: const Locale('zh'), + child: Text(context.tr('chinese')), ), ], ), @@ -513,9 +515,7 @@ class SettingsScreen extends ConsumerWidget { } } catch (e) { if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(context.tr('errorAddingLibrary', args: [e.toString()])))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.tr('errorAddingLibrary', args: [e.toString()])))); } } } From 4edda8e08d34a0d5115caddc21dcabd86577a310 Mon Sep 17 00:00:00 2001 From: liang-work Date: Fri, 9 Jan 2026 23:38:06 +0800 Subject: [PATCH 16/16] =?UTF-8?q?=F0=9F=8E=A8=20Change=20to=20use=20contex?= =?UTF-8?q?t-free=20translation=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/ui/screens/library_screen.dart | 16 +++++++++------- lib/ui/screens/player_screen.dart | 6 +++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/ui/screens/library_screen.dart b/lib/ui/screens/library_screen.dart index 6a3eb70..db9d85e 100644 --- a/lib/ui/screens/library_screen.dart +++ b/lib/ui/screens/library_screen.dart @@ -84,7 +84,7 @@ class LibraryScreen extends HookConsumerWidget { onPressed: clearSelection, ), title: Text( - context.tr('selected', args: [selectedTrackIds.value.length.toString()]), + context.tr('selected').replaceAll('{}', selectedTrackIds.value.length.toString()), ).textColor(Theme.of(context).colorScheme.onPrimary), backgroundColor: Theme.of(context).colorScheme.primary, actions: [ @@ -259,7 +259,7 @@ class LibraryScreen extends HookConsumerWidget { onPressed: clearSelection, ), title: Text( - context.tr('selected', args: [selectedTrackIds.value.length.toString()]), + context.tr('selected').replaceAll('{}', selectedTrackIds.value.length.toString()), ).textColor(Theme.of(context).colorScheme.onPrimary), backgroundColor: Theme.of(context).colorScheme.primary, actions: [ @@ -398,7 +398,7 @@ class LibraryScreen extends HookConsumerWidget { final tracks = snapshot.data!; final totalTracks = tracks.length; if (searchQuery.value.isEmpty) { - hintText = context.tr('searchTracksWithCount', args: [totalTracks.toString()]); + hintText = context.tr('searchTracksWithCount').replaceAll('{}', totalTracks.toString()); } else { final query = searchQuery.value.toLowerCase(); final filteredCount = tracks.where((track) { @@ -421,7 +421,9 @@ class LibraryScreen extends HookConsumerWidget { } return false; }).length; - hintText = context.tr('searchTracksFiltered', args: [filteredCount.toString(), totalTracks.toString()]); + hintText = context.tr('searchTracksFiltered') + .replaceAll('{}', filteredCount.toString()) + .replaceAll('{}', totalTracks.toString()); } } @@ -516,7 +518,7 @@ class LibraryScreen extends HookConsumerWidget { return AlertDialog( title: Text(context.tr('deleteTrack')), content: Text( - context.tr('confirmDeleteTrack', args: [track.title]), + context.tr('confirmDeleteTrack').replaceAll('{}', track.title), ), actions: [ TextButton( @@ -544,7 +546,7 @@ class LibraryScreen extends HookConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - context.tr('deletedTrack', args: [track.title]), + context.tr('deletedTrack').replaceAll('{}', track.title), ), ), ); @@ -742,7 +744,7 @@ class LibraryScreen extends HookConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - context.tr('addedToPlaylist', args: [playlist.name]), + context.tr('addedToPlaylist').replaceAll('{}', playlist.name), ), ), ); diff --git a/lib/ui/screens/player_screen.dart b/lib/ui/screens/player_screen.dart index 565fd2d..b09c6fc 100644 --- a/lib/ui/screens/player_screen.dart +++ b/lib/ui/screens/player_screen.dart @@ -712,7 +712,7 @@ class _PlayerLyrics extends HookConsumerWidget { } } } catch (e) { - return Center(child: Text(context.tr('errorLoadingLyrics', args: [e.toString()]))); + return Center(child: Text(context.tr('errorLoadingLyrics').replaceAll('{}', e.toString()))); } } } @@ -754,7 +754,7 @@ class _FetchLyricsDialog extends StatelessWidget { text: TextSpan( style: TextStyle(color: Theme.of(context).colorScheme.onSurface), children: [ - TextSpan(text: context.tr('searchLyricsWith', args: [searchTerm.split(' ').first])), + TextSpan(text: context.tr('searchLyricsWith').replaceAll('{}', searchTerm.split(' ').first)), TextSpan( text: ' $searchTerm', style: const TextStyle(fontWeight: FontWeight.bold), @@ -1978,7 +1978,7 @@ class _LiveLyricsSyncDialog extends HookConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text(context.tr('offset', args: [tempOffset.value.toString()])), + Text(context.tr('offset').replaceAll('{}', tempOffset.value.toString())), ], ), ),