diff --git a/assets/locales/en.json b/assets/locales/en.json new file mode 100644 index 0000000..56f972b --- /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 {}", + "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: {}ms", + "minus100ms": "-100ms", + "plus10ms": "+10ms", + "reset": "Reset", + "plus100ms": "+100ms", + "fineAdjustment": "Fine Adjustment", + "onlyTimedLyricsCanBeSynced": "Only timed lyrics can be synced", + "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 {} lyrics lines for \"{}\"", + "selected": "{} selected", + "addToPlaylist": "Add to Playlist", + "delete": "Delete", + "groovyBox": "GroovyBox", + "library": "Library", + "importFiles": "Import Files", + "searchTracks": "Search tracks...", + "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 \"{}\" ? This cannot be undone.", + "deletedTrack": "Deleted \"{}\"", + "viewDetails": "View Details", + "editMetadata": "Edit Metadata", + "importLyrics": "Import Lyrics", + "addedToPlaylist": "Added to {}", + "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 {} tracks to {}", + "deleteTracks": "Delete Tracks?", + "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", + "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: {}", + "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: {}", + "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: {}", + "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: {}", + "errorAddingLibrary": "Error adding library: {}", + "librariesScannedSuccessfully": "Libraries scanned successfully", + "errorScanningLibraries": "Error scanning libraries: {}", + "noActiveRemoteProviders": "No active remote providers to index", + "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: {}", + "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..72edd1a --- /dev/null +++ b/assets/locales/zh.json @@ -0,0 +1,157 @@ +{ + "noMediaSelected": "未选择媒体", + "noLyricsAvailable": "无歌词可用", + "fetchLyrics": "获取歌词", + "searchLyricsWith": "使用 {} 搜索歌词", + "whereToSearchLyrics": "您想从哪里搜索歌词?", + "musixmatch": "Musixmatch", + "netease": "网易云音乐", + "lrclib": "Lrclib", + "manualImport": "手动导入", + "cancel": "取消", + "lyricsOptions": "歌词选项", + "refetch": "重新获取", + "clear": "清除", + "liveSyncLyrics": "实时同步歌词", + "manualOffset": "手动偏移", + "adjustLyricsTiming": "调整歌词时间", + "enterOffsetMs": "输入偏移量(毫秒)。\n正值延迟歌词显示,负值提前显示。", + "offsetMs": "偏移量(毫秒)", + "save": "保存", + "liveLyricsSync": "实时歌词同步", + "offset": "偏移量:{}毫秒", + "minus100ms": "-100毫秒", + "plus10ms": "+10毫秒", + "reset": "重置", + "plus100ms": "+100毫秒", + "fineAdjustment": "精细调整", + "onlyTimedLyricsCanBeSynced": "只有带时间戳的歌词才能同步", + "errorLoadingLyrics": "加载歌词时出错:{}", + "errorParsingLyrics": "解析歌词时出错:{}", + "noTracksInQueue": "队列中没有曲目", + "unknownArtist": "未知艺术家", + "showLyrics": "显示歌词", + "showQueue": "显示队列", + "showCover": "显示封面", + "importedLyricsLines": "为\"{}\"导入了 {} 行歌词", + "selected": "已选择 {} 项", + "addToPlaylist": "添加到播放列表", + "delete": "删除", + "groovyBox": "GroovyBox", + "library": "音乐库", + "importFiles": "导入文件", + "searchTracks": "搜索曲目...", + "searchTracksWithCount": "搜索曲目...(共 {} 首)", + "searchTracksFiltered": "搜索曲目...({} / {} 首)", + "error": "错误:{}", + "noTracksYet": "还没有曲目,请添加一些!", + "noTracksMatchSearch": "没有匹配搜索的曲目。", + "deleteTrack": "删除曲目?", + "confirmDeleteTrack": "您确定要删除\"{}\"吗?此操作无法撤销。", + "deletedTrack": "已删除\"{}\"", + "viewDetails": "查看详情", + "editMetadata": "编辑元数据", + "importLyrics": "导入歌词", + "noPlaylistsAvailable": "没有可用的播放列表,请先创建一个!", + "addedToPlaylist": "已添加到 {}", + "trackDetails": "曲目详情", + "close": "关闭", + "title": "标题", + "artist": "艺术家", + "album": "专辑", + "duration": "时长", + "fileSize": "文件大小", + "filePath": "文件路径", + "dateAdded": "添加日期", + "albumArt": "专辑封面", + "present": "存在", + "editTrack": "编辑曲目", + "addedTracksToPlaylist": "已将 {} 首曲目添加到 {}", + "deleteTracks": "删除曲目?", + "confirmDeleteTracks": "您确定要删除 {} 首曲目吗?这将从您的设备中移除它们。", + "deletedTracks": "已删除 {} 首曲目", + "batchImportComplete": "批量导入完成:{} 匹配,{} 不匹配", + "settings": "设置", + "autoScan": "自动扫描", + "autoScanMusicLibraries": "自动扫描音乐库", + "autoScanDescription": "自动扫描音乐库中的新音乐文件", + "watchForChanges": "监视更改", + "watchForChangesDescription": "监视音乐库的文件更改", + "musicLibraries": "音乐库", + "scanLibraries": "扫描库", + "addMusicLibrary": "添加音乐库", + "addMusicLibraryDescription": "添加文件夹库来索引音乐文件。文件将被复制到内部存储以供播放。", + "noMusicLibrariesAdded": "尚未添加音乐库。", + "errorLoadingLibraries": "加载库时出错:{}", + "remoteProviders": "远程提供商", + "indexRemoteProviders": "索引远程提供商", + "addRemoteProvider": "添加远程提供商", + "remoteProvidersDescription": "连接到远程媒体服务器,如Jellyfin,来访问您的音乐库。", + "noRemoteProvidersAdded": "尚未添加远程提供商。", + "errorLoadingProviders": "加载提供商时出错:{}", + "playerSettings": "播放器设置", + "playerSettingsDescription": "配置播放器行为和显示选项。", + "defaultPlayerScreen": "默认播放器屏幕", + "defaultPlayerScreenDescription": "选择打开播放器时显示的屏幕。", + "lyricsMode": "歌词模式", + "lyricsModeDescription": "选择歌词的显示方式。", + "continuePlaying": "继续播放", + "continuePlayingDescription": "队列为空后继续播放音乐", + "databaseManagement": "数据库管理", + "databaseManagementDescription": "管理您的音乐数据库和缓存文件。", + "resetTrackDatabase": "重置曲目数据库", + "resetTrackDatabaseDescription": "从数据库中移除所有曲目并删除缓存文件。此操作无法撤销。", + "errorLoadingSettings": "加载设置时出错:{}", + "noTracksInAlbum": "此专辑中没有曲目", + "playAll": "播放全部", + "addToQueue": "添加到队列", + "noTracksInPlaylist": "此播放列表中没有曲目", + "noAlbumsFound": "未找到专辑", + "createOne": "创建一个", + "addNewPlaylist": "添加新播放列表", + "newPlaylist": "新播放列表", + "playlistName": "播放列表名称", + "create": "创建", + "noPlaylistsYet": "还没有播放列表", + "queue": "队列", + "appSettings": "应用设置", + "appSettingsDescription": "配置应用范围的设置和偏好。", + "language": "语言", + "languageDescription": "选择应用语言。", + "english": "English", + "chinese": "中文", + "settingsTitle": "设置", + "tracks": "曲目", + "albums": "专辑", + "playlists": "播放列表", + "addedMusicLibrary": "已添加音乐库:{}", + "errorAddingLibrary": "添加库时出错:{}", + "librariesScannedSuccessfully": "库扫描成功", + "errorScanningLibraries": "扫描库时出错:{}", + "noActiveRemoteProviders": "没有活动的远程提供商可索引", + "indexedRemoteProviders": "已索引 {} 个远程提供商", + "errorIndexingRemoteProviders": "索引远程提供商时出错:{}", + "addedRemoteProvider": "已添加远程提供商:{}", + "errorAddingProvider": "添加提供商时出错:{}", + "confirmResetTrackDatabase": "这将永久删除数据库中的所有曲目,并移除所有缓存的音乐文件和专辑封面。此操作无法撤销。\n\n您确定要继续吗?", + "trackDatabaseReset": "曲目数据库已重置", + "errorResettingDatabase": "重置数据库时出错:{}", + "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/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/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/main.dart b/lib/main.dart index c099ce7..09273f6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:groovybox/logic/audio_handler.dart'; import 'package:groovybox/logic/window_helpers.dart'; @@ -5,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; @@ -14,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(); @@ -33,24 +38,34 @@ 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 const 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(); + }, + ), ), ), ); } -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 router = ref.watch(routerProvider); @@ -61,6 +76,9 @@ class GroovyApp extends ConsumerWidget { darkTheme: ref.watch(darkThemeProvider), themeMode: themeMode, routerConfig: router, + locale: context.locale, + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, ); } } 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/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..be948d1 100644 --- a/lib/ui/screens/album_detail_screen.dart +++ b/lib/ui/screens/album_detail_screen.dart @@ -1,9 +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/providers/audio_provider.dart'; import 'package:groovybox/ui/widgets/track_tile.dart'; import 'package:groovybox/ui/widgets/universal_image.dart'; @@ -73,9 +75,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(context.tr('noTracksInAlbum'))), ); } @@ -96,7 +98,7 @@ class AlbumDetailScreen extends HookConsumerWidget { _playAlbum(ref, tracks); }, icon: const Icon(Symbols.play_arrow), - label: const Text('Play All'), + label: Text(context.tr('playAll')), ), ), SizedBox( @@ -106,7 +108,7 @@ class AlbumDetailScreen extends HookConsumerWidget { _addToQueue(ref, tracks); }, icon: const Icon(Symbols.queue_music), - label: const Text('Add to Queue'), + label: Text(context.tr('addToQueue')), ), ), ], @@ -223,3 +225,5 @@ class AlbumDetailScreen extends HookConsumerWidget { ); } } + + diff --git a/lib/ui/screens/library_screen.dart b/lib/ui/screens/library_screen.dart index aad9c1c..db9d85e 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,6 +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/logic/lyrics_parser.dart'; import 'package:groovybox/logic/window_helpers.dart'; import 'package:groovybox/providers/audio_provider.dart'; @@ -82,14 +84,14 @@ class LibraryScreen extends HookConsumerWidget { onPressed: clearSelection, ), title: Text( - '${selectedTrackIds.value.length} selected', + context.tr('selected').replaceAll('{}', 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: 'Add to Playlist', + tooltip: context.tr('addToPlaylist'), onPressed: () { _batchAddToPlaylist( context, @@ -102,7 +104,7 @@ class LibraryScreen extends HookConsumerWidget { IconButton( icon: const Icon(Symbols.delete), color: Theme.of(context).colorScheme.onPrimary, - tooltip: 'Delete', + tooltip: context.tr('delete'), onPressed: () { _batchDelete( context, @@ -139,7 +141,7 @@ class LibraryScreen extends HookConsumerWidget { ), ], ) - : const Text('Library'), + : Text(context.tr('library')), actions: [ IconButton( onPressed: () { @@ -149,7 +151,7 @@ class LibraryScreen extends HookConsumerWidget { ), IconButton( icon: const Icon(Symbols.add_circle_outline), - tooltip: 'Import Files', + tooltip: context.tr('importFiles'), onPressed: () async { final result = await FilePicker.platform.pickFiles( type: FileType.any, @@ -207,18 +209,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(context.tr('tracks')), ), NavigationRailDestination( icon: Icon(Symbols.album), - label: Text('Albums'), + label: Text(context.tr('albums')), ), NavigationRailDestination( icon: Icon(Symbols.queue_music), - label: Text('Playlists'), + label: Text(context.tr('playlists')), ), ], ), @@ -257,13 +259,13 @@ class LibraryScreen extends HookConsumerWidget { onPressed: clearSelection, ), title: Text( - '${selectedTrackIds.value.length} selected', + context.tr('selected').replaceAll('{}', 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: 'Add to Playlist', + tooltip: context.tr('addToPlaylist'), color: Theme.of(context).colorScheme.onPrimary, onPressed: () { _batchAddToPlaylist( @@ -276,7 +278,7 @@ class LibraryScreen extends HookConsumerWidget { ), IconButton( icon: const Icon(Symbols.delete), - tooltip: 'Delete', + tooltip: context.tr('delete'), color: Theme.of(context).colorScheme.onPrimary, onPressed: () { _batchDelete( @@ -292,12 +294,12 @@ class LibraryScreen extends HookConsumerWidget { ) : AppBar( centerTitle: true, - title: const Text('Library'), - bottom: const TabBar( + title: Text(context.tr('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: 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: [ @@ -309,7 +311,7 @@ class LibraryScreen extends HookConsumerWidget { ), IconButton( icon: const Icon(Symbols.add_circle_outline), - tooltip: 'Import Files', + tooltip: context.tr('importFiles'), onPressed: () async { final result = await FilePicker.platform.pickFiles( type: FileType.any, @@ -391,12 +393,12 @@ class LibraryScreen extends HookConsumerWidget { // Calculate hintText String hintText; if (!snapshot.hasData || snapshot.hasError) { - hintText = 'Search tracks...'; + hintText = context.tr('searchTracks'); } else { final tracks = snapshot.data!; final totalTracks = tracks.length; if (searchQuery.value.isEmpty) { - hintText = 'Search tracks... ($totalTracks tracks)'; + hintText = context.tr('searchTracksWithCount').replaceAll('{}', totalTracks.toString()); } else { final query = searchQuery.value.toLowerCase(); final filteredCount = tracks.where((track) { @@ -419,8 +421,9 @@ class LibraryScreen extends HookConsumerWidget { } return false; }).length; - hintText = - 'Search tracks... ($filteredCount of $totalTracks tracks)'; + hintText = context.tr('searchTracksFiltered') + .replaceAll('{}', filteredCount.toString()) + .replaceAll('{}', totalTracks.toString()); } } @@ -433,7 +436,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(context.tr('noTracksYet'))); } else { List filteredTracks; if (searchQuery.value.isEmpty) { @@ -463,8 +466,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(context.tr('noTracksMatchSearch')), ); } else { mainContent = ListView.builder( @@ -491,7 +494,7 @@ class LibraryScreen extends HookConsumerWidget { overflow: TextOverflow.ellipsis, ), subtitle: Text( - '${track.artist ?? 'Unknown Artist'} • ${_formatDuration(track.duration)}', + '${track.artist ?? context.tr('unknownArtist')} - ${_formatDuration(track.duration)}', maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -513,15 +516,15 @@ class LibraryScreen extends HookConsumerWidget { context: context, builder: (context) { return AlertDialog( - title: const Text('Delete Track?'), + title: Text(context.tr('deleteTrack')), content: Text( - 'Are you sure you want to delete "${track.title}"? This cannot be undone.', + context.tr('confirmDeleteTrack').replaceAll('{}', track.title), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), - child: const Text('Cancel'), + child: Text(context.tr('cancel')), ), TextButton( onPressed: () => @@ -529,7 +532,7 @@ class LibraryScreen extends HookConsumerWidget { style: TextButton.styleFrom( foregroundColor: Colors.red, ), - child: const Text('Delete'), + child: Text(context.tr('delete')), ), ], ); @@ -541,7 +544,11 @@ class LibraryScreen extends HookConsumerWidget { .read(trackRepositoryProvider.notifier) .deleteTrack(track.id); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Deleted "${track.title}"')), + SnackBar( + content: Text( + context.tr('deletedTrack').replaceAll('{}', track.title), + ), + ), ); }, child: TrackTile( @@ -635,7 +642,7 @@ class LibraryScreen extends HookConsumerWidget { children: [ ListTile( leading: const Icon(Symbols.playlist_add), - title: const Text('Add to Playlist'), + title: Text(context.tr('addToPlaylist')), onTap: () { Navigator.pop(context); _showAddToPlaylistDialog(context, ref, track); @@ -643,7 +650,7 @@ class LibraryScreen extends HookConsumerWidget { ), ListTile( leading: const Icon(Symbols.info), - title: const Text('View Details'), + title: Text(context.tr('viewDetails')), onTap: () { Navigator.pop(context); _showTrackDetails(context, ref, track); @@ -651,7 +658,7 @@ class LibraryScreen extends HookConsumerWidget { ), ListTile( leading: const Icon(Symbols.edit), - title: const Text('Edit Metadata'), + title: Text(context.tr('editMetadata')), onTap: () { Navigator.pop(context); _showEditDialog(context, ref, track); @@ -659,7 +666,7 @@ class LibraryScreen extends HookConsumerWidget { ), ListTile( leading: const Icon(Symbols.lyrics), - title: const Text('Import Lyrics'), + title: Text(context.tr('importLyrics')), onTap: () { Navigator.pop(context); _importLyricsForTrack(context, ref, track); @@ -667,8 +674,8 @@ class LibraryScreen extends HookConsumerWidget { ), ListTile( leading: const Icon(Symbols.delete, color: Colors.red), - title: const Text( - 'Delete Track', + title: Text( + context.tr('deleteTrack'), style: TextStyle(color: Colors.red), ), onTap: () { @@ -699,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(context.tr('addToPlaylist')), content: ConstrainedBox( constraints: BoxConstraints( maxWidth: screenSize.width * 0.8, @@ -718,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( + context.tr('noPlaylistsAvailable'), ); } @@ -736,7 +743,9 @@ class LibraryScreen extends HookConsumerWidget { Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Added to ${playlist.name}'), + content: Text( + context.tr('addedToPlaylist').replaceAll('{}', playlist.name), + ), ), ); }, @@ -752,7 +761,7 @@ class LibraryScreen extends HookConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + child: Text(context.tr('cancel')), ), ], ); @@ -766,9 +775,9 @@ class LibraryScreen extends HookConsumerWidget { Track track, ) async { // Try to get file info - String fileSize = 'Unknown'; - String libraryName = 'Unknown'; - String dateAdded = 'Unknown'; + String fileSize = context.tr('unknown'); + String libraryName = context.tr('unknown'); + String dateAdded = context.tr('unknown'); try { final file = File(track.path); @@ -802,7 +811,7 @@ class LibraryScreen extends HookConsumerWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Track Details'), + title: Text(context.tr('trackDetails')), content: ConstrainedBox( constraints: BoxConstraints(maxWidth: screenSize.width * 0.8), child: SingleChildScrollView( @@ -810,16 +819,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(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('Album Art', 'Present'), + _buildDetailRow(context.tr('albumArt'), 'Present'), ], ), ), @@ -827,7 +836,7 @@ class LibraryScreen extends HookConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Close'), + child: Text(context.tr('close')), ), ], ), @@ -864,7 +873,7 @@ class LibraryScreen extends HookConsumerWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Edit Track'), + title: Text(context.tr('editTrack')), content: ConstrainedBox( constraints: BoxConstraints(maxWidth: screenSize.width * 0.8), child: Column( @@ -873,15 +882,15 @@ class LibraryScreen extends HookConsumerWidget { children: [ TextField( controller: titleController, - decoration: const InputDecoration(labelText: 'Title'), + decoration: InputDecoration(labelText: context.tr('title')), ), TextField( controller: artistController, - decoration: const InputDecoration(labelText: 'Artist'), + decoration: InputDecoration(labelText: context.tr('artist')), ), TextField( controller: albumController, - decoration: const InputDecoration(labelText: 'Album'), + decoration: InputDecoration(labelText: context.tr('album')), ), ], ), @@ -889,7 +898,7 @@ class LibraryScreen extends HookConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + child: Text(context.tr('cancel')), ), TextButton( onPressed: () { @@ -903,7 +912,7 @@ class LibraryScreen extends HookConsumerWidget { ); Navigator.pop(context); }, - child: const Text('Save'), + child: Text(context.tr('save')), ), ], ), @@ -929,7 +938,7 @@ class LibraryScreen extends HookConsumerWidget { context: context, builder: (context) { return AlertDialog( - title: const Text('Add to Playlist'), + title: Text(context.tr('addToPlaylist')), content: ConstrainedBox( constraints: BoxConstraints( maxWidth: screenSize.width * 0.8, @@ -948,7 +957,7 @@ class LibraryScreen extends HookConsumerWidget { } final playlists = snapshot.data!; if (playlists.isEmpty) { - return const Text('No playlists available.'); + return Text(context.tr('noPlaylistsAvailable')); } return SingleChildScrollView( @@ -970,7 +979,7 @@ class LibraryScreen extends HookConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - 'Added ${trackIds.length} tracks to ${playlist.name}', + '${context.tr('added')} ${trackIds.length} ${context.tr('tracks')} ${context.tr('to')} ${playlist.name}', ), ), ); @@ -987,7 +996,7 @@ class LibraryScreen extends HookConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + child: Text(context.tr('cancel')), ), ], ); @@ -1004,20 +1013,20 @@ class LibraryScreen extends HookConsumerWidget { final confirm = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Delete Tracks?'), + title: Text(context.tr('deleteTracks')), content: Text( - 'Are you sure you want to delete ${trackIds.length} tracks? ' - 'This will remove them from your device.', + '${context.tr('confirmDelete')} ${trackIds.length} ${context.tr('tracks')}? ' + '${context.tr('thisWillRemoveThemFromYourDevice')}', ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text('Cancel'), + child: Text(context.tr('cancel')), ), TextButton( onPressed: () => Navigator.pop(context, true), style: TextButton.styleFrom(foregroundColor: Colors.red), - child: const Text('Delete'), + child: Text(context.tr('delete')), ), ], ), @@ -1031,7 +1040,11 @@ class LibraryScreen extends HookConsumerWidget { onSuccess(); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Deleted ${trackIds.length} tracks')), + SnackBar( + content: Text( + '${context.tr('deleted')} ${trackIds.length} ${context.tr('tracks')}', + ), + ), ); } } @@ -1063,7 +1076,7 @@ class LibraryScreen extends HookConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - 'Imported ${lyricsData.lines.length} lyrics lines for "${track.title}"', + '${context.tr('imported')} ${lyricsData.lines.length} ${context.tr('lyricsLines')} for "${track.title}"', ), ), ); @@ -1114,7 +1127,7 @@ class LibraryScreen extends HookConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - 'Batch import complete: $matched matched, $notMatched not matched', + '${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 302be4c..b09c6fc 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,6 +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/logic/lrc_providers.dart'; import 'package:groovybox/logic/lyrics_parser.dart'; import 'package:groovybox/logic/metadata_service.dart'; @@ -68,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 const Center(child: Text('No media selected')); + return Center(child: Text(context.tr('noMediaSelected'))); } final media = medias[index]; @@ -614,7 +616,7 @@ class _PlayerLyrics extends HookConsumerWidget { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('No Lyrics Available'), + Text(context.tr('noLyricsAvailable')), const SizedBox(height: 16), if (lyricsFetcher.isLoading) @@ -625,7 +627,7 @@ class _PlayerLyrics extends HookConsumerWidget { else ElevatedButton.icon( icon: const Icon(Symbols.download), - label: const Text('Fetch Lyrics'), + label: Text(context.tr('fetchLyrics')), onPressed: () => _showFetchLyricsDialog( context, ref, @@ -710,7 +712,7 @@ class _PlayerLyrics extends HookConsumerWidget { } } } catch (e) { - return Center(child: Text('Error parsing lyrics: $e')); + return Center(child: Text(context.tr('errorLoadingLyrics').replaceAll('{}', e.toString()))); } } } @@ -742,7 +744,7 @@ class _FetchLyricsDialog extends StatelessWidget { .trim(); return AlertDialog( - title: const Text('Fetch Lyrics'), + title: Text(context.tr('fetchLyrics')), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -752,15 +754,15 @@ class _FetchLyricsDialog extends StatelessWidget { text: TextSpan( style: TextStyle(color: Theme.of(context).colorScheme.onSurface), children: [ - const TextSpan(text: 'Search lyrics with '), + TextSpan(text: context.tr('searchLyricsWith').replaceAll('{}', searchTerm.split(' ').first)), TextSpan( - text: searchTerm, + text: ' $searchTerm', style: const TextStyle(fontWeight: FontWeight.bold), ), ], ), ), - Text('Where do you want to search lyrics from?'), + Text(context.tr('whereToSearchLyrics')), Card( child: Column( mainAxisSize: MainAxisSize.min, @@ -768,7 +770,7 @@ class _FetchLyricsDialog extends StatelessWidget { ListTile( dense: true, leading: const Icon(Symbols.library_music), - title: const Text('Musixmatch'), + title: Text(context.tr('musixmatch')), shape: RoundedRectangleBorder( borderRadius: const BorderRadius.all(Radius.circular(12)), ), @@ -787,7 +789,7 @@ class _FetchLyricsDialog extends StatelessWidget { ListTile( dense: true, leading: const Icon(Symbols.music_video), - title: const Text('NetEase'), + title: Text(context.tr('netease')), shape: RoundedRectangleBorder( borderRadius: const BorderRadius.all(Radius.circular(12)), ), @@ -806,7 +808,7 @@ class _FetchLyricsDialog extends StatelessWidget { ListTile( dense: true, leading: const Icon(Symbols.library_books), - title: const Text('Lrclib'), + title: Text(context.tr('lrclib')), shape: RoundedRectangleBorder( borderRadius: const BorderRadius.all(Radius.circular(12)), ), @@ -825,7 +827,7 @@ class _FetchLyricsDialog extends StatelessWidget { ListTile( dense: true, leading: const Icon(Symbols.file_upload), - title: const Text('Manual Import'), + title: Text(context.tr('manualImport')), shape: RoundedRectangleBorder( borderRadius: const BorderRadius.all(Radius.circular(12)), ), @@ -842,7 +844,7 @@ class _FetchLyricsDialog extends StatelessWidget { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), + child: Text(context.tr('cancel')), ), ], ); @@ -909,7 +911,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { return IconButton( icon: const Icon(Symbols.settings_applications), iconSize: 24, - tooltip: 'Adjust Lyrics', + tooltip: context.tr('adjustLyricsTiming'), onPressed: () => _showLyricsRefreshDialog( context, ref, @@ -973,7 +975,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Lyrics Options'), + title: Text(context.tr('lyricsOptions')), content: Column( spacing: 8, mainAxisSize: MainAxisSize.min, @@ -987,7 +989,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { Expanded( child: ElevatedButton.icon( icon: const Icon(Symbols.refresh), - label: const Text('Re-fetch'), + label: Text(context.tr('refetch')), onPressed: () { Navigator.of(context).pop(); final metadata = metadataAsync.value; @@ -1007,7 +1009,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { Expanded( child: ElevatedButton.icon( icon: const Icon(Symbols.clear), - label: const Text('Clear'), + label: Text(context.tr('clear')), style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, @@ -1059,7 +1061,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { width: double.infinity, child: ElevatedButton.icon( icon: const Icon(Symbols.sync), - label: const Text('Live Sync Lyrics'), + label: Text(context.tr('liveLyricsSync')), onPressed: () { Navigator.of(context).pop(); _showLiveLyricsSyncDialog( @@ -1076,7 +1078,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { width: double.infinity, child: ElevatedButton.icon( icon: const Icon(Symbols.tune), - label: const Text('Manual Offset'), + label: Text(context.tr('manualOffset')), onPressed: () { Navigator.of(context).pop(); _showLyricsOffsetDialog( @@ -1095,7 +1097,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), + child: Text(context.tr('cancel')), ), ], ), @@ -1115,17 +1117,17 @@ class _LyricsAdjustButton extends HookConsumerWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Adjust Lyrics Timing'), + title: Text(context.tr('adjustLyricsTiming')), content: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text( - 'Enter offset in milliseconds.\nPositive values delay lyrics, negative values advance them.', + Text( + context.tr('enterOffsetMs'), ), const SizedBox(height: 16), TextField( controller: offsetController, - decoration: const InputDecoration(labelText: 'Offset (ms)'), + decoration: InputDecoration(labelText: context.tr('offsetMs')), keyboardType: TextInputType.number, ), ], @@ -1133,7 +1135,7 @@ class _LyricsAdjustButton extends HookConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), + child: Text(context.tr('cancel')), ), TextButton( onPressed: () async { @@ -1195,11 +1197,11 @@ class _ViewToggleButton extends StatelessWidget { String getTooltip() { switch (viewMode.value) { case ViewMode.cover: - return 'Show Lyrics'; + return context.tr('showLyrics'); case ViewMode.lyrics: - return 'Show Queue'; + return context.tr('showQueue'); case ViewMode.queue: - return 'Show Cover'; + return context.tr('showCover'); } } @@ -1250,7 +1252,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(context.tr('noTracksInQueue'))); } return ReorderableListView.builder( @@ -1918,7 +1920,7 @@ class _LiveLyricsSyncDialog extends HookConsumerWidget { return Scaffold( appBar: AppBar( - title: const Text('Live Lyrics Sync'), + title: Text(context.tr('liveLyricsSync')), leading: IconButton( icon: const Icon(Symbols.close), onPressed: () => Navigator.of(context).pop(), @@ -1976,14 +1978,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(context.tr('offset').replaceAll('{}', tempOffset.value.toString())), ], ), ), @@ -1999,30 +1994,30 @@ class _LiveLyricsSyncDialog extends HookConsumerWidget { children: [ ElevatedButton.icon( icon: const Icon(Symbols.fast_rewind), - label: const Text('-100ms'), + label: Text(context.tr('minus100ms')), onPressed: () => tempOffset.value = (tempOffset.value - 100), ), ElevatedButton.icon( icon: const Icon(Symbols.skip_previous), - label: const Text('-10ms'), + label: Text(context.tr('plus10ms')), onPressed: () => tempOffset.value = (tempOffset.value - 10), ), ElevatedButton.icon( icon: const Icon(Symbols.refresh), - label: const Text('Reset'), + label: Text(context.tr('reset')), onPressed: () => tempOffset.value = 0, ), ElevatedButton.icon( icon: const Icon(Symbols.skip_next), - label: const Text('+10ms'), + label: Text(context.tr('plus10ms')), onPressed: () => tempOffset.value = (tempOffset.value + 10), ), ElevatedButton.icon( icon: const Icon(Symbols.fast_forward), - label: const Text('+100ms'), + label: Text(context.tr('plus100ms')), onPressed: () => tempOffset.value = (tempOffset.value + 100), ), @@ -2035,7 +2030,7 @@ class _LiveLyricsSyncDialog extends HookConsumerWidget { padding: const EdgeInsets.all(16.0), child: Column( children: [ - const Text('Fine Adjustment'), + Text(context.tr('fineAdjustment')), Slider( value: tempOffset.value.toDouble().clamp(-5000.0, 5000.0), min: -5000, @@ -2179,7 +2174,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(context.tr('onlyTimedLyricsCanBeSynced'))); } return StreamBuilder( @@ -2260,7 +2255,7 @@ class _LiveLyricsPreview extends HookConsumerWidget { }, ); } catch (e) { - return Center(child: Text('Error loading lyrics: $e')); + 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 870fa90..b867d82 100644 --- a/lib/ui/screens/playlist_detail_screen.dart +++ b/lib/ui/screens/playlist_detail_screen.dart @@ -1,9 +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/providers/audio_provider.dart'; import 'package:groovybox/ui/widgets/track_tile.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -48,10 +50,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(context.tr('noTracksInPlaylist')), ), ); } @@ -73,7 +75,7 @@ class PlaylistDetailScreen extends HookConsumerWidget { _playPlaylist(ref, tracks); }, icon: const Icon(Symbols.play_arrow), - label: const Text('Play All'), + label: Text(context.tr('playAll')), ), ), SizedBox( @@ -83,7 +85,7 @@ class PlaylistDetailScreen extends HookConsumerWidget { _addToQueue(ref, tracks); }, icon: const Icon(Symbols.queue_music), - label: const Text('Add to Queue'), + label: Text(context.tr('addToQueue')), ), ), ], @@ -204,3 +206,5 @@ class PlaylistDetailScreen extends HookConsumerWidget { ); } } + + diff --git a/lib/ui/screens/settings_screen.dart b/lib/ui/screens/settings_screen.dart index 142ace7..0033c42 100644 --- a/lib/ui/screens/settings_screen.dart +++ b/lib/ui/screens/settings_screen.dart @@ -1,6 +1,9 @@ +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/providers/settings_provider.dart'; import 'package:groovybox/providers/watch_folder_provider.dart'; import 'package:groovybox/providers/remote_provider.dart'; @@ -9,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 @@ -19,7 +22,7 @@ class SettingsScreen extends ConsumerWidget { final remoteProvidersAsync = ref.watch(remoteProvidersProvider); return Scaffold( - appBar: AppBar(title: const Text('Settings')), + appBar: AppBar(title: Text(context.tr('settingsTitle'))), body: settingsAsync.when( data: (settings) => Align( alignment: Alignment.topCenter, @@ -37,17 +40,17 @@ class SettingsScreen extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Auto Scan', - style: TextStyle( + Text( + context.tr('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(context.tr('autoScanMusicLibraries')), + subtitle: Text( + context.tr('autoScanDescription'), ), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(8)), @@ -58,9 +61,9 @@ class SettingsScreen extends ConsumerWidget { }, ), SwitchListTile( - title: const Text('Watch for changes'), - subtitle: const Text( - 'Monitor music libraries for file changes', + title: Text(context.tr('watchForChanges')), + subtitle: Text( + context.tr('watchForChangesDescription'), ), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(8)), @@ -89,9 +92,9 @@ class SettingsScreen extends ConsumerWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - 'Music Libraries', - style: TextStyle( + Text( + context.tr('musicLibraries'), + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), @@ -102,7 +105,7 @@ class SettingsScreen extends ConsumerWidget { onPressed: () => _scanLibraries(context, ref), icon: const Icon(Symbols.refresh), - tooltip: 'Scan Libraries', + tooltip: context.tr('scanLibraries'), visualDensity: const VisualDensity( horizontal: -4, vertical: -4, @@ -112,7 +115,7 @@ class SettingsScreen extends ConsumerWidget { onPressed: () => _addMusicLibrary(context, ref), icon: const Icon(Symbols.add), - tooltip: 'Add Music Library', + tooltip: context.tr('addMusicLibrary'), visualDensity: const VisualDensity( horizontal: -4, vertical: -4, @@ -122,9 +125,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( + context.tr('addMusicLibraryDescription'), + style: const TextStyle( color: Colors.grey, fontSize: 14, ), @@ -133,9 +136,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( + context.tr('noMusicLibrariesAdded'), + style: const TextStyle( color: Colors.grey, fontSize: 14, ), @@ -207,9 +210,9 @@ class SettingsScreen extends ConsumerWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - 'Remote Providers', - style: TextStyle( + Text( + context.tr('remoteProviders'), + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), @@ -220,7 +223,7 @@ class SettingsScreen extends ConsumerWidget { onPressed: () => _indexRemoteProviders(context, ref), icon: const Icon(Symbols.refresh), - tooltip: 'Index Remote Providers', + tooltip: context.tr('indexRemoteProviders'), visualDensity: const VisualDensity( horizontal: -4, vertical: -4, @@ -230,7 +233,7 @@ class SettingsScreen extends ConsumerWidget { onPressed: () => _addRemoteProvider(context, ref), icon: const Icon(Symbols.add), - tooltip: 'Add Remote Provider', + tooltip: context.tr('addRemoteProvider'), visualDensity: const VisualDensity( horizontal: -4, vertical: -4, @@ -240,9 +243,9 @@ class SettingsScreen extends ConsumerWidget { ), ], ), - const Text( - 'Connect to remote media servers like Jellyfin to access your music library.', - style: TextStyle( + Text( + context.tr('remoteProvidersDescription'), + style: const TextStyle( color: Colors.grey, fontSize: 14, ), @@ -251,9 +254,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( + context.tr('noRemoteProvidersAdded'), + style: const TextStyle( color: Colors.grey, fontSize: 14, ), @@ -319,21 +322,21 @@ class SettingsScreen extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Player Settings', - style: TextStyle( + Text( + context.tr('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( + context.tr('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(context.tr('defaultPlayerScreen')), + subtitle: Text( + context.tr('defaultPlayerScreenDescription'), ), trailing: DropdownButtonHideUnderline( child: DropdownButton( @@ -357,9 +360,9 @@ class SettingsScreen extends ConsumerWidget { ), ), ListTile( - title: const Text('Lyrics Mode'), - subtitle: const Text( - 'Choose how lyrics are displayed.', + title: Text(context.tr('lyricsMode')), + subtitle: Text( + context.tr('lyricsModeDescription'), ), trailing: DropdownButtonHideUnderline( child: DropdownButton( @@ -381,9 +384,9 @@ class SettingsScreen extends ConsumerWidget { ), ), SwitchListTile( - title: const Text('Continue Playing'), - subtitle: const Text( - 'Continue playing music after the queue is empty', + title: Text(context.tr('continuePlaying')), + subtitle: Text( + context.tr('continuePlayingDescription'), ), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(8)), @@ -400,27 +403,77 @@ class SettingsScreen extends ConsumerWidget { ), ), + // App Settings Section + Card( + margin: EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.tr('appSettings'), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ).padding(horizontal: 16, top: 16), + Text( + 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: context.locale, + onChanged: (Locale? value) { + if (value != null) { + EasyLocalization.of(context)!.setLocale(value); + } else { + EasyLocalization.of(context)!.resetLocale(); + } + }, + items: [ + DropdownMenuItem( + value: const Locale('en'), + child: Text(context.tr('english')), + ), + DropdownMenuItem( + value: const Locale('zh'), + child: Text(context.tr('chinese')), + ), + ], + ), + ), + ), + const SizedBox(height: 8), + ], + ), + ), + // Database Management Section Card( margin: EdgeInsets.zero, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Database Management', - style: TextStyle( + Text( + context.tr('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( + context.tr('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(context.tr('resetTrackDatabase')), + subtitle: Text( + context.tr('resetTrackDatabaseDescription'), ), trailing: ElevatedButton( onPressed: () => _resetTrackDatabase(context, ref), @@ -428,7 +481,7 @@ class SettingsScreen extends ConsumerWidget { backgroundColor: Colors.red, foregroundColor: Colors.white, ), - child: const Text('Reset'), + child: Text(context.tr('reset')), ), ), const SizedBox(height: 8), @@ -457,14 +510,12 @@ 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(context.tr('addedMusicLibrary', args: [path]))), ); } } catch (e) { if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Error adding library: $e'))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.tr('errorAddingLibrary', args: [e.toString()])))); } } } @@ -477,14 +528,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(context.tr('librariesScannedSuccessfully'))), ); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of( context, - ).showSnackBar(SnackBar(content: Text('Error scanning libraries: $e'))); + ).showSnackBar(SnackBar(content: Text(context.tr('errorScanningLibraries', args: [e.toString()])))); } } } @@ -501,8 +552,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(context.tr('noActiveRemoteProviders')), ), ); } @@ -521,7 +572,7 @@ class SettingsScreen extends ConsumerWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - 'Indexed ${activeProviders.length} remote provider(s)', + context.tr('indexedRemoteProviders', args: [activeProviders.length.toString()]), ), ), ); @@ -555,27 +606,27 @@ class SettingsScreen extends ConsumerWidget { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Add Remote Provider'), + title: Text(context.tr('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: context.tr('serverUrl'), + hintText: context.tr('serverUrlHint'), ), keyboardType: TextInputType.url, ), const SizedBox(height: 16), TextField( controller: usernameController, - decoration: const InputDecoration(labelText: 'Username'), + decoration: InputDecoration(labelText: context.tr('username')), ), const SizedBox(height: 16), TextField( controller: passwordController, - decoration: const InputDecoration(labelText: 'Password'), + decoration: InputDecoration(labelText: context.tr('password')), obscureText: true, ), ], @@ -583,7 +634,7 @@ class SettingsScreen extends ConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), + child: Text(context.tr('cancel')), ), TextButton( onPressed: () async { @@ -593,7 +644,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(context.tr('allFieldsRequired'))), ); return; } @@ -605,19 +656,19 @@ class SettingsScreen extends ConsumerWidget { Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Added remote provider: $serverUrl'), + content: Text(context.tr('addedRemoteProvider', args: [serverUrl])), ), ); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error adding provider: $e')), + SnackBar(content: Text(context.tr('errorAddingProvider', args: [e.toString()]))), ); } } }, - child: const Text('Add'), + child: Text(context.tr('add')), ), ], ), @@ -628,14 +679,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(context.tr('resetTrackDatabase')), + content: Text( + context.tr('confirmResetTrackDatabase'), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), + child: Text(context.tr('cancel')), ), TextButton( onPressed: () async { @@ -647,21 +698,21 @@ class SettingsScreen extends ConsumerWidget { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Track database has been reset'), + SnackBar( + content: Text(context.tr('trackDatabaseReset')), ), ); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error resetting database: $e')), + SnackBar(content: Text(context.tr('errorResettingDatabase', args: [e.toString()]))), ); } } }, style: TextButton.styleFrom(foregroundColor: Colors.red), - child: const Text('Reset'), + child: Text(context.tr('reset')), ), ], ), diff --git a/lib/ui/tabs/albums_tab.dart b/lib/ui/tabs/albums_tab.dart index cdd013e..adb3b64 100644 --- a/lib/ui/tabs/albums_tab.dart +++ b/lib/ui/tabs/albums_tab.dart @@ -1,6 +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/ui/screens/album_detail_screen.dart'; import 'package:groovybox/ui/widgets/universal_image.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -22,7 +24,7 @@ class AlbumsTab extends HookConsumerWidget { final albums = snapshot.data!; if (albums.isEmpty) { - return const Center(child: Text('No albums found')); + return Center(child: Text(context.tr('noAlbumsFound'))); } return GridView.builder( @@ -87,3 +89,5 @@ class AlbumsTab extends HookConsumerWidget { ); } } + + diff --git a/lib/ui/tabs/playlists_tab.dart b/lib/ui/tabs/playlists_tab.dart index 5955440..879053b 100644 --- a/lib/ui/tabs/playlists_tab.dart +++ b/lib/ui/tabs/playlists_tab.dart @@ -1,6 +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/ui/screens/playlist_detail_screen.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -19,30 +21,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(context.tr('createOne')), + subtitle: Text(context.tr('addNewPlaylist')), onTap: () async { final nameController = TextEditingController(); final name = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('New Playlist'), + title: Text(context.tr('newPlaylist')), content: TextField( controller: nameController, - decoration: const InputDecoration( - labelText: 'Playlist Name', + decoration: InputDecoration( + labelText: context.tr('playlistName'), ), autofocus: true, ), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + child: Text(context.tr('cancel')), ), TextButton( onPressed: () => Navigator.pop(context, nameController.text), - child: const Text('Create'), + child: Text(context.tr('create')), ), ], ), @@ -63,7 +65,7 @@ class PlaylistsTab extends HookConsumerWidget { final playlists = snapshot.data!; if (playlists.isEmpty) { - return const Center(child: Text('No playlists yet')); + return Center(child: Text(context.tr('noPlaylistsYet'))); } return ListView.builder( @@ -74,7 +76,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}', + '${context.tr('createdAt')} ${playlist.createdAt.day}/${playlist.createdAt.month}/${playlist.createdAt.year}', ), trailing: IconButton( icon: const Icon(Symbols.delete), @@ -100,3 +102,5 @@ class PlaylistsTab extends HookConsumerWidget { ); } } + + diff --git a/lib/ui/widgets/mini_player.dart b/lib/ui/widgets/mini_player.dart index b7e1c00..775be89 100644 --- a/lib/ui/widgets/mini_player.dart +++ b/lib/ui/widgets/mini_player.dart @@ -1,3 +1,4 @@ +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; @@ -174,7 +175,7 @@ class _MobileMiniPlayer extends HookConsumerWidget { overflow: TextOverflow.ellipsis, ), Text( - currentMetadata?.artist ?? 'Unknown Artist', + 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 ?? 'Unknown Artist', + 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: [ - const Text('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 const Center(child: Text('No tracks in queue')); + 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? ?? - 'Unknown Artist', + 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? ?? - 'Unknown Artist', + 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 bac9a1f..bdc0612 100644 --- a/lib/ui/widgets/track_tile.dart +++ b/lib/ui/widgets/track_tile.dart @@ -1,3 +1,4 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:groovybox/data/db.dart' as db; import 'package:groovybox/ui/widgets/universal_image.dart'; @@ -39,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), ), @@ -48,7 +49,7 @@ class TrackTile extends StatelessWidget { leading: Row( mainAxisSize: MainAxisSize.min, children: [ - ?leading, + if (leading != null) leading!, AspectRatio( aspectRatio: 1, child: UniversalImage( @@ -73,11 +74,11 @@ class TrackTile extends StatelessWidget { ), ), subtitle: Text( - '${track.artist ?? 'Unknown Artist'} • ${_formatDuration(track.duration)}', + '${track.artist ?? context.tr('unknownArtist')} - ${_formatDuration(track.duration)}', 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 @@ -96,4 +97,4 @@ class TrackTile extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 9da184a..526cf44 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 @@ -62,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: @@ -88,10 +92,12 @@ 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: - assets/images/ + - assets/locales/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images