diff --git a/.gitignore b/.gitignore index bd805d8..d651c53 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,6 @@ app.*.map.json # 添加以下内容 **/android/key.properties **/android/app/upload-keystore.jks + +# mise +mise.local.toml diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..bcf09d3 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,3 @@ +arb-dir: lib/l10n +template-arb-file: app_zh.arb +output-localization-file: app_localizations.dart diff --git a/lib/common/extensions/mark_status_localizations.dart b/lib/common/extensions/mark_status_localizations.dart new file mode 100644 index 0000000..c9627cb --- /dev/null +++ b/lib/common/extensions/mark_status_localizations.dart @@ -0,0 +1,19 @@ +import 'package:asmrapp/data/models/mark_status.dart'; +import 'package:asmrapp/l10n/app_localizations.dart'; + +extension MarkStatusLocalizations on MarkStatus { + String localizedLabel(AppLocalizations l10n) { + switch (this) { + case MarkStatus.wantToListen: + return l10n.markStatusWantToListen; + case MarkStatus.listening: + return l10n.markStatusListening; + case MarkStatus.listened: + return l10n.markStatusListened; + case MarkStatus.relistening: + return l10n.markStatusRelistening; + case MarkStatus.onHold: + return l10n.markStatusOnHold; + } + } +} diff --git a/lib/common/utils/playlist_localizations.dart b/lib/common/utils/playlist_localizations.dart new file mode 100644 index 0000000..95c9bfc --- /dev/null +++ b/lib/common/utils/playlist_localizations.dart @@ -0,0 +1,12 @@ +import 'package:asmrapp/l10n/app_localizations.dart'; + +String localizedPlaylistName(String? name, AppLocalizations l10n) { + switch (name) { + case '__SYS_PLAYLIST_MARKED': + return l10n.playlistSystemMarked; + case '__SYS_PLAYLIST_LIKED': + return l10n.playlistSystemLiked; + default: + return name ?? ''; + } +} diff --git a/lib/core/audio/audio_player_handler.dart b/lib/core/audio/audio_player_handler.dart index ab973e3..09f7bee 100644 --- a/lib/core/audio/audio_player_handler.dart +++ b/lib/core/audio/audio_player_handler.dart @@ -9,7 +9,7 @@ class AudioPlayerHandler extends BaseAudioHandler { AudioPlayerHandler(this._player, this._eventHub) { AppLogger.debug('AudioPlayerHandler 初始化'); - + // 改为监听 EventHub _eventHub.playbackState.listen((event) { final state = PlaybackState( diff --git a/lib/core/audio/audio_player_service.dart b/lib/core/audio/audio_player_service.dart index 33deb0e..e8411ad 100644 --- a/lib/core/audio/audio_player_service.dart +++ b/lib/core/audio/audio_player_service.dart @@ -24,13 +24,13 @@ class AudioPlayerService implements IAudioPlayerService { AudioPlayerService._internal({ required PlaybackEventHub eventHub, required IPlaybackStateRepository stateRepository, - }) : _eventHub = eventHub, - _stateRepository = stateRepository { + }) : _eventHub = eventHub, + _stateRepository = stateRepository { _init(); } static AudioPlayerService? _instance; - + factory AudioPlayerService({ required PlaybackEventHub eventHub, required IPlaybackStateRepository stateRepository, @@ -130,14 +130,15 @@ class AudioPlayerService implements IAudioPlayerService { try { AppLogger.debug('开始恢复播放状态'); final state = await _stateManager.loadState(); - + if (state == null) { AppLogger.debug('没有可恢复的播放状态'); return; } AppLogger.debug('已加载保存的状态: workId=${state.work.id}'); - AppLogger.debug('播放列表信息: 长度=${state.playlist.length}, 索引=${state.currentIndex}'); + AppLogger.debug( + '播放列表信息: 长度=${state.playlist.length}, 索引=${state.currentIndex}'); if (state.playlist.isEmpty) { AppLogger.debug('保存的播放列表为空,跳过恢复'); diff --git a/lib/core/audio/cache/audio_cache_manager.dart b/lib/core/audio/cache/audio_cache_manager.dart index 9ccea7b..6467f18 100644 --- a/lib/core/audio/cache/audio_cache_manager.dart +++ b/lib/core/audio/cache/audio_cache_manager.dart @@ -1,9 +1,10 @@ +import 'dart:convert'; import 'dart:io'; -import 'package:path_provider/path_provider.dart'; + +import 'package:asmrapp/utils/logger.dart'; import 'package:crypto/crypto.dart'; -import 'dart:convert'; import 'package:just_audio/just_audio.dart'; -import 'package:asmrapp/utils/logger.dart'; +import 'package:path_provider/path_provider.dart'; /// 音频缓存管理器 /// 负责管理音频文件的缓存,对外隐藏具体的缓存实现 @@ -18,10 +19,10 @@ class AudioCacheManager { final cacheFile = await _getCacheFile(url); final fileName = _generateFileName(url); AppLogger.debug('准备创建音频源 - URL: $url, 缓存文件名: $fileName'); - + // 检查缓存文件是否存在且有效 final isValid = await _isCacheValid(cacheFile, fileName); - + if (isValid) { AppLogger.debug('[$fileName] 使用已有缓存文件'); return _createCachingSource(url, cacheFile); @@ -29,7 +30,6 @@ class AudioCacheManager { AppLogger.debug('[$fileName] 创建新的缓存源'); return _createCachingSource(url, cacheFile); - } catch (e) { AppLogger.error('创建缓存音频源失败,使用非缓存源', e); return ProgressiveAudioSource(Uri.parse(url)); @@ -41,7 +41,7 @@ class AudioCacheManager { try { final cacheDir = await _getCacheDir(); final files = await cacheDir.list().toList(); - + // 按修改时间排序 files.sort((a, b) { return a.statSync().modified.compareTo(b.statSync().modified); @@ -51,7 +51,7 @@ class AudioCacheManager { for (var file in files) { if (file is File) { final stat = await file.stat(); - + // 检查是否过期 if (DateTime.now().difference(stat.modified) > _cacheExpiration) { await file.delete(); @@ -59,7 +59,7 @@ class AudioCacheManager { } totalSize += stat.size; - + // 如果总大小超过限制,删除最旧的文件 if (totalSize > _maxCacheSize) { await file.delete(); @@ -76,7 +76,7 @@ class AudioCacheManager { try { final cacheDir = await _getCacheDir(); final files = await cacheDir.list().toList(); - + var totalSize = 0; for (var file in files) { if (file is File) { @@ -94,10 +94,7 @@ class AudioCacheManager { /// 创建缓存音频源 static AudioSource _createCachingSource(String url, File cacheFile) { - return LockCachingAudioSource( - Uri.parse(url), - cacheFile: cacheFile - ); + return LockCachingAudioSource(Uri.parse(url), cacheFile: cacheFile); } /// 检查缓存是否有效 @@ -112,9 +109,9 @@ class AudioCacheManager { final stat = await cacheFile.stat(); final size = stat.size; final age = DateTime.now().difference(stat.modified); - + AppLogger.debug('[$fileName] 缓存验证: 大小=${size}bytes, 年龄=$age'); - + // 移除单个文件大小检查,只保留过期检查 if (age > _cacheExpiration) { AppLogger.debug('[$fileName] 缓存无效: 文件过期 ($age > $_cacheExpiration)'); @@ -153,4 +150,4 @@ class AudioCacheManager { } return audioCacheDir; } -} \ No newline at end of file +} diff --git a/lib/core/audio/controllers/playback_controller.dart b/lib/core/audio/controllers/playback_controller.dart index 4f772c4..b9451a7 100644 --- a/lib/core/audio/controllers/playback_controller.dart +++ b/lib/core/audio/controllers/playback_controller.dart @@ -7,7 +7,6 @@ import '../utils/audio_error_handler.dart'; import 'package:asmrapp/data/models/files/child.dart'; import 'package:asmrapp/data/models/works/work.dart'; - class PlaybackController { final AudioPlayer _player; final PlaybackStateManager _stateManager; @@ -17,16 +16,17 @@ class PlaybackController { required AudioPlayer player, required PlaybackStateManager stateManager, required ConcatenatingAudioSource playlist, - }) : _player = player, - _stateManager = stateManager, - _playlist = playlist; + }) : _player = player, + _stateManager = stateManager, + _playlist = playlist; // 基础播放控制 Future play() => _player.play(); Future pause() => _player.pause(); Future stop() => _player.stop(); - Future seek(Duration position, {int? index}) => _player.seek(position, index: index); - + Future seek(Duration position, {int? index}) => + _player.seek(position, index: index); + // 播放列表控制 Future next() async { try { @@ -66,9 +66,7 @@ class PlaybackController { AppLogger.debug('获取到上一个文件: ${previousFile?.title}'); if (previousFile != null) { _updateTrackAndContext( - previousFile, - _stateManager.currentContext!.work - ); + previousFile, _stateManager.currentContext!.work); AppLogger.debug('执行切换到上一曲'); await _player.seekToPrevious(); } @@ -87,11 +85,14 @@ class PlaybackController { } // 播放上下文设置 - Future setPlaybackContext(PlaybackContext context, {Duration? initialPosition}) async { + Future setPlaybackContext(PlaybackContext context, + {Duration? initialPosition}) async { try { - AppLogger.debug('准备设置播放上下文: workId=${context.work.id}, file=${context.currentFile.title}'); - AppLogger.debug('播放列表状态: 长度=${context.playlist.length}, 当前索引=${context.currentIndex}'); - + AppLogger.debug( + '准备设置播放上下文: workId=${context.work.id}, file=${context.currentFile.title}'); + AppLogger.debug( + '播放列表状态: 长度=${context.playlist.length}, 当前索引=${context.currentIndex}'); + // 验证上下文 try { context.validate(); @@ -99,19 +100,19 @@ class PlaybackController { AppLogger.error('播放上下文验证失败', e); rethrow; } - + // 1. 先停止当前播放 AppLogger.debug('停止当前播放'); await _player.stop(); - + // 2. 等待播放器就绪 AppLogger.debug('暂停播放器'); await _player.pause(); - + // 3. 更新上下文 AppLogger.debug('更新播放上下文'); _stateManager.updateContext(context); - + // 4. 设置新的播放源 AppLogger.debug('设置播放源: 初始位置=${initialPosition?.inMilliseconds}ms'); try { @@ -131,11 +132,11 @@ class PlaybackController { // 删掉,会导致播放器索引回到0 // AppLogger.debug('等待播放器加载'); // await _player.load(); - + // 6. 更新轨道信息 AppLogger.debug('更新轨道信息'); _updateTrackAndContext(context.currentFile, context.work); - + AppLogger.debug('播放上下文设置完成'); } catch (e, stack) { AppLogger.error('设置播放上下文失败', e, stack); @@ -154,4 +155,4 @@ class PlaybackController { AppLogger.debug('更新轨道和上下文: file=${file.title}'); _stateManager.updateTrackAndContext(file, work); } -} \ No newline at end of file +} diff --git a/lib/core/audio/events/playback_event.dart b/lib/core/audio/events/playback_event.dart index cc48b0a..d470f61 100644 --- a/lib/core/audio/events/playback_event.dart +++ b/lib/core/audio/events/playback_event.dart @@ -57,4 +57,4 @@ class InitialStateEvent extends PlaybackEvent { final AudioTrackInfo? track; final PlaybackContext? context; InitialStateEvent(this.track, this.context); -} \ No newline at end of file +} diff --git a/lib/core/audio/events/playback_event_hub.dart b/lib/core/audio/events/playback_event_hub.dart index 90fc9a1..e9fe26a 100644 --- a/lib/core/audio/events/playback_event_hub.dart +++ b/lib/core/audio/events/playback_event_hub.dart @@ -6,33 +6,32 @@ class PlaybackEventHub { final _eventSubject = PublishSubject(); // 分类后的特定事件流 - late final Stream playbackState = _eventSubject - .whereType() - .distinct(); - - late final Stream trackChange = _eventSubject - .whereType(); - - late final Stream contextChange = _eventSubject - .whereType(); - + late final Stream playbackState = + _eventSubject.whereType().distinct(); + + late final Stream trackChange = + _eventSubject.whereType(); + + late final Stream contextChange = + _eventSubject.whereType(); + late final Stream playbackProgress = _eventSubject .whereType() .distinct((prev, next) => prev.position == next.position); - - late final Stream errors = _eventSubject - .whereType(); + + late final Stream errors = + _eventSubject.whereType(); // 添加新的事件流 - late final Stream initialState = _eventSubject - .whereType(); - - late final Stream requestInitialState = _eventSubject - .whereType(); + late final Stream initialState = + _eventSubject.whereType(); + + late final Stream requestInitialState = + _eventSubject.whereType(); // 发送事件 void emit(PlaybackEvent event) => _eventSubject.add(event); // 资源释放 void dispose() => _eventSubject.close(); -} \ No newline at end of file +} diff --git a/lib/core/audio/i_audio_player_service.dart b/lib/core/audio/i_audio_player_service.dart index 1c6685d..0766c84 100644 --- a/lib/core/audio/i_audio_player_service.dart +++ b/lib/core/audio/i_audio_player_service.dart @@ -13,7 +13,7 @@ abstract class IAudioPlayerService { // 上下文管理 Future playWithContext(PlaybackContext context); - + // 状态访问 AudioTrackInfo? get currentTrack; PlaybackContext? get currentContext; diff --git a/lib/core/audio/models/file_path.dart b/lib/core/audio/models/file_path.dart index 9dbbf77..2536355 100644 --- a/lib/core/audio/models/file_path.dart +++ b/lib/core/audio/models/file_path.dart @@ -12,7 +12,7 @@ class FilePath { static String? getPath(Child targetFile, Files root) { AppLogger.debug('开始查找文件路径: ${targetFile.title}'); final segments = _findPathSegments(root.children, targetFile); - + if (segments == null) { AppLogger.debug('未找到文件路径'); return null; @@ -24,23 +24,23 @@ class FilePath { } /// 递归查找文件路径段 - static List? _findPathSegments(List? children, Child targetFile, [List currentPath = const []]) { + static List? _findPathSegments( + List? children, Child targetFile, + [List currentPath = const []]) { if (children == null) return null; for (final child in children) { - if (child.title == targetFile.title && - child.mediaDownloadUrl == targetFile.mediaDownloadUrl && + if (child.title == targetFile.title && + child.mediaDownloadUrl == targetFile.mediaDownloadUrl && child.type == targetFile.type && - child.size == targetFile.size) { // size 作为额外验证 + child.size == targetFile.size) { + // size 作为额外验证 return [...currentPath, child.title!]; } if (child.type == 'folder' && child.children != null) { final result = _findPathSegments( - child.children, - targetFile, - [...currentPath, child.title!] - ); + child.children, targetFile, [...currentPath, child.title!]); if (result != null) return result; } } @@ -52,7 +52,7 @@ class FilePath { /// 返回与目标文件在同一目录下的所有文件 static List getSiblings(Child targetFile, Files root) { AppLogger.debug('开始获取同级文件: ${targetFile.title}'); - + // 获取目标文件的路径 final path = getPath(targetFile, root); if (path == null) { @@ -62,7 +62,8 @@ class FilePath { // 获取父目录路径 final lastSeparator = path.lastIndexOf(separator); - final parentPath = lastSeparator > 0 ? path.substring(0, lastSeparator) : separator; + final parentPath = + lastSeparator > 0 ? path.substring(0, lastSeparator) : separator; AppLogger.debug('父目录路径: $parentPath'); // 查找父目录内容 @@ -93,18 +94,17 @@ class FilePath { if (path == separator) return children; // 分割路径 - final segments = path.split(separator) - ..removeWhere((s) => s.isEmpty); - + final segments = path.split(separator)..removeWhere((s) => s.isEmpty); + List? current = children; - + // 逐级查找目录 for (final segment in segments) { final nextDir = current?.firstWhere( (child) => child.title == segment && child.type == 'folder', orElse: () => Child(), ); - + if (nextDir?.title == null) return null; current = nextDir?.children; } @@ -121,7 +121,7 @@ class FilePath { if (children == null) return null; List? audioFolderPath; - + void findPath(Child folder, List currentPath) { if (audioFolderPath != null) return; @@ -144,7 +144,8 @@ class FilePath { // 如果当前目录没有音频文件,递归检查子目录 for (final child in folder.children!) { if (child.type == 'folder') { - List newPath = List.from(currentPath)..add(child.title ?? ''); + List newPath = List.from(currentPath) + ..add(child.title ?? ''); findPath(child, newPath); } } @@ -168,4 +169,4 @@ class FilePath { if (path == null || folderName == null) return false; return path.contains(folderName); } -} \ No newline at end of file +} diff --git a/lib/core/audio/models/play_mode.dart b/lib/core/audio/models/play_mode.dart index e549b85..8dcc92d 100644 --- a/lib/core/audio/models/play_mode.dart +++ b/lib/core/audio/models/play_mode.dart @@ -1,5 +1,5 @@ enum PlayMode { - single, // 单曲循环 - loop, // 列表循环 - sequence, // 顺序播放 -} \ No newline at end of file + single, // 单曲循环 + loop, // 列表循环 + sequence, // 顺序播放 +} diff --git a/lib/core/audio/models/playback_context.dart b/lib/core/audio/models/playback_context.dart index 4508b12..9c7f1ec 100644 --- a/lib/core/audio/models/playback_context.dart +++ b/lib/core/audio/models/playback_context.dart @@ -1,10 +1,10 @@ +import 'package:asmrapp/core/audio/models/file_path.dart'; +import 'package:asmrapp/core/audio/models/play_mode.dart'; import 'package:asmrapp/core/audio/utils/audio_error_handler.dart'; -import 'package:asmrapp/data/models/works/work.dart'; -import 'package:asmrapp/data/models/files/files.dart'; import 'package:asmrapp/data/models/files/child.dart'; +import 'package:asmrapp/data/models/files/files.dart'; +import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/utils/logger.dart'; -import 'package:asmrapp/core/audio/models/play_mode.dart'; -import 'package:asmrapp/core/audio/models/file_path.dart'; class PlaybackContext { final Work work; @@ -21,7 +21,7 @@ class PlaybackContext { '无效的播放列表状态:播放列表为空', ); } - + if (currentIndex < 0 || currentIndex >= playlist.length) { throw AudioError( AudioErrorType.state, @@ -55,8 +55,9 @@ class PlaybackContext { PlayMode playMode = PlayMode.sequence, }) { final playlist = _getPlaylistFromSameDirectory(currentFile, files); - final currentIndex = playlist.indexWhere((file) => file.title == currentFile.title); - + final currentIndex = + playlist.indexWhere((file) => file.title == currentFile.title); + return PlaybackContext._( work: work, files: files, @@ -68,7 +69,8 @@ class PlaybackContext { } // 获取同级文件列表 - static List _getPlaylistFromSameDirectory(Child currentFile, Files files) { + static List _getPlaylistFromSameDirectory( + Child currentFile, Files files) { // AppLogger.debug('开始获取播放列表...'); // AppLogger.debug('当前文件: ${currentFile.title}'); // AppLogger.debug('当前文件类型: ${currentFile.type}'); @@ -76,7 +78,7 @@ class PlaybackContext { // 获取当前文件的扩展名 final extension = currentFile.title?.split('.').last.toLowerCase(); // AppLogger.debug('当前文件扩展名: $extension'); - + if (extension != 'mp3' && extension != 'wav') { AppLogger.debug('不支持的文件类型: $extension'); return []; @@ -84,17 +86,18 @@ class PlaybackContext { // 使用 FilePath 获取同级文件 final siblings = FilePath.getSiblings(currentFile, files); - + // 过滤出相同扩展名的文件 - final playlist = siblings.where((file) => - file.title?.toLowerCase().endsWith('.$extension') ?? false - ).toList(); - + final playlist = siblings + .where((file) => + file.title?.toLowerCase().endsWith('.$extension') ?? false) + .toList(); + // AppLogger.debug('找到 ${playlist.length} 个可播放文件:'); // for (var file in playlist) { // AppLogger.debug('- [${file.type}] ${file.title} (URL: ${file.mediaDownloadUrl != null ? '有' : '无'})'); // } - + return playlist; } @@ -107,10 +110,10 @@ class PlaybackContext { // 获取下一曲(考虑播放模式) Child? getNextFile() { if (playlist.isEmpty) return null; - + switch (playMode) { case PlayMode.single: - return currentFile; // 单曲循环返回当前文件 + return currentFile; // 单曲循环返回当前文件 case PlayMode.loop: // 列表循环:最后一首返回第一首,否则返回下一首 return hasNext ? playlist[currentIndex + 1] : playlist[0]; @@ -123,13 +126,15 @@ class PlaybackContext { // 获取上一曲 Child? getPreviousFile() { if (playlist.isEmpty) return null; - + switch (playMode) { case PlayMode.single: return currentFile; case PlayMode.loop: // 列表循环:第一首返回最后一首,否则返回上一首 - return hasPrevious ? playlist[currentIndex - 1] : playlist[playlist.length - 1]; + return hasPrevious + ? playlist[currentIndex - 1] + : playlist[playlist.length - 1]; case PlayMode.sequence: // 顺序播放:有上一首则返回,否则返回null return hasPrevious ? playlist[currentIndex - 1] : null; @@ -166,10 +171,10 @@ class PlaybackContext { // 便捷方法:获取可播放文件列表 List getPlayableFiles() { if (files.children == null) return []; - return files.children!.where((file) => - file.mediaDownloadUrl != null && - file.type?.toLowerCase() != 'vtt' - ).toList(); + return files.children! + .where((file) => + file.mediaDownloadUrl != null && file.type?.toLowerCase() != 'vtt') + .toList(); } // 工具方法:获取文件名(不含扩展名) @@ -177,4 +182,4 @@ class PlaybackContext { if (filename == null) return null; return filename.replaceAll(RegExp(r'\.[^.]+$'), ''); } -} \ No newline at end of file +} diff --git a/lib/core/audio/models/subtitle.dart b/lib/core/audio/models/subtitle.dart index 3d6388e..47dd5e6 100644 --- a/lib/core/audio/models/subtitle.dart +++ b/lib/core/audio/models/subtitle.dart @@ -1,9 +1,9 @@ import 'dart:math' as math; enum SubtitleState { - current, // 当前播放的字幕 - waiting, // 即将播放的字幕 - passed // 已经播放过的字幕 + current, // 当前播放的字幕 + waiting, // 即将播放的字幕 + passed // 已经播放过的字幕 } class Subtitle { @@ -41,15 +41,17 @@ class SubtitleList { final List subtitles; int _currentIndex = -1; - SubtitleList(List subtitles) - : subtitles = subtitles.asMap().entries.map( - (entry) => Subtitle( - start: entry.value.start, - end: entry.value.end, - text: entry.value.text, - index: entry.key, - ) - ).toList(); + SubtitleList(List subtitles) + : subtitles = subtitles + .asMap() + .entries + .map((entry) => Subtitle( + start: entry.value.start, + end: entry.value.end, + text: entry.value.text, + index: entry.key, + )) + .toList(); SubtitleWithState? getCurrentSubtitle(Duration position) { if (subtitles.isEmpty) return null; @@ -73,7 +75,7 @@ class SubtitleList { return SubtitleWithState(subtitle, SubtitleState.current); } // 如果已经超过了当前字幕,但还没到下一个字幕 - if (position > subtitle.end && + if (position > subtitle.end && (i == subtitles.length - 1 || position < subtitles[i + 1].start)) { return SubtitleWithState(subtitle, SubtitleState.passed); } @@ -92,18 +94,20 @@ class SubtitleList { (Subtitle?, Subtitle?, Subtitle?) getCurrentContext() { if (_currentIndex == -1) return (null, null, null); - + final previous = _currentIndex > 0 ? subtitles[_currentIndex - 1] : null; final current = subtitles[_currentIndex]; - final next = _currentIndex < subtitles.length - 1 ? subtitles[_currentIndex + 1] : null; - + final next = _currentIndex < subtitles.length - 1 + ? subtitles[_currentIndex + 1] + : null; + return (previous, current, next); } static SubtitleList parse(String vttContent) { final lines = vttContent.split('\n'); final subtitles = []; - + int i = 0; while (i < lines.length && !lines[i].contains('-->')) { i++; @@ -111,13 +115,13 @@ class SubtitleList { while (i < lines.length) { final line = lines[i].trim(); - + if (line.contains('-->')) { final times = line.split('-->'); if (times.length == 2) { final start = _parseTimestamp(times[0].trim()); final end = _parseTimestamp(times[1].trim()); - + i++; String text = ''; while (i < lines.length && lines[i].trim().isNotEmpty) { @@ -125,7 +129,7 @@ class SubtitleList { text += lines[i].trim(); i++; } - + if (start != null && end != null && text.isNotEmpty) { subtitles.add(Subtitle( start: start, @@ -151,7 +155,8 @@ class SubtitleList { hours: int.parse(parts[0]), minutes: int.parse(parts[1]), seconds: int.parse(seconds[0]), - milliseconds: seconds.length > 1 ? int.parse(seconds[1].padRight(3, '0')) : 0, + milliseconds: + seconds.length > 1 ? int.parse(seconds[1].padRight(3, '0')) : 0, ); } } catch (e) { @@ -166,4 +171,4 @@ class SubtitleWithState { final SubtitleState state; SubtitleWithState(this.subtitle, this.state); -} \ No newline at end of file +} diff --git a/lib/core/audio/state/playback_state_manager.dart b/lib/core/audio/state/playback_state_manager.dart index 5f9cfd8..cf542ef 100644 --- a/lib/core/audio/state/playback_state_manager.dart +++ b/lib/core/audio/state/playback_state_manager.dart @@ -1,22 +1,23 @@ import 'dart:async'; + +import 'package:asmrapp/data/models/files/child.dart'; +import 'package:asmrapp/data/models/playback/playback_state.dart'; +import 'package:asmrapp/data/models/works/work.dart'; import 'package:just_audio/just_audio.dart'; + +import '../events/playback_event.dart'; +import '../events/playback_event_hub.dart'; import '../models/audio_track_info.dart'; import '../models/playback_context.dart'; +import '../storage/i_playback_state_repository.dart'; import '../utils/audio_error_handler.dart'; import '../utils/track_info_creator.dart'; -import 'package:asmrapp/data/models/playback/playback_state.dart'; -import '../storage/i_playback_state_repository.dart'; -import '../events/playback_event.dart'; -import '../events/playback_event_hub.dart'; -import 'package:asmrapp/data/models/files/child.dart'; -import 'package:asmrapp/data/models/works/work.dart'; - class PlaybackStateManager { final AudioPlayer _player; final PlaybackEventHub _eventHub; final IPlaybackStateRepository _stateRepository; - + AudioTrackInfo? _currentTrack; PlaybackContext? _currentContext; @@ -26,9 +27,9 @@ class PlaybackStateManager { required AudioPlayer player, required PlaybackEventHub eventHub, required IPlaybackStateRepository stateRepository, - }) : _player = player, - _eventHub = eventHub, - _stateRepository = stateRepository; + }) : _player = player, + _eventHub = eventHub, + _stateRepository = stateRepository; // 初始化状态监听 void initStateListeners() { @@ -44,7 +45,7 @@ class PlaybackStateManager { _player.playerStateStream.listen((state) async { final position = _player.position; final duration = _player.duration; - + // 转换并发送到 EventHub _eventHub.emit(PlaybackStateEvent(state, position, duration)); @@ -55,10 +56,7 @@ class PlaybackStateManager { }); _player.positionStream.listen((position) { - _eventHub.emit(PlaybackProgressEvent( - position, - _player.bufferedPosition - )); + _eventHub.emit(PlaybackProgressEvent(position, _player.bufferedPosition)); }); } @@ -72,7 +70,8 @@ class PlaybackStateManager { void updateTrackInfo(AudioTrackInfo track) { _currentTrack = track; - _eventHub.emit(TrackChangeEvent(track, _currentContext!.currentFile, _currentContext!.work)); + _eventHub.emit(TrackChangeEvent( + track, _currentContext!.currentFile, _currentContext!.work)); } void updateTrackAndContext(Child file, Work work) { @@ -80,7 +79,7 @@ class PlaybackStateManager { final newContext = _currentContext!.copyWithFile(file); updateContext(newContext); } - + final trackInfo = TrackInfoCreator.createFromFile(file, work); updateTrackInfo(trackInfo); } @@ -115,7 +114,7 @@ class PlaybackStateManager { position: (_player.position).inMilliseconds, timestamp: DateTime.now().toIso8601String(), ); - + await _stateRepository.saveState(state); } catch (e, stack) { AudioErrorHandler.handleError( @@ -145,10 +144,7 @@ class PlaybackStateManager { // 处理初始状态请求 _subscriptions.add( _eventHub.requestInitialState.listen((_) { - _eventHub.emit(InitialStateEvent( - _currentTrack, - _currentContext - )); + _eventHub.emit(InitialStateEvent(_currentTrack, _currentContext)); }), ); } @@ -159,4 +155,4 @@ class PlaybackStateManager { } _subscriptions.clear(); } -} \ No newline at end of file +} diff --git a/lib/core/audio/storage/i_playback_state_repository.dart b/lib/core/audio/storage/i_playback_state_repository.dart index 3c56acc..6a910bf 100644 --- a/lib/core/audio/storage/i_playback_state_repository.dart +++ b/lib/core/audio/storage/i_playback_state_repository.dart @@ -3,4 +3,4 @@ import 'package:asmrapp/data/models/playback/playback_state.dart'; abstract class IPlaybackStateRepository { Future saveState(PlaybackState state); Future loadState(); -} \ No newline at end of file +} diff --git a/lib/core/audio/storage/playback_state_repository.dart b/lib/core/audio/storage/playback_state_repository.dart index 1ac1604..aa10091 100644 --- a/lib/core/audio/storage/playback_state_repository.dart +++ b/lib/core/audio/storage/playback_state_repository.dart @@ -41,4 +41,4 @@ class PlaybackStateRepository implements IPlaybackStateRepository { return null; } } -} \ No newline at end of file +} diff --git a/lib/core/audio/utils/audio_error_handler.dart b/lib/core/audio/utils/audio_error_handler.dart index e832cdf..3ca6e33 100644 --- a/lib/core/audio/utils/audio_error_handler.dart +++ b/lib/core/audio/utils/audio_error_handler.dart @@ -1,11 +1,11 @@ import 'package:asmrapp/utils/logger.dart'; enum AudioErrorType { - playback, // 播放错误 - playlist, // 播放列表错误 - state, // 状态错误 - context, // 上下文错误 - init, // 初始化错误 + playback, // 播放错误 + playlist, // 播放列表错误 + state, // 状态错误 + context, // 上下文错误 + init, // 初始化错误 } class AudioError implements Exception { @@ -16,7 +16,8 @@ class AudioError implements Exception { AudioError(this.type, this.message, [this.originalError]); @override - String toString() => '$message${originalError != null ? ': $originalError' : ''}'; + String toString() => + '$message${originalError != null ? ': $originalError' : ''}'; } class AudioErrorHandler { @@ -29,7 +30,7 @@ class AudioErrorHandler { final message = _getErrorMessage(type, operation); AppLogger.error(message, error, stack); } - + static Never throwError( AudioErrorType type, String operation, @@ -53,4 +54,4 @@ class AudioErrorHandler { return '初始化失败: $operation'; } } -} \ No newline at end of file +} diff --git a/lib/core/audio/utils/playlist_builder.dart b/lib/core/audio/utils/playlist_builder.dart index be281d2..4b9c6cd 100644 --- a/lib/core/audio/utils/playlist_builder.dart +++ b/lib/core/audio/utils/playlist_builder.dart @@ -4,11 +4,9 @@ import 'package:asmrapp/core/audio/cache/audio_cache_manager.dart'; class PlaylistBuilder { static Future> buildAudioSources(List files) async { - return await Future.wait( - files.map((file) async { - return AudioCacheManager.createAudioSource(file.mediaDownloadUrl!); - }) - ); + return await Future.wait(files.map((file) async { + return AudioCacheManager.createAudioSource(file.mediaDownloadUrl!); + })); } static Future updatePlaylist( @@ -28,11 +26,11 @@ class PlaylistBuilder { }) async { final sources = await buildAudioSources(files); await updatePlaylist(playlist, sources); - + await player.setAudioSource( playlist, initialIndex: initialIndex, initialPosition: initialPosition, ); } -} \ No newline at end of file +} diff --git a/lib/core/audio/utils/track_info_creator.dart b/lib/core/audio/utils/track_info_creator.dart index 4bd728a..ce74b8b 100644 --- a/lib/core/audio/utils/track_info_creator.dart +++ b/lib/core/audio/utils/track_info_creator.dart @@ -16,7 +16,7 @@ class TrackInfoCreator { url: url, ); } - + static AudioTrackInfo createFromFile(Child file, Work work) { return createTrackInfo( title: file.title ?? '', @@ -25,4 +25,4 @@ class TrackInfoCreator { url: file.mediaDownloadUrl!, ); } -} \ No newline at end of file +} diff --git a/lib/core/cache/recommendation_cache_manager.dart b/lib/core/cache/recommendation_cache_manager.dart index 4237de6..0bc0313 100644 --- a/lib/core/cache/recommendation_cache_manager.dart +++ b/lib/core/cache/recommendation_cache_manager.dart @@ -1,16 +1,16 @@ -import 'dart:collection'; import 'package:asmrapp/data/services/api_service.dart'; import 'package:asmrapp/utils/logger.dart'; class RecommendationCacheManager { // 单例模式 - static final RecommendationCacheManager _instance = RecommendationCacheManager._internal(); + static final RecommendationCacheManager _instance = + RecommendationCacheManager._internal(); factory RecommendationCacheManager() => _instance; RecommendationCacheManager._internal(); // 使用 LinkedHashMap 便于按访问顺序管理缓存 - final _cache = LinkedHashMap(); - + final _cache = {}; + // 缓存配置 static const int _maxCacheSize = 1000; // 最大缓存条目数 static const Duration _cacheDuration = Duration(hours: 24); // 缓存有效期 @@ -43,7 +43,7 @@ class RecommendationCacheManager { /// 存储缓存数据 void set(String itemId, int page, int subtitle, WorksResponse data) { final key = _generateKey(itemId, page, subtitle); - + // 检查缓存大小,如果达到上限则移除最早的条目 if (_cache.length >= _maxCacheSize) { _cache.remove(_cache.keys.first); @@ -73,6 +73,7 @@ class _CacheItem { _CacheItem(this.data) : timestamp = DateTime.now(); - bool get isExpired => - DateTime.now().difference(timestamp) > RecommendationCacheManager._cacheDuration; -} \ No newline at end of file + bool get isExpired => + DateTime.now().difference(timestamp) > + RecommendationCacheManager._cacheDuration; +} diff --git a/lib/core/di/service_locator.dart b/lib/core/di/service_locator.dart index 741f6c1..4813b1f 100644 --- a/lib/core/di/service_locator.dart +++ b/lib/core/di/service_locator.dart @@ -99,14 +99,16 @@ Future setupServiceLocator() async { Future setupSubtitleServices() async { getIt.registerLazySingleton(() => SubtitleLoader()); if (Platform.isAndroid) { - getIt.registerLazySingleton(() => LyricOverlayController()); + getIt.registerLazySingleton( + () => LyricOverlayController()); } else { - getIt.registerLazySingleton(() => DummyLyricOverlayController()); + getIt.registerLazySingleton( + () => DummyLyricOverlayController()); } getIt.registerLazySingleton(() => LyricOverlayManager( - controller: getIt(), - subtitleService: getIt(), - )); + controller: getIt(), + subtitleService: getIt(), + )); // 初始化悬浮窗管理器 await getIt().initialize(); diff --git a/lib/core/platform/dummy_lyric_overlay_controller.dart b/lib/core/platform/dummy_lyric_overlay_controller.dart index c8cf002..2e0fd10 100644 --- a/lib/core/platform/dummy_lyric_overlay_controller.dart +++ b/lib/core/platform/dummy_lyric_overlay_controller.dart @@ -5,23 +5,16 @@ class DummyLyricOverlayController implements ILyricOverlayController { static const _tag = 'LyricOverlay'; @override - Future initialize() async { - } + Future initialize() async {} @override - Future show() async { - - } + Future show() async {} @override - Future hide() async { - - } + Future hide() async {} @override - Future updateLyric(String? text) async { - - } + Future updateLyric(String? text) async {} @override Future checkPermission() async { @@ -35,12 +28,10 @@ class DummyLyricOverlayController implements ILyricOverlayController { } @override - Future dispose() async { - - } + Future dispose() async {} @override Future isShowing() async { return false; } -} \ No newline at end of file +} diff --git a/lib/core/platform/i_lyric_overlay_controller.dart b/lib/core/platform/i_lyric_overlay_controller.dart index e79d860..b2b8c3d 100644 --- a/lib/core/platform/i_lyric_overlay_controller.dart +++ b/lib/core/platform/i_lyric_overlay_controller.dart @@ -1,25 +1,25 @@ abstract class ILyricOverlayController { /// 初始化悬浮窗 Future initialize(); - + /// 显示悬浮窗 Future show(); - + /// 隐藏悬浮窗 Future hide(); - + /// 更新歌词内容 Future updateLyric(String? text); - + /// 检查悬浮窗权限 Future checkPermission(); - + /// 请求悬浮窗权限 Future requestPermission(); - + /// 释放资源 Future dispose(); - + /// 获取悬浮窗当前显示状态 Future isShowing(); -} \ No newline at end of file +} diff --git a/lib/core/platform/lyric_overlay_controller.dart b/lib/core/platform/lyric_overlay_controller.dart index c8f2fb7..8ae456e 100644 --- a/lib/core/platform/lyric_overlay_controller.dart +++ b/lib/core/platform/lyric_overlay_controller.dart @@ -6,7 +6,7 @@ import 'i_lyric_overlay_controller.dart'; class LyricOverlayController implements ILyricOverlayController { static const _tag = 'LyricOverlay'; static const _channel = MethodChannel('one.asmr.yuro/lyric_overlay'); - + @override Future initialize() async { try { @@ -18,47 +18,47 @@ class LyricOverlayController implements ILyricOverlayController { // 因为这个错误不应该影响应用的主要功能 } } - + @override Future show() async { AppLogger.debug('[$_tag] 显示悬浮窗'); await _channel.invokeMethod('show'); } - + @override Future hide() async { AppLogger.debug('[$_tag] 隐藏悬浮窗'); await _channel.invokeMethod('hide'); } - + @override Future updateLyric(String? text) async { AppLogger.debug('[$_tag] 更新歌词: ${text ?? '<空>'}'); await _channel.invokeMethod('updateLyric', {'text': text}); } - + @override Future checkPermission() async { AppLogger.debug('[$_tag] 检查权限'); return await Permission.systemAlertWindow.isGranted; } - + @override Future requestPermission() async { AppLogger.debug('[$_tag] 请求权限'); final status = await Permission.systemAlertWindow.request(); return status.isGranted; } - + @override Future dispose() async { AppLogger.debug('[$_tag] 释放资源'); await _channel.invokeMethod('dispose'); } - + @override Future isShowing() async { final result = await _channel.invokeMethod('isShowing') ?? false; return result; } -} \ No newline at end of file +} diff --git a/lib/core/platform/lyric_overlay_manager.dart b/lib/core/platform/lyric_overlay_manager.dart index e91bdff..4e78498 100644 --- a/lib/core/platform/lyric_overlay_manager.dart +++ b/lib/core/platform/lyric_overlay_manager.dart @@ -9,12 +9,12 @@ class LyricOverlayManager { final ISubtitleService _subtitleService; StreamSubscription? _subscription; bool _isShowing = false; - + LyricOverlayManager({ required ILyricOverlayController controller, required ISubtitleService subtitleService, - }) : _controller = controller, - _subtitleService = subtitleService; + }) : _controller = controller, + _subtitleService = subtitleService; Future initialize() async { await _controller.initialize(); @@ -23,19 +23,19 @@ class LyricOverlayManager { _controller.updateLyric(subtitle?.text); } }); - + _isShowing = await _controller.isShowing(); - + if (_isShowing) { await show(); } } - + Future dispose() async { await _subscription?.cancel(); await _controller.dispose(); } - + Future checkPermission() async { return await _controller.checkPermission(); } @@ -81,22 +81,23 @@ class LyricOverlayManager { Future _showPermissionDialog(BuildContext context) async { return await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('开启悬浮歌词'), - content: const Text('需要悬浮窗权限来显示歌词,是否授予权限?'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('取消'), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: const Text('确定'), + context: context, + builder: (context) => AlertDialog( + title: const Text('开启悬浮歌词'), + content: const Text('需要悬浮窗权限来显示歌词,是否授予权限?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('取消'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('确定'), + ), + ], ), - ], - ), - ) ?? false; + ) ?? + false; } /// 切换显示/隐藏状态 @@ -107,10 +108,10 @@ class LyricOverlayManager { await showWithPermissionCheck(context); } } - + // 其他控制方法... Future syncState() async { _isShowing = await _controller.isShowing(); } -} \ No newline at end of file +} diff --git a/lib/core/platform/wakelock_controller.dart b/lib/core/platform/wakelock_controller.dart index 1f7fdeb..e263fef 100644 --- a/lib/core/platform/wakelock_controller.dart +++ b/lib/core/platform/wakelock_controller.dart @@ -1,8 +1,7 @@ +import 'package:asmrapp/utils/logger.dart'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; - import 'package:wakelock_plus/wakelock_plus.dart'; -import 'package:asmrapp/utils/logger.dart'; class WakeLockController extends ChangeNotifier { static const _tag = 'WakeLock'; @@ -46,6 +45,7 @@ class WakeLockController extends ChangeNotifier { } } + @override Future dispose() async { try { await WakelockPlus.disable(); @@ -54,4 +54,4 @@ class WakeLockController extends ChangeNotifier { } super.dispose(); } -} \ No newline at end of file +} diff --git a/lib/core/subtitle/cache/subtitle_cache_manager.dart b/lib/core/subtitle/cache/subtitle_cache_manager.dart index 1742c4d..951d569 100644 --- a/lib/core/subtitle/cache/subtitle_cache_manager.dart +++ b/lib/core/subtitle/cache/subtitle_cache_manager.dart @@ -6,7 +6,7 @@ import 'package:asmrapp/utils/logger.dart'; class SubtitleCacheManager { static const String key = 'subtitleCache'; - + static final CacheManager instance = CacheManager( Config( key, @@ -62,4 +62,4 @@ class SubtitleCacheManager { return 0; } } -} \ No newline at end of file +} diff --git a/lib/core/subtitle/i_subtitle_service.dart b/lib/core/subtitle/i_subtitle_service.dart index 96ee76f..a97b40e 100644 --- a/lib/core/subtitle/i_subtitle_service.dart +++ b/lib/core/subtitle/i_subtitle_service.dart @@ -3,28 +3,28 @@ import 'package:asmrapp/core/audio/models/subtitle.dart'; abstract class ISubtitleService { // 字幕加载 Future loadSubtitle(String url); - + // 字幕状态流 Stream get subtitleStream; - + // 当前字幕流 Stream get currentSubtitleStream; - + // 当前字幕 Subtitle? get currentSubtitle; - + // 更新播放位置 void updatePosition(Duration position); - + // 资源释放 void dispose(); - + // 添加这一行 - SubtitleList? get subtitleList; // 获取当前字幕列表 - + SubtitleList? get subtitleList; // 获取当前字幕列表 + // 添加清除字幕的方法 void clearSubtitle(); - + Stream get currentSubtitleWithStateStream; SubtitleWithState? get currentSubtitleWithState; -} \ No newline at end of file +} diff --git a/lib/core/subtitle/managers/subtitle_state_manager.dart b/lib/core/subtitle/managers/subtitle_state_manager.dart index be450ae..20cf2b7 100644 --- a/lib/core/subtitle/managers/subtitle_state_manager.dart +++ b/lib/core/subtitle/managers/subtitle_state_manager.dart @@ -9,11 +9,13 @@ class SubtitleStateManager { final _subtitleController = StreamController.broadcast(); final _currentSubtitleController = StreamController.broadcast(); - final _currentSubtitleWithStateController = StreamController.broadcast(); + final _currentSubtitleWithStateController = + StreamController.broadcast(); Stream get subtitleStream => _subtitleController.stream; - Stream get currentSubtitleStream => _currentSubtitleController.stream; - Stream get currentSubtitleWithStateStream => + Stream get currentSubtitleStream => + _currentSubtitleController.stream; + Stream get currentSubtitleWithStateStream => _currentSubtitleWithStateController.stream; Subtitle? get currentSubtitle => _currentSubtitle; @@ -28,10 +30,12 @@ class SubtitleStateManager { void updatePosition(Duration position) { if (_subtitleList != null) { final newSubtitleWithState = _subtitleList!.getCurrentSubtitle(position); - if (newSubtitleWithState?.subtitle != _currentSubtitleWithState?.subtitle) { + if (newSubtitleWithState?.subtitle != + _currentSubtitleWithState?.subtitle) { _currentSubtitleWithState = newSubtitleWithState; _currentSubtitle = newSubtitleWithState?.subtitle; - AppLogger.debug('字幕更新: ${_currentSubtitle?.text ?? '无字幕'} (${newSubtitleWithState?.state})'); + AppLogger.debug( + '字幕更新: ${_currentSubtitle?.text ?? '无字幕'} (${newSubtitleWithState?.state})'); _currentSubtitleWithStateController.add(newSubtitleWithState); _currentSubtitleController.add(_currentSubtitle); } @@ -53,4 +57,4 @@ class SubtitleStateManager { _currentSubtitleController.close(); _currentSubtitleWithStateController.close(); } -} \ No newline at end of file +} diff --git a/lib/core/subtitle/parsers/lrc_parser.dart b/lib/core/subtitle/parsers/lrc_parser.dart index fade29f..a4ebad5 100644 --- a/lib/core/subtitle/parsers/lrc_parser.dart +++ b/lib/core/subtitle/parsers/lrc_parser.dart @@ -5,38 +5,38 @@ import 'package:asmrapp/utils/logger.dart'; class LrcParser extends BaseSubtitleParser { static final _timeTagRegex = RegExp(r'\[(\d{2}):(\d{2})\.(\d{2})\]'); static final _idTagRegex = RegExp(r'^\[(ar|ti|al|by|offset):(.+)\]$'); - + @override bool canParse(String content) { final lines = content.trim().split('\n'); return lines.any((line) => _timeTagRegex.hasMatch(line)); } - + @override SubtitleList doParse(String content) { final lines = content.split('\n'); final subtitles = []; final metadata = {}; - + for (final line in lines) { final trimmedLine = line.trim(); if (trimmedLine.isEmpty) continue; - + // 检查是否是ID标签 final idMatch = _idTagRegex.firstMatch(trimmedLine); if (idMatch != null) { metadata[idMatch.group(1)!] = idMatch.group(2)!; continue; } - + // 解析时间标签和歌词 final timeMatches = _timeTagRegex.allMatches(trimmedLine); if (timeMatches.isEmpty) continue; - + // 获取歌词内容 (移除所有时间标签) final text = trimmedLine.replaceAll(_timeTagRegex, '').trim(); if (text.isEmpty) continue; - + // 一行可能有多个时间标签 for (final match in timeMatches) { try { @@ -45,7 +45,7 @@ class LrcParser extends BaseSubtitleParser { seconds: match.group(2)!, milliseconds: match.group(3)!, ); - + subtitles.add(Subtitle( start: timestamp, end: timestamp + const Duration(seconds: 5), // 默认持续5秒 @@ -58,10 +58,10 @@ class LrcParser extends BaseSubtitleParser { } } } - + // 按时间排序 subtitles.sort((a, b) => a.start.compareTo(b.start)); - + // 设置正确的结束时间 for (int i = 0; i < subtitles.length - 1; i++) { subtitles[i] = Subtitle( @@ -71,11 +71,11 @@ class LrcParser extends BaseSubtitleParser { index: i, ); } - + AppLogger.debug('LRC解析完成: ${subtitles.length}条字幕, ${metadata.length}个元数据'); return SubtitleList(subtitles); } - + Duration _parseTimestamp({ required String minutes, required String seconds, @@ -87,4 +87,4 @@ class LrcParser extends BaseSubtitleParser { milliseconds: int.parse(milliseconds) * 10, ); } -} \ No newline at end of file +} diff --git a/lib/core/subtitle/parsers/subtitle_parser.dart b/lib/core/subtitle/parsers/subtitle_parser.dart index fac2e85..2b9ea02 100644 --- a/lib/core/subtitle/parsers/subtitle_parser.dart +++ b/lib/core/subtitle/parsers/subtitle_parser.dart @@ -4,7 +4,7 @@ import 'package:asmrapp/core/audio/models/subtitle.dart'; abstract class SubtitleParser { /// 解析字幕内容 SubtitleList parse(String content); - + /// 检查内容格式是否匹配 bool canParse(String content); } @@ -14,11 +14,11 @@ abstract class BaseSubtitleParser implements SubtitleParser { @override SubtitleList parse(String content) { if (!canParse(content)) { - throw FormatException('不支持的字幕格式'); + throw const FormatException('不支持的字幕格式'); } return doParse(content); } - + /// 具体的解析实现 SubtitleList doParse(String content); -} \ No newline at end of file +} diff --git a/lib/core/subtitle/parsers/subtitle_parser_factory.dart b/lib/core/subtitle/parsers/subtitle_parser_factory.dart index 0041d5f..83c9939 100644 --- a/lib/core/subtitle/parsers/subtitle_parser_factory.dart +++ b/lib/core/subtitle/parsers/subtitle_parser_factory.dart @@ -8,7 +8,7 @@ class SubtitleParserFactory { VttParser(), LrcParser(), ]; - + static SubtitleParser? getParser(String content) { try { return _parsers.firstWhere((parser) => parser.canParse(content)); @@ -17,4 +17,4 @@ class SubtitleParserFactory { return null; } } -} \ No newline at end of file +} diff --git a/lib/core/subtitle/parsers/vtt_parser.dart b/lib/core/subtitle/parsers/vtt_parser.dart index 13a88a8..3204d75 100644 --- a/lib/core/subtitle/parsers/vtt_parser.dart +++ b/lib/core/subtitle/parsers/vtt_parser.dart @@ -3,23 +3,23 @@ import 'package:asmrapp/core/subtitle/parsers/subtitle_parser.dart'; class VttParser extends BaseSubtitleParser { static final _vttHeaderRegex = RegExp(r'^WEBVTT'); - + @override bool canParse(String content) { return content.trim().startsWith(_vttHeaderRegex); } - + @override SubtitleList doParse(String content) { final lines = content.split('\n'); final subtitles = []; int index = 0; - + // 跳过WEBVTT头部 while (index < lines.length && !lines[index].contains('-->')) { index++; } - + while (index < lines.length) { final timeLine = lines[index]; if (timeLine.contains('-->')) { @@ -27,15 +27,15 @@ class VttParser extends BaseSubtitleParser { if (times.length == 2) { final start = _parseTimeString(times[0].trim()); final end = _parseTimeString(times[1].trim()); - + // 收集字幕文本 index++; String text = ''; while (index < lines.length && lines[index].trim().isNotEmpty) { - text += lines[index].trim() + '\n'; + text += '${lines[index].trim()}\n'; index++; } - + if (text.isNotEmpty) { subtitles.add(Subtitle( start: start, @@ -48,20 +48,21 @@ class VttParser extends BaseSubtitleParser { } index++; } - + return SubtitleList(subtitles); } - + Duration _parseTimeString(String timeString) { final parts = timeString.split(':'); - if (parts.length != 3) throw FormatException('Invalid time format'); - + if (parts.length != 3) throw const FormatException('Invalid time format'); + final seconds = parts[2].split('.'); return Duration( hours: int.parse(parts[0]), minutes: int.parse(parts[1]), seconds: int.parse(seconds[0]), - milliseconds: seconds.length > 1 ? int.parse(seconds[1].padRight(3, '0')) : 0, + milliseconds: + seconds.length > 1 ? int.parse(seconds[1].padRight(3, '0')) : 0, ); } -} \ No newline at end of file +} diff --git a/lib/core/subtitle/subtitle_loader.dart b/lib/core/subtitle/subtitle_loader.dart index a8f2a21..0b8036a 100644 --- a/lib/core/subtitle/subtitle_loader.dart +++ b/lib/core/subtitle/subtitle_loader.dart @@ -14,27 +14,27 @@ class SubtitleLoader { // 查找字幕文件 Child? findSubtitleFile(Child audioFile, Files files) { if (files.children == null || audioFile.title == null) { - AppLogger.debug('无法查找字幕文件: ${files.children == null ? '文件列表为空' : '当前文件名为空'}'); + AppLogger.debug( + '无法查找字幕文件: ${files.children == null ? '文件列表为空' : '当前文件名为空'}'); return null; } AppLogger.debug('开始查找字幕文件...'); - + // 使用 FilePath 获取同级文件 final siblings = FilePath.getSiblings(audioFile, files); - + // 使用 SubtitleMatcher 查找匹配的字幕文件 - final subtitleFile = SubtitleMatcher.findMatchingSubtitle( - audioFile.title!, - siblings - ); - + final subtitleFile = + SubtitleMatcher.findMatchingSubtitle(audioFile.title!, siblings); + if (subtitleFile != null) { - AppLogger.debug('找到字幕文件: ${subtitleFile.title}, URL: ${subtitleFile.mediaDownloadUrl}'); + AppLogger.debug( + '找到字幕文件: ${subtitleFile.title}, URL: ${subtitleFile.mediaDownloadUrl}'); } else { AppLogger.debug('在当前目录中未找到字幕文件'); } - + return subtitleFile; } @@ -52,13 +52,13 @@ class SubtitleLoader { AppLogger.debug('从网络加载字幕: $url'); final response = await _dio.get(url); AppLogger.debug('字幕文件下载状态: ${response.statusCode}'); - + if (response.statusCode == 200) { final content = response.data as String; - + // 保存到缓存 await SubtitleCacheManager.cacheContent(url, content); - + return _parseSubtitleContent(content); } else { throw Exception('字幕下载失败: ${response.statusCode}'); @@ -71,16 +71,17 @@ class SubtitleLoader { // 新增: 解析字幕内容的私有方法 SubtitleList? _parseSubtitleContent(String content) { - AppLogger.debug('字幕文件内容预览: ${content.substring(0, content.length > 100 ? 100 : content.length)}...'); - + AppLogger.debug( + '字幕文件内容预览: ${content.substring(0, content.length > 100 ? 100 : content.length)}...'); + final parser = SubtitleParserFactory.getParser(content); if (parser == null) { throw Exception('不支持的字幕格式'); } - + final subtitleList = parser.parse(content); AppLogger.debug('字幕解析完成,字幕数量: ${subtitleList.subtitles.length}'); - + return subtitleList; } -} \ No newline at end of file +} diff --git a/lib/core/subtitle/subtitle_service.dart b/lib/core/subtitle/subtitle_service.dart index 87091b3..17460cd 100644 --- a/lib/core/subtitle/subtitle_service.dart +++ b/lib/core/subtitle/subtitle_service.dart @@ -6,20 +6,20 @@ import 'package:get_it/get_it.dart'; import 'package:asmrapp/core/subtitle/subtitle_loader.dart'; import 'package:asmrapp/core/subtitle/managers/subtitle_state_manager.dart'; - class SubtitleService implements ISubtitleService { final _subtitleLoader = GetIt.I(); final _stateManager = SubtitleStateManager(); - + @override Stream get subtitleStream => _stateManager.subtitleStream; - + @override - Stream get currentSubtitleStream => _stateManager.currentSubtitleStream; - + Stream get currentSubtitleStream => + _stateManager.currentSubtitleStream; + @override Subtitle? get currentSubtitle => _stateManager.currentSubtitle; - + @override Future loadSubtitle(String url) async { try { @@ -32,7 +32,7 @@ class SubtitleService implements ISubtitleService { rethrow; } } - + @override void updatePosition(Duration position) { _stateManager.updatePosition(position); @@ -52,10 +52,10 @@ class SubtitleService implements ISubtitleService { } @override - Stream get currentSubtitleWithStateStream => + Stream get currentSubtitleWithStateStream => _stateManager.currentSubtitleWithStateStream; - + @override - SubtitleWithState? get currentSubtitleWithState => + SubtitleWithState? get currentSubtitleWithState => _stateManager.currentSubtitleWithState; -} \ No newline at end of file +} diff --git a/lib/core/subtitle/utils/subtitle_matcher.dart b/lib/core/subtitle/utils/subtitle_matcher.dart index 23ad8f0..a75e568 100644 --- a/lib/core/subtitle/utils/subtitle_matcher.dart +++ b/lib/core/subtitle/utils/subtitle_matcher.dart @@ -3,55 +3,55 @@ import 'package:asmrapp/data/models/files/child.dart'; class SubtitleMatcher { // 支持的字幕格式 static const supportedFormats = ['.vtt', '.lrc']; - + // 检查文件是否为字幕文件 static bool isSubtitleFile(String? fileName) { if (fileName == null) return false; - return supportedFormats.any((format) => - fileName.toLowerCase().endsWith(format)); + return supportedFormats + .any((format) => fileName.toLowerCase().endsWith(format)); } - + // 获取音频文件的可能的字幕文件名列表 static List getPossibleSubtitleNames(String audioFileName) { final names = []; final baseName = _getBaseName(audioFileName); - + // 生成可能的字幕文件名 for (final format in supportedFormats) { // 1. 直接替换扩展名: aaa.mp3 -> aaa.vtt names.add('$baseName$format'); - + // 2. 保留原扩展名: aaa.mp3 -> aaa.mp3.vtt names.add('$audioFileName$format'); } - + return names; } - + // 查找匹配的字幕文件 - static Child? findMatchingSubtitle(String audioFileName, List siblings) { + static Child? findMatchingSubtitle( + String audioFileName, List siblings) { final possibleNames = getPossibleSubtitleNames(audioFileName); - + // 遍历所有可能的字幕文件名 for (final subtitleName in possibleNames) { try { final subtitleFile = siblings.firstWhere( - (file) => file.title?.toLowerCase() == subtitleName.toLowerCase() - ); + (file) => file.title?.toLowerCase() == subtitleName.toLowerCase()); return subtitleFile; } catch (_) { // 继续查找下一个可能的文件名 continue; } } - + return null; } - + // 获取不带扩展名的文件名 static String _getBaseName(String fileName) { final lastDot = fileName.lastIndexOf('.'); if (lastDot == -1) return fileName; return fileName.substring(0, lastDot); } -} \ No newline at end of file +} diff --git a/lib/core/theme/app_colors.dart b/lib/core/theme/app_colors.dart index 448a2d2..0e849d9 100644 --- a/lib/core/theme/app_colors.dart +++ b/lib/core/theme/app_colors.dart @@ -10,17 +10,12 @@ class AppColors { // 基础色调 primary: Color(0xFF6750A4), onPrimary: Colors.white, - + // 表面颜色 surface: Colors.white, - surfaceVariant: Color(0xFFF4F4F4), onSurface: Colors.black87, surfaceContainerHighest: Color(0xFFE6E6E6), - - // 背景颜色 - background: Colors.white, - onBackground: Colors.black87, - + // 错误状态颜色 error: Color(0xFFB3261E), errorContainer: Color(0xFFF9DEDC), @@ -32,20 +27,15 @@ class AppColors { // 基础色调 primary: Color(0xFFD0BCFF), onPrimary: Color(0xFF381E72), - + // 表面颜色 surface: Color(0xFF1C1B1F), - surfaceVariant: Color(0xFF2B2930), onSurface: Colors.white, surfaceContainerHighest: Color(0xFF2B2B2B), - - // 背景颜色 - background: Color(0xFF1C1B1F), - onBackground: Colors.white, - + // 错误状态颜色 error: Color(0xFFF2B8B5), errorContainer: Color(0xFF8C1D18), onError: Color(0xFF601410), ); -} \ No newline at end of file +} diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index 023ee93..66b4def 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; + import 'app_colors.dart'; /// 应用主题配置 @@ -8,45 +9,45 @@ class AppTheme { // 亮色主题 static ThemeData get light => ThemeData( - useMaterial3: true, - brightness: Brightness.light, - colorScheme: AppColors.lightColorScheme, - - // Card主题 - cardTheme: const CardTheme( - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - ), - ), - - // AppBar主题 - appBarTheme: const AppBarTheme( - centerTitle: true, - elevation: 0, - scrolledUnderElevation: 0, - ), - ); + useMaterial3: true, + brightness: Brightness.light, + colorScheme: AppColors.lightColorScheme, + + // Card主题 + cardTheme: const CardThemeData( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + ), + + // AppBar主题 + appBarTheme: const AppBarTheme( + centerTitle: true, + elevation: 0, + scrolledUnderElevation: 0, + ), + ); // 暗色主题 static ThemeData get dark => ThemeData( - useMaterial3: true, - brightness: Brightness.dark, - colorScheme: AppColors.darkColorScheme, - - // Card主题 - cardTheme: const CardTheme( - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - ), - ), - - // AppBar主题 - appBarTheme: const AppBarTheme( - centerTitle: true, - elevation: 0, - scrolledUnderElevation: 0, - ), - ); -} \ No newline at end of file + useMaterial3: true, + brightness: Brightness.dark, + colorScheme: AppColors.darkColorScheme, + + // Card主题 + cardTheme: const CardThemeData( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + ), + + // AppBar主题 + appBarTheme: const AppBarTheme( + centerTitle: true, + elevation: 0, + scrolledUnderElevation: 0, + ), + ); +} diff --git a/lib/core/theme/theme_controller.dart b/lib/core/theme/theme_controller.dart index a83877f..e6bfcac 100644 --- a/lib/core/theme/theme_controller.dart +++ b/lib/core/theme/theme_controller.dart @@ -17,25 +17,25 @@ class ThemeController extends ChangeNotifier { } ThemeMode _themeMode = ThemeMode.system; - + ThemeMode get themeMode => _themeMode; // 切换主题模式 Future setThemeMode(ThemeMode mode) async { if (_themeMode == mode) return; - + _themeMode = mode; notifyListeners(); - + // 保存到持久化存储 await _prefs.setString(_themeKey, mode.toString()); } // 切换到下一个主题模式 Future toggleThemeMode() async { - final modes = ThemeMode.values; + const modes = ThemeMode.values; final currentIndex = modes.indexOf(_themeMode); final nextIndex = (currentIndex + 1) % modes.length; await setThemeMode(modes[nextIndex]); } -} \ No newline at end of file +} diff --git a/lib/data/models/mark_status.dart b/lib/data/models/mark_status.dart index 0ef7ea0..c18cc5c 100644 --- a/lib/data/models/mark_status.dart +++ b/lib/data/models/mark_status.dart @@ -7,4 +7,4 @@ enum MarkStatus { final String label; const MarkStatus(this.label); -} \ No newline at end of file +} diff --git a/lib/data/models/playback/playback_state.dart b/lib/data/models/playback/playback_state.dart index 2c399c2..1f15c73 100644 --- a/lib/data/models/playback/playback_state.dart +++ b/lib/data/models/playback/playback_state.dart @@ -16,10 +16,10 @@ class PlaybackState with _$PlaybackState { required List playlist, required int currentIndex, required PlayMode playMode, - required int position, // 使用毫秒存储 - required String timestamp, // ISO8601 格式 + required int position, // 使用毫秒存储 + required String timestamp, // ISO8601 格式 }) = _PlaybackState; - factory PlaybackState.fromJson(Map json) => + factory PlaybackState.fromJson(Map json) => _$PlaybackStateFromJson(json); -} \ No newline at end of file +} diff --git a/lib/data/repositories/auth_repository.dart b/lib/data/repositories/auth_repository.dart index 3267ff0..e3d4d57 100644 --- a/lib/data/repositories/auth_repository.dart +++ b/lib/data/repositories/auth_repository.dart @@ -43,4 +43,4 @@ class AuthRepository { rethrow; } } -} \ No newline at end of file +} diff --git a/lib/data/services/api_service.dart b/lib/data/services/api_service.dart index 92639aa..5aedce7 100644 --- a/lib/data/services/api_service.dart +++ b/lib/data/services/api_service.dart @@ -10,7 +10,6 @@ import 'package:asmrapp/data/services/interceptors/auth_interceptor.dart'; import 'package:asmrapp/data/models/playlists_with_exist_statu/playlist.dart'; import 'package:asmrapp/data/models/my_lists/my_playlists/my_playlists.dart'; - class WorksResponse { final List works; final Pagination pagination; @@ -33,11 +32,11 @@ class ApiService { Future getWorkFiles(String workId, {CancelToken? cancelToken}) async { try { final response = await _dio.get( - '/tracks/$workId', + '/tracks/$workId', queryParameters: { 'v': '1', }, - cancelToken: cancelToken, // 添加 cancelToken 支持 + cancelToken: cancelToken, // 添加 cancelToken 支持 ); if (response.statusCode == 200) { @@ -260,7 +259,8 @@ class ApiService { }) async { try { // 先尝试从缓存获取 - final cachedData = _recommendationCache.get(itemId, page, hasSubtitle ? 1 : 0); + final cachedData = + _recommendationCache.get(itemId, page, hasSubtitle ? 1 : 0); if (cachedData != null) { return cachedData; } @@ -288,7 +288,8 @@ class ApiService { ); // 存入缓存 - _recommendationCache.set(itemId, page, hasSubtitle ? 1 : 0, worksResponse); + _recommendationCache.set( + itemId, page, hasSubtitle ? 1 : 0, worksResponse); return worksResponse; } @@ -415,11 +416,13 @@ class ApiService { /// 获取默认标记目标收藏夹 Future getDefaultMarkTargetPlaylist() async { try { - final response = await _dio.get('/playlist/get-default-mark-target-playlist'); + final response = + await _dio.get('/playlist/get-default-mark-target-playlist'); if (response.statusCode == 200) { final playlist = Playlist.fromJson(response.data); - AppLogger.info('获取默认标记目标收藏夹成功: id=${playlist.id}, name=${playlist.name}'); + AppLogger.info( + '获取默认标记目标收藏夹成功: id=${playlist.id}, name=${playlist.name}'); return playlist; } diff --git a/lib/data/services/auth_service.dart b/lib/data/services/auth_service.dart index a1f784c..516c6b9 100644 --- a/lib/data/services/auth_service.dart +++ b/lib/data/services/auth_service.dart @@ -5,15 +5,16 @@ import '../../utils/logger.dart'; class AuthService { final Dio _dio; - AuthService() - : _dio = Dio(BaseOptions( - baseUrl: 'https://api.asmr.one/api', - )); + AuthService() + : _dio = Dio(BaseOptions( + baseUrl: 'https://api.asmr.one/api', + )); Future login(String name, String password) async { try { AppLogger.info('开始登录请求: name=$name'); - final response = await _dio.post('/auth/me', + final response = await _dio.post( + '/auth/me', data: { 'name': name, 'password': password, @@ -25,7 +26,8 @@ class AuthService { if (response.statusCode == 200) { final authResp = AuthResp.fromJson(response.data); - AppLogger.info('登录成功: username=${authResp.user?.name}, group=${authResp.user?.group}'); + AppLogger.info( + '登录成功: username=${authResp.user?.name}, group=${authResp.user?.group}'); return authResp; } @@ -39,4 +41,4 @@ class AuthService { throw Exception('登录失败: $e'); } } -} \ No newline at end of file +} diff --git a/lib/data/services/interceptors/auth_interceptor.dart b/lib/data/services/interceptors/auth_interceptor.dart index fc7cc93..4caf46d 100644 --- a/lib/data/services/interceptors/auth_interceptor.dart +++ b/lib/data/services/interceptors/auth_interceptor.dart @@ -6,21 +6,21 @@ import 'package:asmrapp/utils/logger.dart'; class AuthInterceptor extends Interceptor { @override Future onRequest( - RequestOptions options, + RequestOptions options, RequestInterceptorHandler handler, ) async { try { final authRepository = GetIt.I(); final authData = await authRepository.getAuthData(); - + if (authData?.token != null) { options.headers['Authorization'] = 'Bearer ${authData!.token}'; } - + handler.next(options); } catch (e) { AppLogger.error('AuthInterceptor: 处理请求失败', e); - handler.next(options); // 即使出错也继续请求 + handler.next(options); // 即使出错也继续请求 } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb new file mode 100644 index 0000000..3d16eb3 --- /dev/null +++ b/lib/l10n/app_ja.arb @@ -0,0 +1,186 @@ +{ + "@@locale": "ja", + "appName": "asmr.one", + "retry": "再試行", + "cancel": "キャンセル", + "confirm": "確認", + "login": "ログイン", + "favorites": "お気に入り", + "settings": "設定", + "cacheManager": "キャッシュ管理", + "screenAlwaysOn": "画面常時オン", + "themeSystem": "システムと同じ", + "themeLight": "ライトモード", + "themeDark": "ダークモード", + "navigationFavorites": "お気に入り", + "navigationHome": "ホーム", + "navigationForYou": "あなた向け", + "navigationPopularWorks": "人気作品", + "navigationRecommend": "おすすめ", + "titleWithCount": "{title} ({count})", + "@titleWithCount": { + "placeholders": { + "title": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "search": "検索", + "searchHint": "検索...", + "searchPromptInitial": "キーワードを入力して検索", + "searchNoResults": "該当する結果がありません", + "subtitle": "字幕", + "subtitleAvailable": "字幕あり", + "orderFieldCollectionTime": "収録日時", + "orderFieldReleaseDate": "発売日", + "orderFieldSales": "売上", + "orderFieldPrice": "価格", + "orderFieldRating": "評価", + "orderFieldReviewCount": "レビュー数", + "orderFieldId": "RJ番号", + "orderFieldMyRating": "自分の評価", + "orderFieldAllAges": "全年齢", + "orderFieldRandom": "ランダム", + "orderLabel": "並び替え", + "orderDirectionDesc": "降順", + "orderDirectionAsc": "昇順", + "searchOrderNewest": "最新収録", + "searchOrderOldest": "最古の収録", + "searchOrderReleaseDesc": "発売日(新しい順)", + "searchOrderReleaseAsc": "発売日(古い順)", + "searchOrderSalesDesc": "売上(高い順)", + "searchOrderSalesAsc": "売上(低い順)", + "searchOrderPriceDesc": "価格(高い順)", + "searchOrderPriceAsc": "価格(低い順)", + "searchOrderRatingDesc": "評価(高い順)", + "searchOrderReviewCountDesc": "レビュー数(多い順)", + "searchOrderIdDesc": "RJ番号(大きい順)", + "searchOrderIdAsc": "RJ番号(小さい順)", + "searchOrderRandom": "ランダム順", + "favoritesTitle": "お気に入り", + "pleaseLogin": "先にログインしてください", + "emptyContent": "内容がありません", + "emptyWorks": "作品がありません", + "similarWorksTitle": "関連作品", + "playlistAddToFavorites": "お気に入りに追加", + "playlistEmpty": "リストがありません", + "playlistAddSuccess": "追加しました: {name}", + "@playlistAddSuccess": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "playlistRemoveSuccess": "削除しました: {name}", + "@playlistRemoveSuccess": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "playlistSystemMarked": "自分のマーク", + "playlistSystemLiked": "いいねした", + "playlistWorksCount": "{count} 件の作品", + "@playlistWorksCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "workActionFavorite": "お気に入り", + "workActionMark": "マーク", + "workActionRate": "評価", + "workActionChecking": "確認中", + "workActionRecommend": "おすすめ", + "workActionNoRecommendation": "おすすめはありません", + "markStatusTitle": "マーク状態", + "markStatusWantToListen": "聴きたい", + "markStatusListening": "聴いている", + "markStatusListened": "聴いた", + "markStatusRelistening": "聴き直し", + "markStatusOnHold": "保留", + "markUpdated": "状態を{status}に変更", + "@markUpdated": { + "placeholders": { + "status": { + "type": "String" + } + } + }, + "markFailed": "マーク失敗: {error}", + "@markFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "workFilesTitle": "ファイル一覧", + "playUnsupportedFileType": "未対応形式: {type}", + "@playUnsupportedFileType": { + "placeholders": { + "type": { + "type": "String" + } + } + }, + "playUrlMissing": "再生できません: URLがありません", + "playFilesNotLoaded": "ファイル一覧未読み込み", + "playFailed": "再生失敗: {error}", + "@playFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "operationFailed": "操作失敗: {error}", + "@operationFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "cacheManagerTitle": "キャッシュ管理", + "cacheAudio": "音声キャッシュ", + "cacheSubtitle": "字幕キャッシュ", + "cacheTotal": "キャッシュ総量", + "cacheClear": "削除", + "cacheClearAll": "全て削除", + "cacheInfoTitle": "キャッシュについて", + "cacheDescription": "キャッシュは直近の音声と字幕を保存し、次回再生を速くします。期限切れや容量超過は自動で整理されます。", + "cacheLoadFailed": "読み込み失敗: {error}", + "@cacheLoadFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "cacheClearFailed": "削除失敗: {error}", + "@cacheClearFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "subtitleTag": "字幕", + "noPlaying": "再生中なし", + "screenOnDisable": "画面常時オンをオフ", + "screenOnEnable": "画面常時オンをオン", + "unknownWorkTitle": "不明な作品", + "unknownArtist": "不明な出演者", + "lyricsEmpty": "歌詞なし", + "loginTitle": "ログイン", + "loginUsernameLabel": "ユーザー名", + "loginPasswordLabel": "パスワード", + "loginAction": "ログイン" +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart new file mode 100644 index 0000000..c18fe6b --- /dev/null +++ b/lib/l10n/app_localizations.dart @@ -0,0 +1,751 @@ +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_ja.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('ja'), + Locale('zh') + ]; + + /// No description provided for @appName. + /// + /// In zh, this message translates to: + /// **'asmr.one'** + String get appName; + + /// No description provided for @retry. + /// + /// In zh, this message translates to: + /// **'重试'** + String get retry; + + /// No description provided for @cancel. + /// + /// In zh, this message translates to: + /// **'取消'** + String get cancel; + + /// No description provided for @confirm. + /// + /// In zh, this message translates to: + /// **'确认'** + String get confirm; + + /// No description provided for @login. + /// + /// In zh, this message translates to: + /// **'登录'** + String get login; + + /// No description provided for @favorites. + /// + /// In zh, this message translates to: + /// **'我的收藏'** + String get favorites; + + /// No description provided for @settings. + /// + /// In zh, this message translates to: + /// **'设置'** + String get settings; + + /// No description provided for @cacheManager. + /// + /// In zh, this message translates to: + /// **'缓存管理'** + String get cacheManager; + + /// No description provided for @screenAlwaysOn. + /// + /// In zh, this message translates to: + /// **'屏幕常亮'** + String get screenAlwaysOn; + + /// No description provided for @themeSystem. + /// + /// In zh, this message translates to: + /// **'跟随系统主题'** + String get themeSystem; + + /// No description provided for @themeLight. + /// + /// In zh, this message translates to: + /// **'浅色模式'** + String get themeLight; + + /// No description provided for @themeDark. + /// + /// In zh, this message translates to: + /// **'深色模式'** + String get themeDark; + + /// No description provided for @navigationFavorites. + /// + /// In zh, this message translates to: + /// **'收藏'** + String get navigationFavorites; + + /// No description provided for @navigationHome. + /// + /// In zh, this message translates to: + /// **'主页'** + String get navigationHome; + + /// No description provided for @navigationForYou. + /// + /// In zh, this message translates to: + /// **'为你推荐'** + String get navigationForYou; + + /// No description provided for @navigationPopularWorks. + /// + /// In zh, this message translates to: + /// **'热门作品'** + String get navigationPopularWorks; + + /// No description provided for @navigationRecommend. + /// + /// In zh, this message translates to: + /// **'推荐'** + String get navigationRecommend; + + /// No description provided for @titleWithCount. + /// + /// In zh, this message translates to: + /// **'{title} ({count})'** + String titleWithCount(String title, int count); + + /// No description provided for @search. + /// + /// In zh, this message translates to: + /// **'搜索'** + String get search; + + /// No description provided for @searchHint. + /// + /// In zh, this message translates to: + /// **'搜索...'** + String get searchHint; + + /// No description provided for @searchPromptInitial. + /// + /// In zh, this message translates to: + /// **'输入关键词开始搜索'** + String get searchPromptInitial; + + /// No description provided for @searchNoResults. + /// + /// In zh, this message translates to: + /// **'没有找到相关结果'** + String get searchNoResults; + + /// No description provided for @subtitle. + /// + /// In zh, this message translates to: + /// **'字幕'** + String get subtitle; + + /// No description provided for @subtitleAvailable. + /// + /// In zh, this message translates to: + /// **'有字幕'** + String get subtitleAvailable; + + /// No description provided for @orderFieldCollectionTime. + /// + /// In zh, this message translates to: + /// **'收录时间'** + String get orderFieldCollectionTime; + + /// No description provided for @orderFieldReleaseDate. + /// + /// In zh, this message translates to: + /// **'发售日期'** + String get orderFieldReleaseDate; + + /// No description provided for @orderFieldSales. + /// + /// In zh, this message translates to: + /// **'销量'** + String get orderFieldSales; + + /// No description provided for @orderFieldPrice. + /// + /// In zh, this message translates to: + /// **'价格'** + String get orderFieldPrice; + + /// No description provided for @orderFieldRating. + /// + /// In zh, this message translates to: + /// **'评价'** + String get orderFieldRating; + + /// No description provided for @orderFieldReviewCount. + /// + /// In zh, this message translates to: + /// **'评论数量'** + String get orderFieldReviewCount; + + /// No description provided for @orderFieldId. + /// + /// In zh, this message translates to: + /// **'RJ号'** + String get orderFieldId; + + /// No description provided for @orderFieldMyRating. + /// + /// In zh, this message translates to: + /// **'我的评价'** + String get orderFieldMyRating; + + /// No description provided for @orderFieldAllAges. + /// + /// In zh, this message translates to: + /// **'全年龄'** + String get orderFieldAllAges; + + /// No description provided for @orderFieldRandom. + /// + /// In zh, this message translates to: + /// **'随机'** + String get orderFieldRandom; + + /// No description provided for @orderLabel. + /// + /// In zh, this message translates to: + /// **'排序'** + String get orderLabel; + + /// No description provided for @orderDirectionDesc. + /// + /// In zh, this message translates to: + /// **'降序'** + String get orderDirectionDesc; + + /// No description provided for @orderDirectionAsc. + /// + /// In zh, this message translates to: + /// **'升序'** + String get orderDirectionAsc; + + /// No description provided for @searchOrderNewest. + /// + /// In zh, this message translates to: + /// **'最新收录'** + String get searchOrderNewest; + + /// No description provided for @searchOrderOldest. + /// + /// In zh, this message translates to: + /// **'最早收录'** + String get searchOrderOldest; + + /// No description provided for @searchOrderReleaseDesc. + /// + /// In zh, this message translates to: + /// **'发售日期倒序'** + String get searchOrderReleaseDesc; + + /// No description provided for @searchOrderReleaseAsc. + /// + /// In zh, this message translates to: + /// **'发售日期顺序'** + String get searchOrderReleaseAsc; + + /// No description provided for @searchOrderSalesDesc. + /// + /// In zh, this message translates to: + /// **'销量倒序'** + String get searchOrderSalesDesc; + + /// No description provided for @searchOrderSalesAsc. + /// + /// In zh, this message translates to: + /// **'销量顺序'** + String get searchOrderSalesAsc; + + /// No description provided for @searchOrderPriceDesc. + /// + /// In zh, this message translates to: + /// **'价格倒序'** + String get searchOrderPriceDesc; + + /// No description provided for @searchOrderPriceAsc. + /// + /// In zh, this message translates to: + /// **'价格顺序'** + String get searchOrderPriceAsc; + + /// No description provided for @searchOrderRatingDesc. + /// + /// In zh, this message translates to: + /// **'评价倒序'** + String get searchOrderRatingDesc; + + /// No description provided for @searchOrderReviewCountDesc. + /// + /// In zh, this message translates to: + /// **'评论数量倒序'** + String get searchOrderReviewCountDesc; + + /// No description provided for @searchOrderIdDesc. + /// + /// In zh, this message translates to: + /// **'RJ号倒序'** + String get searchOrderIdDesc; + + /// No description provided for @searchOrderIdAsc. + /// + /// In zh, this message translates to: + /// **'RJ号顺序'** + String get searchOrderIdAsc; + + /// No description provided for @searchOrderRandom. + /// + /// In zh, this message translates to: + /// **'随机排序'** + String get searchOrderRandom; + + /// No description provided for @favoritesTitle. + /// + /// In zh, this message translates to: + /// **'我的收藏'** + String get favoritesTitle; + + /// No description provided for @pleaseLogin. + /// + /// In zh, this message translates to: + /// **'请先登录'** + String get pleaseLogin; + + /// No description provided for @emptyContent. + /// + /// In zh, this message translates to: + /// **'暂无内容'** + String get emptyContent; + + /// No description provided for @emptyWorks. + /// + /// In zh, this message translates to: + /// **'暂无作品'** + String get emptyWorks; + + /// No description provided for @similarWorksTitle. + /// + /// In zh, this message translates to: + /// **'相关推荐'** + String get similarWorksTitle; + + /// No description provided for @playlistAddToFavorites. + /// + /// In zh, this message translates to: + /// **'添加到收藏夹'** + String get playlistAddToFavorites; + + /// No description provided for @playlistEmpty. + /// + /// In zh, this message translates to: + /// **'暂无收藏夹'** + String get playlistEmpty; + + /// No description provided for @playlistAddSuccess. + /// + /// In zh, this message translates to: + /// **'添加成功: {name}'** + String playlistAddSuccess(String name); + + /// No description provided for @playlistRemoveSuccess. + /// + /// In zh, this message translates to: + /// **'移除成功: {name}'** + String playlistRemoveSuccess(String name); + + /// No description provided for @playlistSystemMarked. + /// + /// In zh, this message translates to: + /// **'我标记的'** + String get playlistSystemMarked; + + /// No description provided for @playlistSystemLiked. + /// + /// In zh, this message translates to: + /// **'我喜欢的'** + String get playlistSystemLiked; + + /// No description provided for @playlistWorksCount. + /// + /// In zh, this message translates to: + /// **'{count} 个作品'** + String playlistWorksCount(int count); + + /// No description provided for @workActionFavorite. + /// + /// In zh, this message translates to: + /// **'收藏'** + String get workActionFavorite; + + /// No description provided for @workActionMark. + /// + /// In zh, this message translates to: + /// **'标记'** + String get workActionMark; + + /// No description provided for @workActionRate. + /// + /// In zh, this message translates to: + /// **'评分'** + String get workActionRate; + + /// No description provided for @workActionChecking. + /// + /// In zh, this message translates to: + /// **'检查中'** + String get workActionChecking; + + /// No description provided for @workActionRecommend. + /// + /// In zh, this message translates to: + /// **'相关推荐'** + String get workActionRecommend; + + /// No description provided for @workActionNoRecommendation. + /// + /// In zh, this message translates to: + /// **'暂无推荐'** + String get workActionNoRecommendation; + + /// No description provided for @markStatusTitle. + /// + /// In zh, this message translates to: + /// **'标记状态'** + String get markStatusTitle; + + /// No description provided for @markStatusWantToListen. + /// + /// In zh, this message translates to: + /// **'想听'** + String get markStatusWantToListen; + + /// No description provided for @markStatusListening. + /// + /// In zh, this message translates to: + /// **'在听'** + String get markStatusListening; + + /// No description provided for @markStatusListened. + /// + /// In zh, this message translates to: + /// **'听过'** + String get markStatusListened; + + /// No description provided for @markStatusRelistening. + /// + /// In zh, this message translates to: + /// **'重听'** + String get markStatusRelistening; + + /// No description provided for @markStatusOnHold. + /// + /// In zh, this message translates to: + /// **'搁置'** + String get markStatusOnHold; + + /// No description provided for @markUpdated. + /// + /// In zh, this message translates to: + /// **'已标记为{status}'** + String markUpdated(String status); + + /// No description provided for @markFailed. + /// + /// In zh, this message translates to: + /// **'标记失败: {error}'** + String markFailed(String error); + + /// No description provided for @workFilesTitle. + /// + /// In zh, this message translates to: + /// **'文件列表'** + String get workFilesTitle; + + /// No description provided for @playUnsupportedFileType. + /// + /// In zh, this message translates to: + /// **'不支持的文件类型: {type}'** + String playUnsupportedFileType(String type); + + /// No description provided for @playUrlMissing. + /// + /// In zh, this message translates to: + /// **'无法播放:文件URL不存在'** + String get playUrlMissing; + + /// No description provided for @playFilesNotLoaded. + /// + /// In zh, this message translates to: + /// **'文件列表未加载'** + String get playFilesNotLoaded; + + /// No description provided for @playFailed. + /// + /// In zh, this message translates to: + /// **'播放失败: {error}'** + String playFailed(String error); + + /// No description provided for @operationFailed. + /// + /// In zh, this message translates to: + /// **'操作失败: {error}'** + String operationFailed(String error); + + /// No description provided for @cacheManagerTitle. + /// + /// In zh, this message translates to: + /// **'缓存管理'** + String get cacheManagerTitle; + + /// No description provided for @cacheAudio. + /// + /// In zh, this message translates to: + /// **'音频缓存'** + String get cacheAudio; + + /// No description provided for @cacheSubtitle. + /// + /// In zh, this message translates to: + /// **'字幕缓存'** + String get cacheSubtitle; + + /// No description provided for @cacheTotal. + /// + /// In zh, this message translates to: + /// **'总缓存大小'** + String get cacheTotal; + + /// No description provided for @cacheClear. + /// + /// In zh, this message translates to: + /// **'清理'** + String get cacheClear; + + /// No description provided for @cacheClearAll. + /// + /// In zh, this message translates to: + /// **'清理全部'** + String get cacheClearAll; + + /// No description provided for @cacheInfoTitle. + /// + /// In zh, this message translates to: + /// **'缓存说明'** + String get cacheInfoTitle; + + /// No description provided for @cacheDescription. + /// + /// In zh, this message translates to: + /// **'缓存用于存储最近播放的音频文件和字幕文件,以提高再次播放时的加载速度。系统会自动清理过期和超量的缓存。'** + String get cacheDescription; + + /// No description provided for @cacheLoadFailed. + /// + /// In zh, this message translates to: + /// **'加载失败: {error}'** + String cacheLoadFailed(String error); + + /// No description provided for @cacheClearFailed. + /// + /// In zh, this message translates to: + /// **'清理失败: {error}'** + String cacheClearFailed(String error); + + /// No description provided for @subtitleTag. + /// + /// In zh, this message translates to: + /// **'字幕'** + String get subtitleTag; + + /// No description provided for @noPlaying. + /// + /// In zh, this message translates to: + /// **'未在播放'** + String get noPlaying; + + /// No description provided for @screenOnDisable. + /// + /// In zh, this message translates to: + /// **'关闭屏幕常亮'** + String get screenOnDisable; + + /// No description provided for @screenOnEnable. + /// + /// In zh, this message translates to: + /// **'开启屏幕常亮'** + String get screenOnEnable; + + /// No description provided for @unknownWorkTitle. + /// + /// In zh, this message translates to: + /// **'未知作品'** + String get unknownWorkTitle; + + /// No description provided for @unknownArtist. + /// + /// In zh, this message translates to: + /// **'未知演员'** + String get unknownArtist; + + /// No description provided for @lyricsEmpty. + /// + /// In zh, this message translates to: + /// **'无歌词'** + String get lyricsEmpty; + + /// No description provided for @loginTitle. + /// + /// In zh, this message translates to: + /// **'登录'** + String get loginTitle; + + /// No description provided for @loginUsernameLabel. + /// + /// In zh, this message translates to: + /// **'用户名'** + String get loginUsernameLabel; + + /// No description provided for @loginPasswordLabel. + /// + /// In zh, this message translates to: + /// **'密码'** + String get loginPasswordLabel; + + /// No description provided for @loginAction. + /// + /// In zh, this message translates to: + /// **'登录'** + String get loginAction; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['ja', '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 'ja': + return AppLocalizationsJa(); + 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_ja.dart b/lib/l10n/app_localizations_ja.dart new file mode 100644 index 0000000..21e3fbe --- /dev/null +++ b/lib/l10n/app_localizations_ja.dart @@ -0,0 +1,342 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Japanese (`ja`). +class AppLocalizationsJa extends AppLocalizations { + AppLocalizationsJa([String locale = 'ja']) : super(locale); + + @override + String get appName => 'asmr.one'; + + @override + String get retry => '再試行'; + + @override + String get cancel => 'キャンセル'; + + @override + String get confirm => '確認'; + + @override + String get login => 'ログイン'; + + @override + String get favorites => 'お気に入り'; + + @override + String get settings => '設定'; + + @override + String get cacheManager => 'キャッシュ管理'; + + @override + String get screenAlwaysOn => '画面常時オン'; + + @override + String get themeSystem => 'システムと同じ'; + + @override + String get themeLight => 'ライトモード'; + + @override + String get themeDark => 'ダークモード'; + + @override + String get navigationFavorites => 'お気に入り'; + + @override + String get navigationHome => 'ホーム'; + + @override + String get navigationForYou => 'あなた向け'; + + @override + String get navigationPopularWorks => '人気作品'; + + @override + String get navigationRecommend => 'おすすめ'; + + @override + String titleWithCount(String title, int count) { + return '$title ($count)'; + } + + @override + String get search => '検索'; + + @override + String get searchHint => '検索...'; + + @override + String get searchPromptInitial => 'キーワードを入力して検索'; + + @override + String get searchNoResults => '該当する結果がありません'; + + @override + String get subtitle => '字幕'; + + @override + String get subtitleAvailable => '字幕あり'; + + @override + String get orderFieldCollectionTime => '収録日時'; + + @override + String get orderFieldReleaseDate => '発売日'; + + @override + String get orderFieldSales => '売上'; + + @override + String get orderFieldPrice => '価格'; + + @override + String get orderFieldRating => '評価'; + + @override + String get orderFieldReviewCount => 'レビュー数'; + + @override + String get orderFieldId => 'RJ番号'; + + @override + String get orderFieldMyRating => '自分の評価'; + + @override + String get orderFieldAllAges => '全年齢'; + + @override + String get orderFieldRandom => 'ランダム'; + + @override + String get orderLabel => '並び替え'; + + @override + String get orderDirectionDesc => '降順'; + + @override + String get orderDirectionAsc => '昇順'; + + @override + String get searchOrderNewest => '最新収録'; + + @override + String get searchOrderOldest => '最古の収録'; + + @override + String get searchOrderReleaseDesc => '発売日(新しい順)'; + + @override + String get searchOrderReleaseAsc => '発売日(古い順)'; + + @override + String get searchOrderSalesDesc => '売上(高い順)'; + + @override + String get searchOrderSalesAsc => '売上(低い順)'; + + @override + String get searchOrderPriceDesc => '価格(高い順)'; + + @override + String get searchOrderPriceAsc => '価格(低い順)'; + + @override + String get searchOrderRatingDesc => '評価(高い順)'; + + @override + String get searchOrderReviewCountDesc => 'レビュー数(多い順)'; + + @override + String get searchOrderIdDesc => 'RJ番号(大きい順)'; + + @override + String get searchOrderIdAsc => 'RJ番号(小さい順)'; + + @override + String get searchOrderRandom => 'ランダム順'; + + @override + String get favoritesTitle => 'お気に入り'; + + @override + String get pleaseLogin => '先にログインしてください'; + + @override + String get emptyContent => '内容がありません'; + + @override + String get emptyWorks => '作品がありません'; + + @override + String get similarWorksTitle => '関連作品'; + + @override + String get playlistAddToFavorites => 'お気に入りに追加'; + + @override + String get playlistEmpty => 'リストがありません'; + + @override + String playlistAddSuccess(String name) { + return '追加しました: $name'; + } + + @override + String playlistRemoveSuccess(String name) { + return '削除しました: $name'; + } + + @override + String get playlistSystemMarked => '自分のマーク'; + + @override + String get playlistSystemLiked => 'いいねした'; + + @override + String playlistWorksCount(int count) { + return '$count 件の作品'; + } + + @override + String get workActionFavorite => 'お気に入り'; + + @override + String get workActionMark => 'マーク'; + + @override + String get workActionRate => '評価'; + + @override + String get workActionChecking => '確認中'; + + @override + String get workActionRecommend => 'おすすめ'; + + @override + String get workActionNoRecommendation => 'おすすめはありません'; + + @override + String get markStatusTitle => 'マーク状態'; + + @override + String get markStatusWantToListen => '聴きたい'; + + @override + String get markStatusListening => '聴いている'; + + @override + String get markStatusListened => '聴いた'; + + @override + String get markStatusRelistening => '聴き直し'; + + @override + String get markStatusOnHold => '保留'; + + @override + String markUpdated(String status) { + return '状態を$statusに変更'; + } + + @override + String markFailed(String error) { + return 'マーク失敗: $error'; + } + + @override + String get workFilesTitle => 'ファイル一覧'; + + @override + String playUnsupportedFileType(String type) { + return '未対応形式: $type'; + } + + @override + String get playUrlMissing => '再生できません: URLがありません'; + + @override + String get playFilesNotLoaded => 'ファイル一覧未読み込み'; + + @override + String playFailed(String error) { + return '再生失敗: $error'; + } + + @override + String operationFailed(String error) { + return '操作失敗: $error'; + } + + @override + String get cacheManagerTitle => 'キャッシュ管理'; + + @override + String get cacheAudio => '音声キャッシュ'; + + @override + String get cacheSubtitle => '字幕キャッシュ'; + + @override + String get cacheTotal => 'キャッシュ総量'; + + @override + String get cacheClear => '削除'; + + @override + String get cacheClearAll => '全て削除'; + + @override + String get cacheInfoTitle => 'キャッシュについて'; + + @override + String get cacheDescription => + 'キャッシュは直近の音声と字幕を保存し、次回再生を速くします。期限切れや容量超過は自動で整理されます。'; + + @override + String cacheLoadFailed(String error) { + return '読み込み失敗: $error'; + } + + @override + String cacheClearFailed(String error) { + return '削除失敗: $error'; + } + + @override + String get subtitleTag => '字幕'; + + @override + String get noPlaying => '再生中なし'; + + @override + String get screenOnDisable => '画面常時オンをオフ'; + + @override + String get screenOnEnable => '画面常時オンをオン'; + + @override + String get unknownWorkTitle => '不明な作品'; + + @override + String get unknownArtist => '不明な出演者'; + + @override + String get lyricsEmpty => '歌詞なし'; + + @override + String get loginTitle => 'ログイン'; + + @override + String get loginUsernameLabel => 'ユーザー名'; + + @override + String get loginPasswordLabel => 'パスワード'; + + @override + String get loginAction => 'ログイン'; +} diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart new file mode 100644 index 0000000..631df50 --- /dev/null +++ b/lib/l10n/app_localizations_zh.dart @@ -0,0 +1,342 @@ +// 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 appName => 'asmr.one'; + + @override + String get retry => '重试'; + + @override + String get cancel => '取消'; + + @override + String get confirm => '确认'; + + @override + String get login => '登录'; + + @override + String get favorites => '我的收藏'; + + @override + String get settings => '设置'; + + @override + String get cacheManager => '缓存管理'; + + @override + String get screenAlwaysOn => '屏幕常亮'; + + @override + String get themeSystem => '跟随系统主题'; + + @override + String get themeLight => '浅色模式'; + + @override + String get themeDark => '深色模式'; + + @override + String get navigationFavorites => '收藏'; + + @override + String get navigationHome => '主页'; + + @override + String get navigationForYou => '为你推荐'; + + @override + String get navigationPopularWorks => '热门作品'; + + @override + String get navigationRecommend => '推荐'; + + @override + String titleWithCount(String title, int count) { + return '$title ($count)'; + } + + @override + String get search => '搜索'; + + @override + String get searchHint => '搜索...'; + + @override + String get searchPromptInitial => '输入关键词开始搜索'; + + @override + String get searchNoResults => '没有找到相关结果'; + + @override + String get subtitle => '字幕'; + + @override + String get subtitleAvailable => '有字幕'; + + @override + String get orderFieldCollectionTime => '收录时间'; + + @override + String get orderFieldReleaseDate => '发售日期'; + + @override + String get orderFieldSales => '销量'; + + @override + String get orderFieldPrice => '价格'; + + @override + String get orderFieldRating => '评价'; + + @override + String get orderFieldReviewCount => '评论数量'; + + @override + String get orderFieldId => 'RJ号'; + + @override + String get orderFieldMyRating => '我的评价'; + + @override + String get orderFieldAllAges => '全年龄'; + + @override + String get orderFieldRandom => '随机'; + + @override + String get orderLabel => '排序'; + + @override + String get orderDirectionDesc => '降序'; + + @override + String get orderDirectionAsc => '升序'; + + @override + String get searchOrderNewest => '最新收录'; + + @override + String get searchOrderOldest => '最早收录'; + + @override + String get searchOrderReleaseDesc => '发售日期倒序'; + + @override + String get searchOrderReleaseAsc => '发售日期顺序'; + + @override + String get searchOrderSalesDesc => '销量倒序'; + + @override + String get searchOrderSalesAsc => '销量顺序'; + + @override + String get searchOrderPriceDesc => '价格倒序'; + + @override + String get searchOrderPriceAsc => '价格顺序'; + + @override + String get searchOrderRatingDesc => '评价倒序'; + + @override + String get searchOrderReviewCountDesc => '评论数量倒序'; + + @override + String get searchOrderIdDesc => 'RJ号倒序'; + + @override + String get searchOrderIdAsc => 'RJ号顺序'; + + @override + String get searchOrderRandom => '随机排序'; + + @override + String get favoritesTitle => '我的收藏'; + + @override + String get pleaseLogin => '请先登录'; + + @override + String get emptyContent => '暂无内容'; + + @override + String get emptyWorks => '暂无作品'; + + @override + String get similarWorksTitle => '相关推荐'; + + @override + String get playlistAddToFavorites => '添加到收藏夹'; + + @override + String get playlistEmpty => '暂无收藏夹'; + + @override + String playlistAddSuccess(String name) { + return '添加成功: $name'; + } + + @override + String playlistRemoveSuccess(String name) { + return '移除成功: $name'; + } + + @override + String get playlistSystemMarked => '我标记的'; + + @override + String get playlistSystemLiked => '我喜欢的'; + + @override + String playlistWorksCount(int count) { + return '$count 个作品'; + } + + @override + String get workActionFavorite => '收藏'; + + @override + String get workActionMark => '标记'; + + @override + String get workActionRate => '评分'; + + @override + String get workActionChecking => '检查中'; + + @override + String get workActionRecommend => '相关推荐'; + + @override + String get workActionNoRecommendation => '暂无推荐'; + + @override + String get markStatusTitle => '标记状态'; + + @override + String get markStatusWantToListen => '想听'; + + @override + String get markStatusListening => '在听'; + + @override + String get markStatusListened => '听过'; + + @override + String get markStatusRelistening => '重听'; + + @override + String get markStatusOnHold => '搁置'; + + @override + String markUpdated(String status) { + return '已标记为$status'; + } + + @override + String markFailed(String error) { + return '标记失败: $error'; + } + + @override + String get workFilesTitle => '文件列表'; + + @override + String playUnsupportedFileType(String type) { + return '不支持的文件类型: $type'; + } + + @override + String get playUrlMissing => '无法播放:文件URL不存在'; + + @override + String get playFilesNotLoaded => '文件列表未加载'; + + @override + String playFailed(String error) { + return '播放失败: $error'; + } + + @override + String operationFailed(String error) { + return '操作失败: $error'; + } + + @override + String get cacheManagerTitle => '缓存管理'; + + @override + String get cacheAudio => '音频缓存'; + + @override + String get cacheSubtitle => '字幕缓存'; + + @override + String get cacheTotal => '总缓存大小'; + + @override + String get cacheClear => '清理'; + + @override + String get cacheClearAll => '清理全部'; + + @override + String get cacheInfoTitle => '缓存说明'; + + @override + String get cacheDescription => + '缓存用于存储最近播放的音频文件和字幕文件,以提高再次播放时的加载速度。系统会自动清理过期和超量的缓存。'; + + @override + String cacheLoadFailed(String error) { + return '加载失败: $error'; + } + + @override + String cacheClearFailed(String error) { + return '清理失败: $error'; + } + + @override + String get subtitleTag => '字幕'; + + @override + String get noPlaying => '未在播放'; + + @override + String get screenOnDisable => '关闭屏幕常亮'; + + @override + String get screenOnEnable => '开启屏幕常亮'; + + @override + String get unknownWorkTitle => '未知作品'; + + @override + String get unknownArtist => '未知演员'; + + @override + String get lyricsEmpty => '无歌词'; + + @override + String get loginTitle => '登录'; + + @override + String get loginUsernameLabel => '用户名'; + + @override + String get loginPasswordLabel => '密码'; + + @override + String get loginAction => '登录'; +} diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb new file mode 100644 index 0000000..b4de91f --- /dev/null +++ b/lib/l10n/app_zh.arb @@ -0,0 +1,186 @@ +{ + "@@locale": "zh", + "appName": "asmr.one", + "retry": "重试", + "cancel": "取消", + "confirm": "确认", + "login": "登录", + "favorites": "我的收藏", + "settings": "设置", + "cacheManager": "缓存管理", + "screenAlwaysOn": "屏幕常亮", + "themeSystem": "跟随系统主题", + "themeLight": "浅色模式", + "themeDark": "深色模式", + "navigationFavorites": "收藏", + "navigationHome": "主页", + "navigationForYou": "为你推荐", + "navigationPopularWorks": "热门作品", + "navigationRecommend": "推荐", + "titleWithCount": "{title} ({count})", + "@titleWithCount": { + "placeholders": { + "title": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "search": "搜索", + "searchHint": "搜索...", + "searchPromptInitial": "输入关键词开始搜索", + "searchNoResults": "没有找到相关结果", + "subtitle": "字幕", + "subtitleAvailable": "有字幕", + "orderFieldCollectionTime": "收录时间", + "orderFieldReleaseDate": "发售日期", + "orderFieldSales": "销量", + "orderFieldPrice": "价格", + "orderFieldRating": "评价", + "orderFieldReviewCount": "评论数量", + "orderFieldId": "RJ号", + "orderFieldMyRating": "我的评价", + "orderFieldAllAges": "全年龄", + "orderFieldRandom": "随机", + "orderLabel": "排序", + "orderDirectionDesc": "降序", + "orderDirectionAsc": "升序", + "searchOrderNewest": "最新收录", + "searchOrderOldest": "最早收录", + "searchOrderReleaseDesc": "发售日期倒序", + "searchOrderReleaseAsc": "发售日期顺序", + "searchOrderSalesDesc": "销量倒序", + "searchOrderSalesAsc": "销量顺序", + "searchOrderPriceDesc": "价格倒序", + "searchOrderPriceAsc": "价格顺序", + "searchOrderRatingDesc": "评价倒序", + "searchOrderReviewCountDesc": "评论数量倒序", + "searchOrderIdDesc": "RJ号倒序", + "searchOrderIdAsc": "RJ号顺序", + "searchOrderRandom": "随机排序", + "favoritesTitle": "我的收藏", + "pleaseLogin": "请先登录", + "emptyContent": "暂无内容", + "emptyWorks": "暂无作品", + "similarWorksTitle": "相关推荐", + "playlistAddToFavorites": "添加到收藏夹", + "playlistEmpty": "暂无收藏夹", + "playlistAddSuccess": "添加成功: {name}", + "@playlistAddSuccess": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "playlistRemoveSuccess": "移除成功: {name}", + "@playlistRemoveSuccess": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "playlistSystemMarked": "我标记的", + "playlistSystemLiked": "我喜欢的", + "playlistWorksCount": "{count} 个作品", + "@playlistWorksCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "workActionFavorite": "收藏", + "workActionMark": "标记", + "workActionRate": "评分", + "workActionChecking": "检查中", + "workActionRecommend": "相关推荐", + "workActionNoRecommendation": "暂无推荐", + "markStatusTitle": "标记状态", + "markStatusWantToListen": "想听", + "markStatusListening": "在听", + "markStatusListened": "听过", + "markStatusRelistening": "重听", + "markStatusOnHold": "搁置", + "markUpdated": "已标记为{status}", + "@markUpdated": { + "placeholders": { + "status": { + "type": "String" + } + } + }, + "markFailed": "标记失败: {error}", + "@markFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "workFilesTitle": "文件列表", + "playUnsupportedFileType": "不支持的文件类型: {type}", + "@playUnsupportedFileType": { + "placeholders": { + "type": { + "type": "String" + } + } + }, + "playUrlMissing": "无法播放:文件URL不存在", + "playFilesNotLoaded": "文件列表未加载", + "playFailed": "播放失败: {error}", + "@playFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "operationFailed": "操作失败: {error}", + "@operationFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "cacheManagerTitle": "缓存管理", + "cacheAudio": "音频缓存", + "cacheSubtitle": "字幕缓存", + "cacheTotal": "总缓存大小", + "cacheClear": "清理", + "cacheClearAll": "清理全部", + "cacheInfoTitle": "缓存说明", + "cacheDescription": "缓存用于存储最近播放的音频文件和字幕文件,以提高再次播放时的加载速度。系统会自动清理过期和超量的缓存。", + "cacheLoadFailed": "加载失败: {error}", + "@cacheLoadFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "cacheClearFailed": "清理失败: {error}", + "@cacheClearFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "subtitleTag": "字幕", + "noPlaying": "未在播放", + "screenOnDisable": "关闭屏幕常亮", + "screenOnEnable": "开启屏幕常亮", + "unknownWorkTitle": "未知作品", + "unknownArtist": "未知演员", + "lyricsEmpty": "无歌词", + "loginTitle": "登录", + "loginUsernameLabel": "用户名", + "loginPasswordLabel": "密码", + "loginAction": "登录" +} diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart new file mode 100644 index 0000000..df574ff --- /dev/null +++ b/lib/l10n/l10n.dart @@ -0,0 +1,6 @@ +import 'package:flutter/widgets.dart'; +import 'package:asmrapp/l10n/app_localizations.dart'; + +extension L10nX on BuildContext { + AppLocalizations get l10n => AppLocalizations.of(this)!; +} diff --git a/lib/main.dart b/lib/main.dart index e9b9afb..3f0cbd3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,14 @@ -import 'package:flutter/material.dart'; -import 'package:asmrapp/common/constants/strings.dart'; +import 'package:asmrapp/core/theme/app_theme.dart'; +import 'package:asmrapp/core/theme/theme_controller.dart'; +import 'package:asmrapp/l10n/app_localizations.dart'; +import 'package:asmrapp/l10n/l10n.dart'; import 'package:asmrapp/presentation/viewmodels/auth_viewmodel.dart'; -import 'core/di/service_locator.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:provider/provider.dart'; + +import 'core/di/service_locator.dart'; import 'screens/main_screen.dart'; -import 'package:asmrapp/core/theme/app_theme.dart'; -import 'package:asmrapp/core/theme/theme_controller.dart'; import 'screens/search_screen.dart'; void main() async { @@ -34,7 +37,14 @@ class MyApp extends StatelessWidget { child: Consumer( builder: (context, themeController, child) { return MaterialApp( - title: Strings.appName, + onGenerateTitle: (context) => context.l10n.appName, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, theme: AppTheme.light, darkTheme: AppTheme.dark, themeMode: themeController.themeMode, diff --git a/lib/presentation/models/filter_state.dart b/lib/presentation/models/filter_state.dart index facbec0..33e986f 100644 --- a/lib/presentation/models/filter_state.dart +++ b/lib/presentation/models/filter_state.dart @@ -9,7 +9,8 @@ class FilterState { bool get showSortDirection => orderField != 'random'; - String get sortValue => orderField == 'random' ? 'desc' : (isDescending ? 'desc' : 'asc'); + String get sortValue => + orderField == 'random' ? 'desc' : (isDescending ? 'desc' : 'asc'); FilterState copyWith({ String? orderField, @@ -23,13 +24,13 @@ class FilterState { // 用于持久化 Map toJson() => { - 'orderField': orderField, - 'isDescending': isDescending, - }; + 'orderField': orderField, + 'isDescending': isDescending, + }; // 从持久化恢复 factory FilterState.fromJson(Map json) => FilterState( - orderField: json['orderField'] ?? 'create_date', - isDescending: json['isDescending'] ?? true, - ); -} \ No newline at end of file + orderField: json['orderField'] ?? 'create_date', + isDescending: json['isDescending'] ?? true, + ); +} diff --git a/lib/presentation/viewmodels/auth_viewmodel.dart b/lib/presentation/viewmodels/auth_viewmodel.dart index 85b1242..588469e 100644 --- a/lib/presentation/viewmodels/auth_viewmodel.dart +++ b/lib/presentation/viewmodels/auth_viewmodel.dart @@ -37,10 +37,10 @@ class AuthViewModel extends ChangeNotifier { try { AppLogger.info('AuthViewModel: 开始登录流程'); _authData = await _authService.login(name, password); - + // 保存认证数据 await _authRepository.saveAuthData(_authData!); - + AppLogger.info(''' 登录成功,完整数据: - token: ${_authData?.token} @@ -50,7 +50,6 @@ class AuthViewModel extends ChangeNotifier { - email: ${_authData?.user?.email} - recommenderUuid: ${_authData?.user?.recommenderUuid} '''); - } catch (e) { AppLogger.error('AuthViewModel: 登录失败', e); _error = e.toString(); @@ -69,7 +68,7 @@ class AuthViewModel extends ChangeNotifier { - group: ${_authData?.user?.group} - token: ${_authData?.token} '''); - + await _authRepository.clearAuthData(); _authData = null; notifyListeners(); @@ -91,4 +90,4 @@ class AuthViewModel extends ChangeNotifier { } notifyListeners(); } -} \ No newline at end of file +} diff --git a/lib/presentation/viewmodels/base/paginated_works_viewmodel.dart b/lib/presentation/viewmodels/base/paginated_works_viewmodel.dart index 80f615f..d9ae5b2 100644 --- a/lib/presentation/viewmodels/base/paginated_works_viewmodel.dart +++ b/lib/presentation/viewmodels/base/paginated_works_viewmodel.dart @@ -30,9 +30,10 @@ abstract class PaginatedWorksViewModel extends ChangeNotifier { bool get isLoading => _isLoading; String? get error => _error; int get currentPage => _currentPage; - int? get totalPages => _pagination?.totalCount != null && _pagination?.pageSize != null - ? (_pagination!.totalCount! / _pagination!.pageSize!).ceil() - : null; + int? get totalPages => + _pagination?.totalCount != null && _pagination?.pageSize != null + ? (_pagination!.totalCount! / _pagination!.pageSize!).ceil() + : null; // 获取页面名称,用于日志 String get pageName; @@ -82,4 +83,4 @@ abstract class PaginatedWorksViewModel extends ChangeNotifier { // 添加 pagination getter Pagination? get pagination => _pagination; -} \ No newline at end of file +} diff --git a/lib/presentation/viewmodels/detail_viewmodel.dart b/lib/presentation/viewmodels/detail_viewmodel.dart index 71d0629..96b1e8c 100644 --- a/lib/presentation/viewmodels/detail_viewmodel.dart +++ b/lib/presentation/viewmodels/detail_viewmodel.dart @@ -13,6 +13,27 @@ import 'package:asmrapp/widgets/detail/playlist_selection_dialog.dart'; import 'package:asmrapp/data/models/mark_status.dart'; import 'package:asmrapp/widgets/detail/mark_selection_dialog.dart'; import 'package:dio/dio.dart'; +import 'package:asmrapp/l10n/l10n.dart'; +import 'package:asmrapp/common/extensions/mark_status_localizations.dart'; + +enum PlaybackError { + unsupportedType, + missingUrl, + filesNotLoaded, + failed, +} + +class PlaybackException implements Exception { + final PlaybackError error; + final String? detail; + final Object? originalError; + + const PlaybackException( + this.error, { + this.detail, + this.originalError, + }); +} class DetailViewModel extends ChangeNotifier { late final ApiService _apiService; @@ -23,7 +44,7 @@ class DetailViewModel extends ChangeNotifier { bool _isLoading = false; String? _error; bool _disposed = false; - + bool _hasRecommendations = false; bool _checkingRecommendations = false; @@ -63,10 +84,11 @@ class DetailViewModel extends ChangeNotifier { bool get loadingPlaylists => _loadingPlaylists; String? get playlistsError => _playlistsError; List? get playlists => _playlists; - int? get playlistsTotalPages => - _playlistsPagination?.totalCount != null && _playlistsPagination?.pageSize != null - ? (_playlistsPagination!.totalCount! / _playlistsPagination!.pageSize!).ceil() - : null; + int? get playlistsTotalPages => _playlistsPagination?.totalCount != null && + _playlistsPagination?.pageSize != null + ? (_playlistsPagination!.totalCount! / _playlistsPagination!.pageSize!) + .ceil() + : null; Future _checkRecommendations() async { _checkingRecommendations = true; @@ -118,15 +140,18 @@ class DetailViewModel extends ChangeNotifier { Future playFile(Child file, BuildContext context) async { if (file.type?.toLowerCase() != 'audio') { - throw Exception('不支持的文件类型: ${file.type}'); + throw PlaybackException( + PlaybackError.unsupportedType, + detail: file.type, + ); } if (file.mediaDownloadUrl == null) { - throw Exception('无法播放:文件URL不存在'); + throw const PlaybackException(PlaybackError.missingUrl); } if (_files == null) { - throw Exception('文件列表未加载'); + throw const PlaybackException(PlaybackError.filesNotLoaded); } try { @@ -141,7 +166,11 @@ class DetailViewModel extends ChangeNotifier { if (!_disposed) { AppLogger.error('播放失败', e); } - rethrow; + throw PlaybackException( + PlaybackError.failed, + detail: e.toString(), + originalError: e, + ); } } @@ -158,7 +187,7 @@ class DetailViewModel extends ChangeNotifier { workId: work.id.toString(), page: page, ); - + _playlists = response.playlists; _playlistsPagination = response.pagination; AppLogger.info('收藏夹列表加载成功: ${_playlists?.length ?? 0}个收藏夹'); @@ -174,14 +203,14 @@ class DetailViewModel extends ChangeNotifier { Future showPlaylistsDialog(BuildContext context) async { _loadingFavorite = true; notifyListeners(); - + try { await loadPlaylists(); _loadingFavorite = false; notifyListeners(); - + if (!context.mounted) return; - + await showDialog( context: context, builder: (context) => PlaylistSelectionDialog( @@ -195,7 +224,11 @@ class DetailViewModel extends ChangeNotifier { } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('操作失败: $e')), + SnackBar( + content: Text( + context.l10n.operationFailed(e.toString()), + ), + ), ); } } @@ -222,7 +255,7 @@ class DetailViewModel extends ChangeNotifier { workId: work.id.toString(), ); } - + // 更新本地收藏夹状态 final index = _playlists?.indexWhere((p) => p.id == playlist.id); if (index != null && index != -1) { @@ -230,7 +263,7 @@ class DetailViewModel extends ChangeNotifier { ..[index] = playlist.copyWith(exist: !(playlist.exist ?? false)); notifyListeners(); } - + final action = (playlist.exist ?? false) ? '移除' : '添加'; AppLogger.info('$action收藏成功: ${playlist.name}'); } catch (e) { @@ -248,7 +281,7 @@ class DetailViewModel extends ChangeNotifier { work.id.toString(), _apiService.convertMarkStatusToApi(status), ); - + _currentMarkStatus = status; AppLogger.info('更新标记状态成功: ${status.label}'); } catch (e) { @@ -272,7 +305,10 @@ class DetailViewModel extends ChangeNotifier { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('已标记为${status.label}'), + content: Text( + context.l10n + .markUpdated(status.localizedLabel(context.l10n)), + ), duration: const Duration(seconds: 2), behavior: SnackBarBehavior.floating, ), @@ -281,7 +317,9 @@ class DetailViewModel extends ChangeNotifier { } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('标记失败: $e')), + SnackBar( + content: Text(context.l10n.markFailed(e.toString())), + ), ); } } diff --git a/lib/presentation/viewmodels/favorites_viewmodel.dart b/lib/presentation/viewmodels/favorites_viewmodel.dart index cd0ed6a..ad7f550 100644 --- a/lib/presentation/viewmodels/favorites_viewmodel.dart +++ b/lib/presentation/viewmodels/favorites_viewmodel.dart @@ -52,4 +52,4 @@ class FavoritesViewModel extends ChangeNotifier { Future loadFavorites({bool refresh = false}) async { await loadPage(1); } -} \ No newline at end of file +} diff --git a/lib/presentation/viewmodels/home_viewmodel.dart b/lib/presentation/viewmodels/home_viewmodel.dart index 8486e5f..a1b1bee 100644 --- a/lib/presentation/viewmodels/home_viewmodel.dart +++ b/lib/presentation/viewmodels/home_viewmodel.dart @@ -9,11 +9,11 @@ import 'package:shared_preferences/shared_preferences.dart'; class HomeViewModel extends PaginatedWorksViewModel { static const String _filterStateKey = 'home_filter_state'; static const String _subtitleFilterKey = 'subtitle_filter'; - + bool _filterPanelExpanded = false; bool _hasSubtitle = false; FilterState _filterState = const FilterState(); - + bool get filterPanelExpanded => _filterPanelExpanded; bool get hasSubtitle => _hasSubtitle; FilterState get filterState => _filterState; diff --git a/lib/presentation/viewmodels/player_viewmodel.dart b/lib/presentation/viewmodels/player_viewmodel.dart index b7e0644..7c2c4e0 100644 --- a/lib/presentation/viewmodels/player_viewmodel.dart +++ b/lib/presentation/viewmodels/player_viewmodel.dart @@ -29,9 +29,9 @@ class PlayerViewModel extends ChangeNotifier { required IAudioPlayerService audioService, required PlaybackEventHub eventHub, required ISubtitleService subtitleService, - }) : _audioService = audioService, - _eventHub = eventHub, - _subtitleService = subtitleService { + }) : _audioService = audioService, + _eventHub = eventHub, + _subtitleService = subtitleService { _initStreams(); _requestInitialState(); } @@ -174,10 +174,8 @@ class PlayerViewModel extends ChangeNotifier { // 修改字幕加载方法,返回 Future 以便等待加载完成 Future _loadSubtitleIfAvailable(PlaybackContext context) async { - final subtitleFile = _subtitleLoader.findSubtitleFile( - context.currentFile, - context.files - ); + final subtitleFile = + _subtitleLoader.findSubtitleFile(context.currentFile, context.files); if (subtitleFile?.mediaDownloadUrl != null) { await _subtitleService.loadSubtitle(subtitleFile!.mediaDownloadUrl!); } else { @@ -192,7 +190,7 @@ class PlayerViewModel extends ChangeNotifier { Future seekToNextLyric() async { final currentSubtitle = _subtitleService.currentSubtitleWithState; final subtitleList = _subtitleService.subtitleList; - + if (currentSubtitle != null && subtitleList != null) { final nextSubtitle = currentSubtitle.subtitle.getNext(subtitleList); if (nextSubtitle != null) { @@ -204,9 +202,10 @@ class PlayerViewModel extends ChangeNotifier { Future seekToPreviousLyric() async { final currentSubtitle = _subtitleService.currentSubtitleWithState; final subtitleList = _subtitleService.subtitleList; - + if (currentSubtitle != null && subtitleList != null) { - final previousSubtitle = currentSubtitle.subtitle.getPrevious(subtitleList); + final previousSubtitle = + currentSubtitle.subtitle.getPrevious(subtitleList); if (previousSubtitle != null) { await seek(previousSubtitle.start); } diff --git a/lib/presentation/viewmodels/playlist_works_viewmodel.dart b/lib/presentation/viewmodels/playlist_works_viewmodel.dart index 2b48f59..e604add 100644 --- a/lib/presentation/viewmodels/playlist_works_viewmodel.dart +++ b/lib/presentation/viewmodels/playlist_works_viewmodel.dart @@ -9,7 +9,7 @@ import 'package:get_it/get_it.dart'; class PlaylistWorksViewModel extends ChangeNotifier { final ApiService _apiService = GetIt.I(); final Playlist playlist; - + List _works = []; bool _isLoading = false; String? _error; @@ -22,9 +22,10 @@ class PlaylistWorksViewModel extends ChangeNotifier { bool get isLoading => _isLoading; String? get error => _error; int get currentPage => _currentPage; - int? get totalPages => _pagination?.totalCount != null && _pagination?.pageSize != null - ? (_pagination!.totalCount! / _pagination!.pageSize!).ceil() - : null; + int? get totalPages => + _pagination?.totalCount != null && _pagination?.pageSize != null + ? (_pagination!.totalCount! / _pagination!.pageSize!).ceil() + : null; Future loadWorks({int page = 1}) async { if (_isLoading) return; @@ -39,7 +40,7 @@ class PlaylistWorksViewModel extends ChangeNotifier { playlistId: playlist.id!, page: page, ); - + _works = response.works; _pagination = response.pagination; _currentPage = page; diff --git a/lib/presentation/viewmodels/playlists_viewmodel.dart b/lib/presentation/viewmodels/playlists_viewmodel.dart index f3eb659..44f7d33 100644 --- a/lib/presentation/viewmodels/playlists_viewmodel.dart +++ b/lib/presentation/viewmodels/playlists_viewmodel.dart @@ -1,15 +1,14 @@ -import 'package:asmrapp/data/models/works/work.dart'; -import 'package:flutter/foundation.dart'; -import 'package:asmrapp/data/models/my_lists/my_playlists/my_playlists.dart'; -import 'package:asmrapp/data/models/my_lists/my_playlists/playlist.dart'; import 'package:asmrapp/data/models/my_lists/my_playlists/pagination.dart'; +import 'package:asmrapp/data/models/my_lists/my_playlists/playlist.dart'; +import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/data/services/api_service.dart'; import 'package:asmrapp/utils/logger.dart'; +import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; class PlaylistsViewModel extends ChangeNotifier { final ApiService _apiService = GetIt.I(); - + List? _playlists; bool _isLoading = false; String? _error; @@ -29,17 +28,19 @@ class PlaylistsViewModel extends ChangeNotifier { bool get isLoading => _isLoading; String? get error => _error; int get currentPage => _currentPage; - int? get totalPages => _pagination?.totalCount != null && _pagination?.pageSize != null - ? (_pagination!.totalCount! / _pagination!.pageSize!).ceil() - : null; + int? get totalPages => + _pagination?.totalCount != null && _pagination?.pageSize != null + ? (_pagination!.totalCount! / _pagination!.pageSize!).ceil() + : null; Playlist? get selectedPlaylist => _selectedPlaylist; List get playlistWorks => _playlistWorks; bool get loadingWorks => _loadingWorks; String? get worksError => _worksError; int get worksCurrentPage => _worksCurrentPage; - int? get worksTotalPages => _worksPagination?.totalCount != null && _worksPagination?.pageSize != null - ? (_worksPagination!.totalCount! / _worksPagination!.pageSize!).ceil() - : null; + int? get worksTotalPages => + _worksPagination?.totalCount != null && _worksPagination?.pageSize != null + ? (_worksPagination!.totalCount! / _worksPagination!.pageSize!).ceil() + : null; PlaylistsViewModel() { loadPlaylists(); @@ -82,7 +83,7 @@ class PlaylistsViewModel extends ChangeNotifier { _worksPagination = null; _worksCurrentPage = 1; notifyListeners(); - + await loadPlaylistWorks(); } @@ -99,7 +100,9 @@ class PlaylistsViewModel extends ChangeNotifier { /// 加载播放列表作品 Future loadPlaylistWorks({int page = 1}) async { if (_loadingWorks || _selectedPlaylist == null) return; - if (page < 1 || (worksTotalPages != null && page > worksTotalPages!)) return; + if (page < 1 || (worksTotalPages != null && page > worksTotalPages!)) { + return; + } _loadingWorks = true; _worksError = null; @@ -110,7 +113,7 @@ class PlaylistsViewModel extends ChangeNotifier { playlistId: _selectedPlaylist!.id!, page: page, ); - + _playlistWorks = response.works; _worksPagination = response.pagination as Pagination?; _worksCurrentPage = page; @@ -127,21 +130,9 @@ class PlaylistsViewModel extends ChangeNotifier { /// 刷新播放列表作品 Future refreshWorks() => loadPlaylistWorks(page: 1); - /// 获取播放列表显示名称 - String getDisplayName(String? name) { - switch (name) { - case '__SYS_PLAYLIST_MARKED': - return '我标记的'; - case '__SYS_PLAYLIST_LIKED': - return '我喜欢的'; - default: - return name ?? ''; - } - } - @override void dispose() { AppLogger.info('销毁 PlaylistsViewModel'); super.dispose(); } -} \ No newline at end of file +} diff --git a/lib/presentation/viewmodels/popular_viewmodel.dart b/lib/presentation/viewmodels/popular_viewmodel.dart index 08786c0..5b6ecc9 100644 --- a/lib/presentation/viewmodels/popular_viewmodel.dart +++ b/lib/presentation/viewmodels/popular_viewmodel.dart @@ -15,7 +15,7 @@ class PopularViewModel extends PaginatedWorksViewModel { @override Future onInit() async { - await _loadSubtitleFilter(); // 使用 onInit 钩子加载状态 + await _loadSubtitleFilter(); // 使用 onInit 钩子加载状态 } Future _loadSubtitleFilter() async { @@ -81,12 +81,12 @@ class PopularViewModel extends PaginatedWorksViewModel { } // 保持原有的便捷方法 - Future loadPopular({bool refresh = false}) => - refresh ? this.refresh() : loadPage(1); + Future loadPopular({bool refresh = false}) => + refresh ? this.refresh() : loadPage(1); @override void dispose() { _saveFilterState(); super.dispose(); } -} \ No newline at end of file +} diff --git a/lib/presentation/viewmodels/recommend_viewmodel.dart b/lib/presentation/viewmodels/recommend_viewmodel.dart index 9fc15d9..f470018 100644 --- a/lib/presentation/viewmodels/recommend_viewmodel.dart +++ b/lib/presentation/viewmodels/recommend_viewmodel.dart @@ -8,7 +8,8 @@ import 'package:get_it/get_it.dart'; import 'package:shared_preferences/shared_preferences.dart'; class RecommendViewModel extends ChangeNotifier { - static const _subtitleFilterKey = 'subtitle_filter'; // 与 PopularViewModel 使用相同的 key 实现全局共享 + static const _subtitleFilterKey = + 'subtitle_filter'; // 与 PopularViewModel 使用相同的 key 实现全局共享 final ApiService _apiService; final AuthViewModel _authViewModel; List _works = []; @@ -18,8 +19,10 @@ class RecommendViewModel extends ChangeNotifier { int _currentPage = 1; bool _hasSubtitle = false; bool _filterPanelExpanded = false; + bool _loginRequired = false; - RecommendViewModel(this._authViewModel) : _apiService = GetIt.I() { + RecommendViewModel(this._authViewModel) + : _apiService = GetIt.I() { _loadFilterState(); } @@ -57,6 +60,7 @@ class RecommendViewModel extends ChangeNotifier { : null; bool get hasSubtitle => _hasSubtitle; bool get filterPanelExpanded => _filterPanelExpanded; + bool get loginRequired => _loginRequired; Pagination? get pagination => _pagination; @@ -84,17 +88,19 @@ class RecommendViewModel extends ChangeNotifier { Future loadPage(int page) async { if (_isLoading) return; if (page < 1 || (totalPages != null && page > totalPages!)) return; - + // 检查是否已登录 final uuid = _authViewModel.recommenderUuid; if (uuid == null) { - _error = '请先登录'; + _loginRequired = true; + _error = null; notifyListeners(); return; } _isLoading = true; _error = null; + _loginRequired = false; notifyListeners(); try { @@ -126,4 +132,4 @@ class RecommendViewModel extends ChangeNotifier { _saveFilterState(); // 在销毁时保存状态 super.dispose(); } -} \ No newline at end of file +} diff --git a/lib/presentation/viewmodels/settings/cache_manager_viewmodel.dart b/lib/presentation/viewmodels/settings/cache_manager_viewmodel.dart index 4cb0428..459a9cf 100644 --- a/lib/presentation/viewmodels/settings/cache_manager_viewmodel.dart +++ b/lib/presentation/viewmodels/settings/cache_manager_viewmodel.dart @@ -7,13 +7,16 @@ class CacheManagerViewModel extends ChangeNotifier { bool _isLoading = false; int _audioCacheSize = 0; int _subtitleCacheSize = 0; - String? _error; + String? _errorDetail; + CacheOperation? _lastFailedOperation; bool get isLoading => _isLoading; int get audioCacheSize => _audioCacheSize; int get subtitleCacheSize => _subtitleCacheSize; int get totalCacheSize => _audioCacheSize + _subtitleCacheSize; - String? get error => _error; + String? get error => _errorDetail; + String? get errorDetail => _errorDetail; + CacheOperation? get lastFailedOperation => _lastFailedOperation; // 格式化缓存大小显示 String _formatSize(int size) { @@ -30,17 +33,20 @@ class CacheManagerViewModel extends ChangeNotifier { Future loadCacheSize() async { try { _isLoading = true; + _errorDetail = null; + _lastFailedOperation = null; notifyListeners(); - + // 获取音频缓存大小 _audioCacheSize = await AudioCacheManager.getCacheSize(); // 获取字幕缓存大小 _subtitleCacheSize = await SubtitleCacheManager.getSize(); - - _error = null; + + _errorDetail = null; } catch (e) { AppLogger.error('加载缓存大小失败', e); - _error = '加载失败: $e'; + _errorDetail = e.toString(); + _lastFailedOperation = CacheOperation.load; } finally { _isLoading = false; notifyListeners(); @@ -51,14 +57,17 @@ class CacheManagerViewModel extends ChangeNotifier { Future clearAudioCache() async { try { _isLoading = true; + _errorDetail = null; + _lastFailedOperation = null; notifyListeners(); - + await AudioCacheManager.cleanCache(); await loadCacheSize(); - _error = null; + _errorDetail = null; } catch (e) { AppLogger.error('清理音频缓存失败', e); - _error = '清理失败: $e'; + _errorDetail = e.toString(); + _lastFailedOperation = CacheOperation.clearAudio; } finally { _isLoading = false; notifyListeners(); @@ -69,14 +78,17 @@ class CacheManagerViewModel extends ChangeNotifier { Future clearSubtitleCache() async { try { _isLoading = true; + _errorDetail = null; + _lastFailedOperation = null; notifyListeners(); - + await SubtitleCacheManager.clearCache(); await loadCacheSize(); - _error = null; + _errorDetail = null; } catch (e) { AppLogger.error('清理字幕缓存失败', e); - _error = '清理失败: $e'; + _errorDetail = e.toString(); + _lastFailedOperation = CacheOperation.clearSubtitle; } finally { _isLoading = false; notifyListeners(); @@ -87,21 +99,31 @@ class CacheManagerViewModel extends ChangeNotifier { Future clearAllCache() async { try { _isLoading = true; + _errorDetail = null; + _lastFailedOperation = null; notifyListeners(); - + await Future.wait([ AudioCacheManager.cleanCache(), SubtitleCacheManager.clearCache(), ]); - + await loadCacheSize(); - _error = null; + _errorDetail = null; } catch (e) { AppLogger.error('清理缓存失败', e); - _error = '清理失败: $e'; + _errorDetail = e.toString(); + _lastFailedOperation = CacheOperation.clearAll; } finally { _isLoading = false; notifyListeners(); } } -} \ No newline at end of file +} + +enum CacheOperation { + load, + clearAudio, + clearSubtitle, + clearAll, +} diff --git a/lib/presentation/viewmodels/similar_works_viewmodel.dart b/lib/presentation/viewmodels/similar_works_viewmodel.dart index e56d58f..ed2e00d 100644 --- a/lib/presentation/viewmodels/similar_works_viewmodel.dart +++ b/lib/presentation/viewmodels/similar_works_viewmodel.dart @@ -7,7 +7,8 @@ import 'package:get_it/get_it.dart'; import 'package:shared_preferences/shared_preferences.dart'; class SimilarWorksViewModel extends ChangeNotifier { - static const _subtitleFilterKey = 'subtitle_filter'; // 与其他 ViewModel 使用相同的 key + static const _subtitleFilterKey = + 'subtitle_filter'; // 与其他 ViewModel 使用相同的 key final ApiService _apiService; final Work work; List _works = []; @@ -115,4 +116,4 @@ class SimilarWorksViewModel extends ChangeNotifier { _saveFilterState(); super.dispose(); } -} \ No newline at end of file +} diff --git a/lib/presentation/widgets/auth/login_dialog.dart b/lib/presentation/widgets/auth/login_dialog.dart index 230cd61..085137f 100644 --- a/lib/presentation/widgets/auth/login_dialog.dart +++ b/lib/presentation/widgets/auth/login_dialog.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:asmrapp/presentation/viewmodels/auth_viewmodel.dart'; import 'package:asmrapp/utils/logger.dart'; +import 'package:asmrapp/l10n/l10n.dart'; class LoginDialog extends StatefulWidget { const LoginDialog({super.key}); @@ -25,10 +26,10 @@ class _LoginDialogState extends State { Future _handleLogin() async { final name = _nameController.text.trim(); AppLogger.info('LoginDialog: 尝试登录: name=$name'); - + final authVM = context.read(); await authVM.login(name, _passwordController.text); - + if (mounted) { if (authVM.error == null) { AppLogger.info('LoginDialog: 登录成功,关闭对话框'); @@ -42,15 +43,15 @@ class _LoginDialogState extends State { @override Widget build(BuildContext context) { return AlertDialog( - title: const Text('登录'), + title: Text(context.l10n.loginTitle), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: _nameController, - decoration: const InputDecoration( - labelText: '用户名', - border: OutlineInputBorder(), + decoration: InputDecoration( + labelText: context.l10n.loginUsernameLabel, + border: const OutlineInputBorder(), ), textInputAction: TextInputAction.next, ), @@ -58,7 +59,7 @@ class _LoginDialogState extends State { TextField( controller: _passwordController, decoration: InputDecoration( - labelText: '密码', + labelText: context.l10n.loginPasswordLabel, border: const OutlineInputBorder(), suffixIcon: IconButton( icon: Icon( @@ -94,7 +95,7 @@ class _LoginDialogState extends State { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('取消'), + child: Text(context.l10n.cancel), ), Consumer( builder: (context, authVM, _) { @@ -108,11 +109,11 @@ class _LoginDialogState extends State { strokeWidth: 2, ), ) - : const Text('登录'), + : Text(context.l10n.loginAction), ); }, ), ], ); } -} \ No newline at end of file +} diff --git a/lib/screens/contents/home_content.dart b/lib/screens/contents/home_content.dart index c760295..d3c2127 100644 --- a/lib/screens/contents/home_content.dart +++ b/lib/screens/contents/home_content.dart @@ -1,9 +1,9 @@ +import 'package:asmrapp/presentation/layouts/work_layout_strategy.dart'; +import 'package:asmrapp/presentation/viewmodels/home_viewmodel.dart'; import 'package:asmrapp/widgets/filter/filter_panel.dart'; +import 'package:asmrapp/widgets/work_grid/enhanced_work_grid_view.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:asmrapp/presentation/viewmodels/home_viewmodel.dart'; -import 'package:asmrapp/presentation/layouts/work_layout_strategy.dart'; -import 'package:asmrapp/widgets/work_grid/enhanced_work_grid_view.dart'; class HomeContent extends StatefulWidget { const HomeContent({super.key}); @@ -79,7 +79,7 @@ class _HomeContentState extends State color: Theme.of(context).colorScheme.surface, boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 8, spreadRadius: 1, offset: const Offset(0, 1), diff --git a/lib/screens/contents/playlists/playlist_works_view.dart b/lib/screens/contents/playlists/playlist_works_view.dart index 59fcb58..790f275 100644 --- a/lib/screens/contents/playlists/playlist_works_view.dart +++ b/lib/screens/contents/playlists/playlist_works_view.dart @@ -2,9 +2,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:asmrapp/data/models/my_lists/my_playlists/playlist.dart'; import 'package:asmrapp/presentation/viewmodels/playlist_works_viewmodel.dart'; -import 'package:asmrapp/presentation/viewmodels/playlists_viewmodel.dart'; import 'package:asmrapp/widgets/work_grid/enhanced_work_grid_view.dart'; import 'package:asmrapp/presentation/layouts/work_layout_strategy.dart'; +import 'package:asmrapp/l10n/l10n.dart'; +import 'package:asmrapp/common/utils/playlist_localizations.dart'; class PlaylistWorksView extends StatelessWidget { final Playlist playlist; @@ -19,8 +20,6 @@ class PlaylistWorksView extends StatelessWidget { @override Widget build(BuildContext context) { - final playlistsViewModel = context.read(); - return ChangeNotifierProvider( create: (_) => PlaylistWorksViewModel(playlist)..loadWorks(), child: Consumer( @@ -39,7 +38,7 @@ class PlaylistWorksView extends StatelessWidget { ), Expanded( child: Text( - playlistsViewModel.getDisplayName(playlist.name), + localizedPlaylistName(playlist.name, context.l10n), style: Theme.of(context).textTheme.titleMedium, overflow: TextOverflow.ellipsis, ), @@ -59,7 +58,7 @@ class PlaylistWorksView extends StatelessWidget { totalPages: viewModel.totalPages, onPageChanged: (page) => viewModel.loadWorks(page: page), layoutStrategy: _layoutStrategy, - emptyMessage: '暂无作品', + emptyMessage: context.l10n.emptyWorks, ), ), ], @@ -68,4 +67,4 @@ class PlaylistWorksView extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/screens/contents/playlists/playlists_list_view.dart b/lib/screens/contents/playlists/playlists_list_view.dart index afe586f..7c03d78 100644 --- a/lib/screens/contents/playlists/playlists_list_view.dart +++ b/lib/screens/contents/playlists/playlists_list_view.dart @@ -3,6 +3,8 @@ import 'package:provider/provider.dart'; import 'package:asmrapp/presentation/viewmodels/playlists_viewmodel.dart'; import 'package:asmrapp/data/models/my_lists/my_playlists/playlist.dart'; import 'package:asmrapp/widgets/pagination_controls.dart'; +import 'package:asmrapp/l10n/l10n.dart'; +import 'package:asmrapp/common/utils/playlist_localizations.dart'; class PlaylistsListView extends StatelessWidget { final Function(Playlist) onPlaylistSelected; @@ -29,7 +31,7 @@ class PlaylistsListView extends StatelessWidget { const SizedBox(height: 16), ElevatedButton( onPressed: viewModel.refresh, - child: const Text('重试'), + child: Text(context.l10n.retry), ), ], ), @@ -47,8 +49,13 @@ class PlaylistsListView extends StatelessWidget { final playlist = viewModel.playlists[index]; return ListTile( leading: const Icon(Icons.playlist_play), - title: Text(viewModel.getDisplayName(playlist.name)), - subtitle: Text('${playlist.worksCount ?? 0} 个作品'), + title: Text( + localizedPlaylistName(playlist.name, context.l10n), + ), + subtitle: Text( + context.l10n + .playlistWorksCount(playlist.worksCount ?? 0), + ), onTap: () => onPlaylistSelected(playlist), ); }, @@ -67,4 +74,4 @@ class PlaylistsListView extends StatelessWidget { }, ); } -} \ No newline at end of file +} diff --git a/lib/screens/contents/playlists_content.dart b/lib/screens/contents/playlists_content.dart index 05ec9ae..6335be7 100644 --- a/lib/screens/contents/playlists_content.dart +++ b/lib/screens/contents/playlists_content.dart @@ -1,9 +1,7 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:asmrapp/data/models/my_lists/my_playlists/playlist.dart'; -import 'package:asmrapp/screens/contents/playlists/playlists_list_view.dart'; import 'package:asmrapp/screens/contents/playlists/playlist_works_view.dart'; -import 'package:asmrapp/presentation/viewmodels/playlists_viewmodel.dart'; +import 'package:asmrapp/screens/contents/playlists/playlists_list_view.dart'; +import 'package:flutter/material.dart'; class PlaylistsContent extends StatefulWidget { const PlaylistsContent({super.key}); @@ -12,7 +10,8 @@ class PlaylistsContent extends StatefulWidget { State createState() => _PlaylistsContentState(); } -class _PlaylistsContentState extends State with AutomaticKeepAliveClientMixin { +class _PlaylistsContentState extends State + with AutomaticKeepAliveClientMixin { Playlist? _selectedPlaylist; @override @@ -30,18 +29,10 @@ class _PlaylistsContentState extends State with AutomaticKeepA }); } - Future _onWillPop() async { - if (_selectedPlaylist != null) { - _handleBack(); - return false; - } - return true; - } - @override Widget build(BuildContext context) { super.build(context); - + return PopScope( canPop: _selectedPlaylist == null, onPopInvokedWithResult: (didPop, result) { @@ -59,4 +50,4 @@ class _PlaylistsContentState extends State with AutomaticKeepA ), ); } -} \ No newline at end of file +} diff --git a/lib/screens/contents/popular_content.dart b/lib/screens/contents/popular_content.dart index 4365f77..b1bb746 100644 --- a/lib/screens/contents/popular_content.dart +++ b/lib/screens/contents/popular_content.dart @@ -12,7 +12,8 @@ class PopularContent extends StatefulWidget { State createState() => _PopularContentState(); } -class _PopularContentState extends State with AutomaticKeepAliveClientMixin { +class _PopularContentState extends State + with AutomaticKeepAliveClientMixin { final _layoutStrategy = const WorkLayoutStrategy(); final _scrollController = ScrollController(); @@ -32,7 +33,8 @@ class _PopularContentState extends State with AutomaticKeepAlive } void _onScroll() { - if (_scrollController.position.pixels != _scrollController.position.minScrollExtent) { + if (_scrollController.position.pixels != + _scrollController.position.minScrollExtent) { final viewModel = context.read(); if (viewModel.filterPanelExpanded) { viewModel.closeFilterPanel(); @@ -80,4 +82,4 @@ class _PopularContentState extends State with AutomaticKeepAlive }, ); } -} \ No newline at end of file +} diff --git a/lib/screens/contents/recommend_content.dart b/lib/screens/contents/recommend_content.dart index ed5f1b9..307acfc 100644 --- a/lib/screens/contents/recommend_content.dart +++ b/lib/screens/contents/recommend_content.dart @@ -1,10 +1,10 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:asmrapp/presentation/viewmodels/recommend_viewmodel.dart'; import 'package:asmrapp/presentation/layouts/work_layout_strategy.dart'; -import 'package:asmrapp/widgets/work_grid/enhanced_work_grid_view.dart'; -import 'package:asmrapp/presentation/viewmodels/auth_viewmodel.dart'; +import 'package:asmrapp/presentation/viewmodels/recommend_viewmodel.dart'; import 'package:asmrapp/widgets/filter/filter_with_keyword.dart'; +import 'package:asmrapp/widgets/work_grid/enhanced_work_grid_view.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:asmrapp/l10n/l10n.dart'; class RecommendContent extends StatefulWidget { const RecommendContent({super.key}); @@ -13,7 +13,8 @@ class RecommendContent extends StatefulWidget { State createState() => _RecommendContentState(); } -class _RecommendContentState extends State with AutomaticKeepAliveClientMixin { +class _RecommendContentState extends State + with AutomaticKeepAliveClientMixin { final _layoutStrategy = const WorkLayoutStrategy(); final _scrollController = ScrollController(); @@ -21,7 +22,8 @@ class _RecommendContentState extends State with AutomaticKeepA bool get wantKeepAlive => true; void _onScroll() { - if (_scrollController.position.pixels != _scrollController.position.minScrollExtent) { + if (_scrollController.position.pixels != + _scrollController.position.minScrollExtent) { final viewModel = context.read(); if (viewModel.filterPanelExpanded) { viewModel.closeFilterPanel(); @@ -56,7 +58,9 @@ class _RecommendContentState extends State with AutomaticKeepA EnhancedWorkGridView( works: viewModel.works, isLoading: viewModel.isLoading, - error: viewModel.error, + error: viewModel.loginRequired + ? context.l10n.pleaseLogin + : viewModel.error, currentPage: viewModel.currentPage, totalPages: viewModel.totalPages, onPageChanged: (page) => viewModel.loadPage(page), @@ -85,4 +89,4 @@ class _RecommendContentState extends State with AutomaticKeepA }, ); } -} \ No newline at end of file +} diff --git a/lib/screens/detail_screen.dart b/lib/screens/detail_screen.dart index ede94ef..555670b 100644 --- a/lib/screens/detail_screen.dart +++ b/lib/screens/detail_screen.dart @@ -1,14 +1,15 @@ -import 'package:asmrapp/widgets/mini_player/mini_player.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:asmrapp/data/models/works/work.dart'; +import 'package:asmrapp/l10n/l10n.dart'; +import 'package:asmrapp/presentation/viewmodels/detail_viewmodel.dart'; +import 'package:asmrapp/screens/similar_works_screen.dart'; +import 'package:asmrapp/widgets/detail/work_action_buttons.dart'; import 'package:asmrapp/widgets/detail/work_cover.dart'; -import 'package:asmrapp/widgets/detail/work_info.dart'; import 'package:asmrapp/widgets/detail/work_files_list.dart'; import 'package:asmrapp/widgets/detail/work_files_skeleton.dart'; -import 'package:asmrapp/presentation/viewmodels/detail_viewmodel.dart'; -import 'package:asmrapp/widgets/detail/work_action_buttons.dart'; -import 'package:asmrapp/screens/similar_works_screen.dart'; +import 'package:asmrapp/widgets/detail/work_info.dart'; +import 'package:asmrapp/widgets/mini_player/mini_player.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class DetailScreen extends StatelessWidget { final Work work; @@ -31,7 +32,9 @@ class DetailScreen extends StatelessWidget { title: Text(work.sourceId ?? ''), ), body: SingleChildScrollView( - padding: const EdgeInsets.only(bottom: MiniPlayer.height), + padding: EdgeInsets.only( + bottom: MiniPlayer.heightWithSafeArea(context), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -52,7 +55,8 @@ class DetailScreen extends StatelessWidget { PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => SimilarWorksScreen(work: work), - transitionsBuilder: (context, animation, secondaryAnimation, child) { + transitionsBuilder: + (context, animation, secondaryAnimation, child) { const begin = Offset(1.0, 0.0); const end = Offset.zero; const curve = Curves.easeInOut; @@ -99,7 +103,11 @@ class DetailScreen extends StatelessWidget { } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('播放失败: $e')), + SnackBar( + content: Text( + _playbackErrorMessage(context, e), + ), + ), ); } } @@ -117,4 +125,20 @@ class DetailScreen extends StatelessWidget { ), ); } + + String _playbackErrorMessage(BuildContext context, Object error) { + if (error is PlaybackException) { + switch (error.error) { + case PlaybackError.unsupportedType: + return context.l10n.playUnsupportedFileType(error.detail ?? ''); + case PlaybackError.missingUrl: + return context.l10n.playUrlMissing; + case PlaybackError.filesNotLoaded: + return context.l10n.playFilesNotLoaded; + case PlaybackError.failed: + return context.l10n.playFailed(error.detail ?? ''); + } + } + return context.l10n.playFailed(error.toString()); + } } diff --git a/lib/screens/favorites_screen.dart b/lib/screens/favorites_screen.dart index 0d98662..83bfd1a 100644 --- a/lib/screens/favorites_screen.dart +++ b/lib/screens/favorites_screen.dart @@ -5,6 +5,7 @@ import 'package:asmrapp/presentation/viewmodels/favorites_viewmodel.dart'; import 'package:asmrapp/presentation/layouts/work_layout_strategy.dart'; import 'package:asmrapp/widgets/pagination_controls.dart'; import 'package:asmrapp/widgets/work_grid_view.dart'; +import 'package:asmrapp/l10n/l10n.dart'; class FavoritesScreen extends StatefulWidget { const FavoritesScreen({super.key}); @@ -48,7 +49,7 @@ class _FavoritesScreenState extends State { value: _viewModel, child: Scaffold( appBar: AppBar( - title: const Text('我的收藏'), + title: Text(context.l10n.favoritesTitle), ), drawer: const DrawerMenu(), body: Consumer( @@ -80,4 +81,4 @@ class _FavoritesScreenState extends State { ), ); } -} \ No newline at end of file +} diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index ed2078b..17fcfc1 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -1,17 +1,18 @@ -import 'package:asmrapp/screens/contents/playlists_content.dart'; -import 'package:flutter/material.dart'; -import 'package:asmrapp/widgets/mini_player/mini_player.dart'; -import 'package:asmrapp/widgets/drawer_menu.dart'; +import 'package:asmrapp/l10n/l10n.dart'; +import 'package:asmrapp/presentation/viewmodels/auth_viewmodel.dart'; +import 'package:asmrapp/presentation/viewmodels/home_viewmodel.dart'; +import 'package:asmrapp/presentation/viewmodels/playlists_viewmodel.dart'; +import 'package:asmrapp/presentation/viewmodels/popular_viewmodel.dart'; +import 'package:asmrapp/presentation/viewmodels/recommend_viewmodel.dart'; import 'package:asmrapp/screens/contents/home_content.dart'; -import 'package:asmrapp/screens/contents/recommend_content.dart'; +import 'package:asmrapp/screens/contents/playlists_content.dart'; import 'package:asmrapp/screens/contents/popular_content.dart'; +import 'package:asmrapp/screens/contents/recommend_content.dart'; import 'package:asmrapp/screens/search_screen.dart'; +import 'package:asmrapp/widgets/drawer_menu.dart'; +import 'package:asmrapp/widgets/mini_player/mini_player.dart'; +import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:asmrapp/presentation/viewmodels/home_viewmodel.dart'; -import 'package:asmrapp/presentation/viewmodels/popular_viewmodel.dart'; -import 'package:asmrapp/presentation/viewmodels/recommend_viewmodel.dart'; -import 'package:asmrapp/presentation/viewmodels/auth_viewmodel.dart'; -import 'package:asmrapp/presentation/viewmodels/playlists_viewmodel.dart'; /// MainScreen 是应用的主界面,负责管理底部导航栏和对应的内容页面。 /// 它采用了集中式的状态管理架构,所有子页面的 ViewModel 都在这里初始化和提供。 @@ -38,8 +39,6 @@ class _MainScreenState extends State { late final RecommendViewModel _recommendViewModel; late final PlaylistsViewModel _playlistsViewModel; - final _titles = const ['收藏', '主页', '为你推荐', '热门作品']; - // 页面内容列表 // 注意:这些页面不应该创建自己的 ViewModel 实例 // 而是应该通过 Provider.of 或 context.read 获取 MainScreen 提供的实例 @@ -110,10 +109,18 @@ class _MainScreenState extends State { ? context.watch().pagination?.totalCount : null; + final titles = [ + context.l10n.navigationFavorites, + context.l10n.navigationHome, + context.l10n.navigationForYou, + context.l10n.navigationPopularWorks, + ]; + final baseTitle = titles[_currentIndex]; + // 构建标题文本 final title = totalCount != null - ? '${_titles[_currentIndex]} (${totalCount})' - : _titles[_currentIndex]; + ? context.l10n.titleWithCount(baseTitle, totalCount) + : baseTitle; return Scaffold( appBar: AppBar( @@ -154,7 +161,7 @@ class _MainScreenState extends State { bottomNavigationBar: Column( mainAxisSize: MainAxisSize.min, children: [ - const MiniPlayer(), + const MiniPlayer(respectSafeArea: false), NavigationBar( height: 60, labelBehavior: NavigationDestinationLabelBehavior.alwaysHide, @@ -162,26 +169,26 @@ class _MainScreenState extends State { elevation: 0, selectedIndex: _currentIndex, onDestinationSelected: _onTabTapped, - destinations: const [ + destinations: [ NavigationDestination( - icon: Icon(Icons.favorite_outline), - selectedIcon: Icon(Icons.favorite), - label: '收藏', + icon: const Icon(Icons.favorite_outline), + selectedIcon: const Icon(Icons.favorite), + label: context.l10n.navigationFavorites, ), NavigationDestination( - icon: Icon(Icons.home_outlined), - selectedIcon: Icon(Icons.home), - label: '主页', + icon: const Icon(Icons.home_outlined), + selectedIcon: const Icon(Icons.home), + label: context.l10n.navigationHome, ), NavigationDestination( - icon: Icon(Icons.recommend_outlined), - selectedIcon: Icon(Icons.recommend), - label: '推荐', + icon: const Icon(Icons.recommend_outlined), + selectedIcon: const Icon(Icons.recommend), + label: context.l10n.navigationRecommend, ), NavigationDestination( - icon: Icon(Icons.trending_up_outlined), - selectedIcon: Icon(Icons.trending_up), - label: '热门', + icon: const Icon(Icons.trending_up_outlined), + selectedIcon: const Icon(Icons.trending_up), + label: context.l10n.navigationPopularWorks, ), ], ), @@ -192,4 +199,4 @@ class _MainScreenState extends State { ), ); } -} \ No newline at end of file +} diff --git a/lib/screens/player_screen.dart b/lib/screens/player_screen.dart index 472c412..630fa57 100644 --- a/lib/screens/player_screen.dart +++ b/lib/screens/player_screen.dart @@ -1,14 +1,15 @@ import 'package:asmrapp/core/platform/lyric_overlay_manager.dart'; -import 'package:flutter/material.dart'; -import 'package:get_it/get_it.dart'; +import 'package:asmrapp/core/platform/wakelock_controller.dart'; import 'package:asmrapp/presentation/viewmodels/player_viewmodel.dart'; -import 'package:asmrapp/widgets/player/player_controls.dart'; -import 'package:asmrapp/widgets/player/player_progress.dart'; -import 'package:asmrapp/widgets/player/player_cover.dart'; import 'package:asmrapp/screens/detail_screen.dart'; import 'package:asmrapp/widgets/lyrics/components/player_lyric_view.dart'; +import 'package:asmrapp/widgets/player/player_controls.dart'; +import 'package:asmrapp/widgets/player/player_cover.dart'; +import 'package:asmrapp/widgets/player/player_progress.dart'; import 'package:asmrapp/widgets/player/player_work_info.dart'; -import 'package:asmrapp/core/platform/wakelock_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:asmrapp/l10n/l10n.dart'; class PlayerScreen extends StatefulWidget { const PlayerScreen({super.key}); @@ -35,7 +36,7 @@ class _PlayerScreenState extends State { switchOutCurve: Curves.easeInQuart, transitionBuilder: (Widget child, Animation animation) { final isLyrics = (child as dynamic).key == const ValueKey('lyrics'); - + return FadeTransition( opacity: animation, child: SlideTransition( @@ -102,8 +103,12 @@ class _PlayerScreenState extends State { child: Material( color: Colors.transparent, child: Text( - _viewModel.currentTrackInfo?.title ?? '未在播放', - style: Theme.of(context).textTheme.titleLarge?.copyWith( + _viewModel.currentTrackInfo?.title ?? + context.l10n.noPlaying, + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith( fontWeight: FontWeight.w600, ), textAlign: TextAlign.center, @@ -114,11 +119,14 @@ class _PlayerScreenState extends State { if (_viewModel.currentTrackInfo?.artist != null) Text( _viewModel.currentTrackInfo!.artist, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith( color: Theme.of(context) .colorScheme .onSurface - .withOpacity(0.7), + .withValues(alpha: 0.7), ), textAlign: TextAlign.center, ), @@ -175,11 +183,13 @@ class _PlayerScreenState extends State { builder: (context, _) { return IconButton( icon: Icon( - wakeLockController.enabled - ? Icons.lightbulb - : Icons.lightbulb_outline, + wakeLockController.enabled + ? Icons.lightbulb + : Icons.lightbulb_outline, ), - tooltip: wakeLockController.enabled ? '关闭屏幕常亮' : '开启屏幕常亮', + tooltip: wakeLockController.enabled + ? context.l10n.screenOnDisable + : context.l10n.screenOnEnable, onPressed: () => wakeLockController.toggle(), ); }, @@ -206,8 +216,8 @@ class _PlayerScreenState extends State { ), Container( padding: const EdgeInsets.fromLTRB(12, 0, 12, 32), - child: Column( - children: const [ + child: const Column( + children: [ PlayerProgress(), SizedBox(height: 8), SizedBox(height: 8), diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index eb3c1b8..6c5c628 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -1,10 +1,11 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:asmrapp/presentation/viewmodels/search_viewmodel.dart'; -import 'package:asmrapp/widgets/work_grid_view.dart'; +import 'package:asmrapp/l10n/l10n.dart'; import 'package:asmrapp/presentation/layouts/work_layout_strategy.dart'; +import 'package:asmrapp/presentation/viewmodels/search_viewmodel.dart'; import 'package:asmrapp/utils/logger.dart'; import 'package:asmrapp/widgets/pagination_controls.dart'; +import 'package:asmrapp/widgets/work_grid_view.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class SearchScreen extends StatelessWidget { final String? initialKeyword; @@ -44,7 +45,7 @@ class _SearchScreenContentState extends State { void initState() { super.initState(); _searchController = TextEditingController(text: widget.initialKeyword); - + // 如果有初始关键词,自动执行搜索 if (widget.initialKeyword?.isNotEmpty == true) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -80,26 +81,33 @@ class _SearchScreenContentState extends State { } } - String _getOrderText(String order, String sort) { + String _getOrderText(BuildContext context, String order, String sort) { + final l10n = context.l10n; switch (order) { case 'create_date': - return sort == 'desc' ? '最新收录' : '最早收录'; + return sort == 'desc' ? l10n.searchOrderNewest : l10n.searchOrderOldest; case 'release': - return sort == 'desc' ? '发售日期倒序' : '发售日期顺序'; + return sort == 'desc' + ? l10n.searchOrderReleaseDesc + : l10n.searchOrderReleaseAsc; case 'dl_count': - return sort == 'desc' ? '销量倒序' : '销量顺序'; + return sort == 'desc' + ? l10n.searchOrderSalesDesc + : l10n.searchOrderSalesAsc; case 'price': - return sort == 'desc' ? '价格倒序' : '价格顺序'; + return sort == 'desc' + ? l10n.searchOrderPriceDesc + : l10n.searchOrderPriceAsc; case 'rate_average_2dp': - return '评价倒序'; + return l10n.searchOrderRatingDesc; case 'review_count': - return '评论数量倒序'; + return l10n.searchOrderReviewCountDesc; case 'id': - return sort == 'desc' ? 'RJ号倒序' : 'RJ号顺序'; + return sort == 'desc' ? l10n.searchOrderIdDesc : l10n.searchOrderIdAsc; case 'random': - return '随机排序'; + return l10n.searchOrderRandom; default: - return '排序'; + return l10n.orderLabel; } } @@ -121,12 +129,12 @@ class _SearchScreenContentState extends State { child: TextField( controller: _searchController, decoration: InputDecoration( - hintText: '搜索...', + hintText: context.l10n.searchHint, filled: true, fillColor: Theme.of(context) .colorScheme .surfaceContainerHighest - .withOpacity(0.5), + .withValues(alpha: 0.5), border: OutlineInputBorder( borderRadius: BorderRadius.circular(24), borderSide: BorderSide.none, @@ -158,7 +166,7 @@ class _SearchScreenContentState extends State { // 字幕选项 Consumer( builder: (context, viewModel, _) => FilterChip( - label: const Text('字幕'), + label: Text(context.l10n.subtitle), selected: viewModel.hasSubtitle, onSelected: (_) => viewModel.toggleSubtitle(), showCheckmark: true, @@ -170,56 +178,57 @@ class _SearchScreenContentState extends State { builder: (context, viewModel, _) => PopupMenuButton<(String, String)>( child: Chip( - label: Text( - _getOrderText(viewModel.order, viewModel.sort)), + label: Text(_getOrderText( + context, viewModel.order, viewModel.sort)), deleteIcon: const Icon(Icons.arrow_drop_down, size: 18), onDeleted: null, ), itemBuilder: (context) => [ - const PopupMenuItem( + PopupMenuItem( value: ('create_date', 'desc'), - child: Text('最新收录'), + child: Text(context.l10n.searchOrderNewest), ), - const PopupMenuItem( + PopupMenuItem( value: ('release', 'desc'), - child: Text('发售日期倒序'), + child: Text(context.l10n.searchOrderReleaseDesc), ), - const PopupMenuItem( + PopupMenuItem( value: ('release', 'asc'), - child: Text('发售日期顺序'), + child: Text(context.l10n.searchOrderReleaseAsc), ), - const PopupMenuItem( + PopupMenuItem( value: ('dl_count', 'desc'), - child: Text('销量倒序'), + child: Text(context.l10n.searchOrderSalesDesc), ), - const PopupMenuItem( + PopupMenuItem( value: ('price', 'asc'), - child: Text('价格顺序'), + child: Text(context.l10n.searchOrderPriceAsc), ), - const PopupMenuItem( + PopupMenuItem( value: ('price', 'desc'), - child: Text('价格倒序'), + child: Text(context.l10n.searchOrderPriceDesc), ), - const PopupMenuItem( + PopupMenuItem( value: ('rate_average_2dp', 'desc'), - child: Text('评价倒序'), + child: Text(context.l10n.searchOrderRatingDesc), ), - const PopupMenuItem( + PopupMenuItem( value: ('review_count', 'desc'), - child: Text('评论数量倒序'), + child: + Text(context.l10n.searchOrderReviewCountDesc), ), - const PopupMenuItem( + PopupMenuItem( value: ('id', 'desc'), - child: Text('RJ号倒序'), + child: Text(context.l10n.searchOrderIdDesc), ), - const PopupMenuItem( + PopupMenuItem( value: ('id', 'asc'), - child: Text('RJ号顺序'), + child: Text(context.l10n.searchOrderIdAsc), ), - const PopupMenuItem( + PopupMenuItem( value: ('random', 'desc'), - child: Text('随机排序'), + child: Text(context.l10n.searchOrderRandom), ), ], onSelected: (value) => @@ -237,12 +246,12 @@ class _SearchScreenContentState extends State { builder: (context, viewModel, child) { Widget? emptyWidget; if (viewModel.works.isEmpty && viewModel.keyword.isEmpty) { - emptyWidget = const Center( - child: Text('输入关键词开始搜索'), + emptyWidget = Center( + child: Text(context.l10n.searchPromptInitial), ); } else if (viewModel.works.isEmpty) { - emptyWidget = const Center( - child: Text('没有找到相关结果'), + emptyWidget = Center( + child: Text(context.l10n.searchNoResults), ); } diff --git a/lib/screens/settings/cache_manager_screen.dart b/lib/screens/settings/cache_manager_screen.dart index 7054a73..5081ba8 100644 --- a/lib/screens/settings/cache_manager_screen.dart +++ b/lib/screens/settings/cache_manager_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:asmrapp/presentation/viewmodels/settings/cache_manager_viewmodel.dart'; +import 'package:asmrapp/l10n/l10n.dart'; class CacheManagerScreen extends StatelessWidget { const CacheManagerScreen({super.key}); @@ -11,7 +12,7 @@ class CacheManagerScreen extends StatelessWidget { create: (_) => CacheManagerViewModel()..loadCacheSize(), child: Scaffold( appBar: AppBar( - title: const Text('缓存管理'), + title: Text(context.l10n.cacheManagerTitle), ), body: Consumer( builder: (context, viewModel, _) { @@ -19,10 +20,15 @@ class CacheManagerScreen extends StatelessWidget { return const Center(child: CircularProgressIndicator()); } - if (viewModel.error != null) { + if (viewModel.errorDetail != null) { + final message = switch (viewModel.lastFailedOperation) { + CacheOperation.load => + context.l10n.cacheLoadFailed(viewModel.errorDetail!), + _ => context.l10n.cacheClearFailed(viewModel.errorDetail!), + }; return Center( child: Text( - viewModel.error!, + message, style: TextStyle(color: Theme.of(context).colorScheme.error), ), ); @@ -32,50 +38,47 @@ class CacheManagerScreen extends StatelessWidget { children: [ // 音频缓存 ListTile( - title: const Text('音频缓存'), + title: Text(context.l10n.cacheAudio), subtitle: Text(viewModel.audioCacheSizeFormatted), trailing: TextButton( - onPressed: viewModel.isLoading - ? null - : () => viewModel.clearAudioCache(), - child: const Text('清理'), + onPressed: viewModel.isLoading + ? null + : () => viewModel.clearAudioCache(), + child: Text(context.l10n.cacheClear), ), ), const Divider(), - + // 字幕缓存 ListTile( - title: const Text('字幕缓存'), + title: Text(context.l10n.cacheSubtitle), subtitle: Text(viewModel.subtitleCacheSizeFormatted), trailing: TextButton( - onPressed: viewModel.isLoading - ? null - : () => viewModel.clearSubtitleCache(), - child: const Text('清理'), + onPressed: viewModel.isLoading + ? null + : () => viewModel.clearSubtitleCache(), + child: Text(context.l10n.cacheClear), ), ), const Divider(), - + // 总缓存大小 ListTile( - title: const Text('总缓存大小'), + title: Text(context.l10n.cacheTotal), subtitle: Text(viewModel.totalCacheSizeFormatted), trailing: TextButton( - onPressed: viewModel.isLoading - ? null - : () => viewModel.clearAllCache(), - child: const Text('清理全部'), + onPressed: viewModel.isLoading + ? null + : () => viewModel.clearAllCache(), + child: Text(context.l10n.cacheClearAll), ), ), const Divider(), - + // 缓存说明 - const ListTile( - title: Text('缓存说明'), - subtitle: Text( - '缓存用于存储最近播放的音频文件和字幕文件,以提高再次播放时的加载速度。' - '系统会自动清理过期和超量的缓存。' - ), + ListTile( + title: Text(context.l10n.cacheInfoTitle), + subtitle: Text(context.l10n.cacheDescription), ), ], ); @@ -84,4 +87,4 @@ class CacheManagerScreen extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/screens/similar_works_screen.dart b/lib/screens/similar_works_screen.dart index 91e8fa5..acd9006 100644 --- a/lib/screens/similar_works_screen.dart +++ b/lib/screens/similar_works_screen.dart @@ -6,6 +6,7 @@ import 'package:asmrapp/presentation/viewmodels/similar_works_viewmodel.dart'; import 'package:asmrapp/widgets/work_grid_view.dart'; import 'package:asmrapp/presentation/layouts/work_layout_strategy.dart'; import 'package:asmrapp/widgets/pagination_controls.dart'; +import 'package:asmrapp/l10n/l10n.dart'; class SimilarWorksScreen extends StatefulWidget { final Work work; @@ -39,7 +40,8 @@ class _SimilarWorksScreenState extends State { } void _onScroll() { - if (_scrollController.position.pixels != _scrollController.position.minScrollExtent) { + if (_scrollController.position.pixels != + _scrollController.position.minScrollExtent) { if (_viewModel.filterPanelExpanded) { _viewModel.closeFilterPanel(); } @@ -62,7 +64,7 @@ class _SimilarWorksScreenState extends State { value: _viewModel, child: Scaffold( appBar: AppBar( - title: const Text('相关推荐'), + title: Text(context.l10n.similarWorksTitle), actions: [ Consumer( builder: (context, viewModel, _) => IconButton( @@ -111,7 +113,8 @@ class _SimilarWorksScreenState extends State { offset: Offset(0, viewModel.filterPanelExpanded ? 0 : -1), child: FilterWithKeyword( hasSubtitle: viewModel.hasSubtitle, - onSubtitleChanged: (_) => viewModel.toggleSubtitleFilter(), + onSubtitleChanged: (_) => + viewModel.toggleSubtitleFilter(), ), ), ), @@ -122,4 +125,4 @@ class _SimilarWorksScreenState extends State { ), ); } -} \ No newline at end of file +} diff --git a/lib/utils/logger.dart b/lib/utils/logger.dart index 3e04275..4f1e609 100644 --- a/lib/utils/logger.dart +++ b/lib/utils/logger.dart @@ -8,7 +8,7 @@ class AppLogger { lineLength: 120, colors: true, printEmojis: true, - printTime: true, + dateTimeFormat: DateTimeFormat.dateAndTime, ), ); diff --git a/lib/widgets/common/tag_chip.dart b/lib/widgets/common/tag_chip.dart index 22c619b..db5221b 100644 --- a/lib/widgets/common/tag_chip.dart +++ b/lib/widgets/common/tag_chip.dart @@ -22,13 +22,15 @@ class TagChip extends StatelessWidget { child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: backgroundColor ?? Theme.of(context).colorScheme.surfaceContainerHighest, + color: backgroundColor ?? + Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), child: Text( text, style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: textColor ?? Theme.of(context).colorScheme.onSurfaceVariant, + color: + textColor ?? Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 13, ), ), diff --git a/lib/widgets/detail/mark_selection_dialog.dart b/lib/widgets/detail/mark_selection_dialog.dart index 4bed11b..d03246a 100644 --- a/lib/widgets/detail/mark_selection_dialog.dart +++ b/lib/widgets/detail/mark_selection_dialog.dart @@ -1,5 +1,7 @@ -import 'package:flutter/material.dart'; +import 'package:asmrapp/common/extensions/mark_status_localizations.dart'; import 'package:asmrapp/data/models/mark_status.dart'; +import 'package:asmrapp/l10n/l10n.dart'; +import 'package:flutter/material.dart'; class MarkSelectionDialog extends StatelessWidget { final MarkStatus? currentStatus; @@ -16,11 +18,11 @@ class MarkSelectionDialog extends StatelessWidget { @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; - + return AlertDialog( backgroundColor: isDark ? const Color(0xFF2C2C2C) : Colors.white, title: Text( - '标记状态', + context.l10n.markStatusTitle, style: TextStyle( color: isDark ? Colors.white70 : Colors.black87, ), @@ -34,24 +36,26 @@ class MarkSelectionDialog extends StatelessWidget { leading: Radio( value: status, groupValue: currentStatus, - onChanged: loading ? null : (MarkStatus? value) { - if (value != null) { - onMarkSelected(value); - Navigator.of(context).pop(); - } - }, - fillColor: MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.disabled)) { + onChanged: loading + ? null + : (MarkStatus? value) { + if (value != null) { + onMarkSelected(value); + Navigator.of(context).pop(); + } + }, + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.disabled)) { return isDark ? Colors.white24 : Colors.black26; } - if (states.contains(MaterialState.selected)) { + if (states.contains(WidgetState.selected)) { return isDark ? Colors.white70 : Colors.black87; } return isDark ? Colors.white38 : Colors.black45; }), ), title: Text( - status.label, + status.localizedLabel(context.l10n), style: TextStyle( color: loading ? (isDark ? Colors.white38 : Colors.black38) @@ -60,13 +64,15 @@ class MarkSelectionDialog extends StatelessWidget { : (isDark ? Colors.white70 : Colors.black54)), ), ), - onTap: loading ? null : () { - onMarkSelected(status); - Navigator.of(context).pop(); - }, - hoverColor: isDark - ? Colors.white.withOpacity(0.05) - : Colors.black.withOpacity(0.05), + onTap: loading + ? null + : () { + onMarkSelected(status); + Navigator.of(context).pop(); + }, + hoverColor: isDark + ? Colors.white.withValues(alpha: 0.05) + : Colors.black.withValues(alpha: 0.05), ); }).toList(), ), @@ -75,4 +81,4 @@ class MarkSelectionDialog extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/widgets/detail/playlist_selection_dialog.dart b/lib/widgets/detail/playlist_selection_dialog.dart index e674011..6c7a155 100644 --- a/lib/widgets/detail/playlist_selection_dialog.dart +++ b/lib/widgets/detail/playlist_selection_dialog.dart @@ -1,5 +1,7 @@ -import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/playlists_with_exist_statu/playlist.dart'; +import 'package:flutter/material.dart'; +import 'package:asmrapp/l10n/l10n.dart'; +import 'package:asmrapp/common/utils/playlist_localizations.dart'; class PlaylistSelectionDialog extends StatefulWidget { final List? playlists; @@ -18,7 +20,8 @@ class PlaylistSelectionDialog extends StatefulWidget { }); @override - State createState() => _PlaylistSelectionDialogState(); + State createState() => + _PlaylistSelectionDialogState(); } class _PlaylistSelectionDialogState extends State { @@ -40,7 +43,7 @@ class _PlaylistSelectionDialogState extends State { void _updateItemStates() { if (widget.playlists == null) return; - + final newStates = {}; for (final playlist in widget.playlists!) { newStates[playlist.id!] = _PlaylistItemState( @@ -67,7 +70,7 @@ class _PlaylistSelectionDialogState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '添加到收藏夹', + context.l10n.playlistAddToFavorites, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 16), @@ -98,7 +101,7 @@ class _PlaylistSelectionDialogState extends State { const SizedBox(height: 8), ElevatedButton( onPressed: widget.onRetry, - child: const Text('重试'), + child: Text(context.l10n.retry), ), ], ], @@ -107,8 +110,8 @@ class _PlaylistSelectionDialogState extends State { } if (widget.playlists == null || widget.playlists!.isEmpty) { - return const Center( - child: Text('暂无收藏夹'), + return Center( + child: Text(context.l10n.playlistEmpty), ); } @@ -135,33 +138,39 @@ class _PlaylistSelectionDialogState extends State { try { await widget.onPlaylistTap!(state.playlist); - + if (mounted) { final newPlaylist = state.playlist.copyWith( exist: !(state.playlist.exist ?? false), ); - + _itemStates[state.playlist.id!] = _PlaylistItemState( playlist: newPlaylist, isLoading: false, ); + final playlistName = + localizedPlaylistName(newPlaylist.name, context.l10n); + final message = newPlaylist.exist! + ? context.l10n.playlistAddSuccess(playlistName) + : context.l10n.playlistRemoveSuccess(playlistName); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - '${newPlaylist.exist! ? '添加成功' : '移除成功'}: ${_getDisplayName(newPlaylist.name)}', + message, style: TextStyle( color: Theme.of(context).colorScheme.onSurface, ), ), duration: const Duration(seconds: 1), - backgroundColor: Theme.of(context).colorScheme.surfaceVariant, + backgroundColor: + Theme.of(context).colorScheme.surfaceContainerHighest, showCloseIcon: true, closeIconColor: Theme.of(context).colorScheme.onSurface, behavior: SnackBarBehavior.floating, ), ); - + setState(() {}); } } finally { @@ -172,17 +181,6 @@ class _PlaylistSelectionDialogState extends State { } } } - - String _getDisplayName(String? name) { - switch (name) { - case '__SYS_PLAYLIST_MARKED': - return '我标记的'; - case '__SYS_PLAYLIST_LIKED': - return '我喜欢的'; - default: - return name ?? ''; - } - } } class _PlaylistItem extends StatelessWidget { @@ -194,22 +192,15 @@ class _PlaylistItem extends StatelessWidget { this.onTap, }); - String _getDisplayName(String? name) { - switch (name) { - case '__SYS_PLAYLIST_MARKED': - return '我标记的'; - case '__SYS_PLAYLIST_LIKED': - return '我喜欢的'; - default: - return name ?? ''; - } - } - @override Widget build(BuildContext context) { return ListTile( - title: Text(_getDisplayName(state.playlist.name)), - subtitle: Text('${state.playlist.worksCount ?? 0} 个作品'), + title: Text( + localizedPlaylistName(state.playlist.name, context.l10n), + ), + subtitle: Text( + context.l10n.playlistWorksCount(state.playlist.worksCount ?? 0), + ), trailing: state.isLoading ? const SizedBox( width: 24, @@ -230,9 +221,9 @@ class _PlaylistItem extends StatelessWidget { class _PlaylistItemState { final Playlist playlist; bool isLoading; - + _PlaylistItemState({ required this.playlist, this.isLoading = false, }); -} \ No newline at end of file +} diff --git a/lib/widgets/detail/work_action_buttons.dart b/lib/widgets/detail/work_action_buttons.dart index 3396250..0802334 100644 --- a/lib/widgets/detail/work_action_buttons.dart +++ b/lib/widgets/detail/work_action_buttons.dart @@ -1,5 +1,7 @@ import 'package:asmrapp/data/models/mark_status.dart'; import 'package:flutter/material.dart'; +import 'package:asmrapp/l10n/l10n.dart'; +import 'package:asmrapp/common/extensions/mark_status_localizations.dart'; class WorkActionButtons extends StatelessWidget { final VoidCallback onRecommendationsTap; @@ -32,26 +34,31 @@ class WorkActionButtons extends StatelessWidget { children: [ _ActionButton( icon: Icons.favorite_border, - label: '收藏', + label: context.l10n.workActionFavorite, onTap: onFavoriteTap, loading: loadingFavorite, ), _ActionButton( icon: Icons.bookmark_border, - label: currentMarkStatus?.label ?? '标记', + label: currentMarkStatus?.localizedLabel(context.l10n) ?? + context.l10n.workActionMark, onTap: onMarkTap, loading: loadingMark, ), _ActionButton( icon: Icons.star_border, - label: '评分', + label: context.l10n.workActionRate, onTap: () { // TODO: 实现评分功能 }, ), _ActionButton( icon: Icons.recommend, - label: checkingRecommendations ? '检查中' : (hasRecommendations ? '相关推荐' : '暂无推荐'), + label: checkingRecommendations + ? context.l10n.workActionChecking + : (hasRecommendations + ? context.l10n.workActionRecommend + : context.l10n.workActionNoRecommendation), onTap: hasRecommendations ? onRecommendationsTap : null, loading: checkingRecommendations, ), @@ -78,7 +85,7 @@ class _ActionButton extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final disabled = onTap == null && !loading; - + return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8), @@ -99,8 +106,8 @@ class _ActionButton extends StatelessWidget { else Icon( icon, - color: disabled - ? theme.colorScheme.onSurface.withOpacity(0.38) + color: disabled + ? theme.colorScheme.onSurface.withValues(alpha: 0.38) : null, ), const SizedBox(height: 4), @@ -108,7 +115,7 @@ class _ActionButton extends StatelessWidget { label, style: theme.textTheme.bodySmall?.copyWith( color: disabled - ? theme.colorScheme.onSurface.withOpacity(0.38) + ? theme.colorScheme.onSurface.withValues(alpha: 0.38) : null, ), ), @@ -117,4 +124,4 @@ class _ActionButton extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/widgets/detail/work_cover.dart b/lib/widgets/detail/work_cover.dart index 795bde2..de4ca14 100644 --- a/lib/widgets/detail/work_cover.dart +++ b/lib/widgets/detail/work_cover.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; class WorkCover extends StatelessWidget { final String imageUrl; @@ -8,7 +8,6 @@ class WorkCover extends StatelessWidget { final String? releaseDate; final String? heroTag; - const WorkCover({ super.key, required this.imageUrl, @@ -37,7 +36,7 @@ class WorkCover extends StatelessWidget { child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: Colors.black.withOpacity(0.7), + color: Colors.black.withValues(alpha: .7), borderRadius: BorderRadius.circular(4), ), child: Text( @@ -56,7 +55,7 @@ class WorkCover extends StatelessWidget { child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: Colors.black.withOpacity(0.7), + color: Colors.black.withValues(alpha: 0.7), borderRadius: BorderRadius.circular(4), ), child: Text( diff --git a/lib/widgets/detail/work_file_item.dart b/lib/widgets/detail/work_file_item.dart index 685bf2b..e085fc8 100644 --- a/lib/widgets/detail/work_file_item.dart +++ b/lib/widgets/detail/work_file_item.dart @@ -19,7 +19,7 @@ class WorkFileItem extends StatelessWidget { Widget build(BuildContext context) { final bool isAudio = file.type?.toLowerCase() == 'audio'; final colorScheme = Theme.of(context).colorScheme; - + return Padding( padding: EdgeInsets.only(left: indentation), child: ListTile( @@ -40,10 +40,12 @@ class WorkFileItem extends StatelessWidget { color: isAudio ? Colors.green : Colors.blue, ), dense: true, - onTap: isAudio ? () { - AppLogger.debug('点击音频文件: ${file.title}'); - onFileTap?.call(file); - } : null, + onTap: isAudio + ? () { + AppLogger.debug('点击音频文件: ${file.title}'); + onFileTap?.call(file); + } + : null, ), ); } diff --git a/lib/widgets/detail/work_files_list.dart b/lib/widgets/detail/work_files_list.dart index 3411981..5e900f5 100644 --- a/lib/widgets/detail/work_files_list.dart +++ b/lib/widgets/detail/work_files_list.dart @@ -1,8 +1,9 @@ -import 'package:flutter/material.dart'; -import 'package:asmrapp/data/models/files/files.dart'; import 'package:asmrapp/data/models/files/child.dart'; -import 'package:asmrapp/widgets/detail/work_folder_item.dart'; +import 'package:asmrapp/data/models/files/files.dart'; import 'package:asmrapp/widgets/detail/work_file_item.dart'; +import 'package:asmrapp/widgets/detail/work_folder_item.dart'; +import 'package:flutter/material.dart'; +import 'package:asmrapp/l10n/l10n.dart'; class WorkFilesList extends StatelessWidget { final Files files; @@ -18,7 +19,7 @@ class WorkFilesList extends StatelessWidget { Widget build(BuildContext context) { // 重置文件夹展开状态 WorkFolderItem.resetExpandState(); - + return Card( margin: const EdgeInsets.all(8), child: Column( @@ -27,7 +28,7 @@ class WorkFilesList extends StatelessWidget { Padding( padding: const EdgeInsets.all(16), child: Text( - '文件列表', + context.l10n.workFilesTitle, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), @@ -35,7 +36,7 @@ class WorkFilesList extends StatelessWidget { ), Divider( height: 1, - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, ), ...files.children ?.map((child) => child.type == 'folder' diff --git a/lib/widgets/detail/work_folder_item.dart b/lib/widgets/detail/work_folder_item.dart index bf02e91..060f120 100644 --- a/lib/widgets/detail/work_folder_item.dart +++ b/lib/widgets/detail/work_folder_item.dart @@ -30,9 +30,9 @@ class WorkFolderItem extends StatelessWidget { bool _shouldExpandFolder(Child folder) { // 如果还没有找到第一个音频文件夹,就搜索并记录 _audioFolderPath ??= FilePath.findFirstAudioFolderPath( - [folder], - formats: _audioFormats, - ); + [folder], + formats: _audioFormats, + ); // 判断当前文件夹是否在音频文件夹的路径上 return FilePath.isInPath(_audioFolderPath, folder.title); @@ -50,9 +50,9 @@ class WorkFolderItem extends StatelessWidget { dividerColor: Colors.transparent, // 确保子组件也能继承正确的文字颜色 textTheme: Theme.of(context).textTheme.apply( - bodyColor: colorScheme.onSurface, - displayColor: colorScheme.onSurface, - ), + bodyColor: colorScheme.onSurface, + displayColor: colorScheme.onSurface, + ), ), child: ExpansionTile( title: Text( diff --git a/lib/widgets/detail/work_info_header.dart b/lib/widgets/detail/work_info_header.dart index 198e09e..72a7153 100644 --- a/lib/widgets/detail/work_info_header.dart +++ b/lib/widgets/detail/work_info_header.dart @@ -1,8 +1,9 @@ -import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/works/work.dart'; +import 'package:asmrapp/utils/logger.dart'; import 'package:asmrapp/widgets/common/tag_chip.dart'; import 'package:asmrapp/widgets/detail/work_stats_info.dart'; -import 'package:asmrapp/utils/logger.dart'; +import 'package:flutter/material.dart'; +import 'package:asmrapp/l10n/l10n.dart'; class WorkInfoHeader extends StatelessWidget { final Work work; @@ -42,22 +43,22 @@ class WorkInfoHeader extends StatelessWidget { if (work.circle?.name != null) TagChip( text: work.circle?.name ?? '', - backgroundColor: Colors.orange.withOpacity(0.2), + backgroundColor: Colors.orange.withValues(alpha: 0.2), textColor: Colors.orange[700], onTap: () => _onTagTap(context, work.circle?.name ?? ''), ), ...?work.vas?.map( (va) => TagChip( text: va['name'] ?? '', - backgroundColor: Colors.green.withOpacity(0.2), + backgroundColor: Colors.green.withValues(alpha: 0.2), textColor: Colors.green[700], onTap: () => _onTagTap(context, va['name'] ?? ''), ), ), if (work.hasSubtitle == true) TagChip( - text: '字幕', - backgroundColor: Colors.blue.withOpacity(0.2), + text: context.l10n.subtitleTag, + backgroundColor: Colors.blue.withValues(alpha: 0.2), textColor: Colors.blue[700], ), ], @@ -65,4 +66,4 @@ class WorkInfoHeader extends StatelessWidget { ], ); } -} \ No newline at end of file +} diff --git a/lib/widgets/detail/work_stats_info.dart b/lib/widgets/detail/work_stats_info.dart index c94bd36..a331fcc 100644 --- a/lib/widgets/detail/work_stats_info.dart +++ b/lib/widgets/detail/work_stats_info.dart @@ -65,4 +65,4 @@ class WorkStatsInfo extends StatelessWidget { ], ); } -} \ No newline at end of file +} diff --git a/lib/widgets/drawer_menu.dart b/lib/widgets/drawer_menu.dart index 4c99513..b56ce0e 100644 --- a/lib/widgets/drawer_menu.dart +++ b/lib/widgets/drawer_menu.dart @@ -1,13 +1,13 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:asmrapp/common/constants/strings.dart'; +import 'package:asmrapp/core/platform/wakelock_controller.dart'; +import 'package:asmrapp/core/theme/theme_controller.dart'; +import 'package:asmrapp/l10n/l10n.dart'; import 'package:asmrapp/presentation/viewmodels/auth_viewmodel.dart'; import 'package:asmrapp/presentation/widgets/auth/login_dialog.dart'; import 'package:asmrapp/screens/favorites_screen.dart'; import 'package:asmrapp/screens/settings/cache_manager_screen.dart'; -import 'package:asmrapp/core/theme/theme_controller.dart'; -import 'package:asmrapp/core/platform/wakelock_controller.dart'; +import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; +import 'package:provider/provider.dart'; class DrawerMenu extends StatelessWidget { const DrawerMenu({super.key}); @@ -32,13 +32,13 @@ class DrawerMenu extends StatelessWidget { data: Theme.of(context).copyWith( dividerTheme: const DividerThemeData(color: Colors.transparent), ), - child: const DrawerHeader( - decoration: BoxDecoration( + child: DrawerHeader( + decoration: const BoxDecoration( color: Colors.deepPurple, ), child: Text( - Strings.appName, - style: TextStyle( + context.l10n.appName, + style: const TextStyle( color: Colors.white, fontSize: 24, ), @@ -50,7 +50,9 @@ class DrawerMenu extends StatelessWidget { return ListTile( leading: const Icon(Icons.person), title: Text( - authVM.isLoggedIn ? authVM.username ?? '' : '登录', + authVM.isLoggedIn + ? authVM.username ?? '' + : context.l10n.login, ), onTap: () { Navigator.pop(context); @@ -63,10 +65,9 @@ class DrawerMenu extends StatelessWidget { ); }, ), - ListTile( leading: const Icon(Icons.favorite), - title: const Text(Strings.favorites), + title: Text(context.l10n.favoritesTitle), onTap: () { Navigator.pop(context); // 检查用户是否已登录 @@ -87,7 +88,7 @@ class DrawerMenu extends StatelessWidget { ), ListTile( leading: const Icon(Icons.settings), - title: const Text(Strings.settings), + title: Text(context.l10n.settings), onTap: () { Navigator.pop(context); // TODO: 导航到设置页面 @@ -95,7 +96,7 @@ class DrawerMenu extends StatelessWidget { ), ListTile( leading: const Icon(Icons.storage), - title: const Text('缓存管理'), + title: Text(context.l10n.cacheManager), onTap: () { Navigator.pop(context); Navigator.push( @@ -106,15 +107,17 @@ class DrawerMenu extends StatelessWidget { ); }, ), - Divider( - color: Theme.of(context).colorScheme.surfaceVariant, - height: 1, - ), + Divider( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + height: 1, + ), Consumer( builder: (context, themeController, _) { return ListTile( leading: Icon(_getThemeIcon(themeController.themeMode)), - title: Text(_getThemeText(themeController.themeMode)), + title: Text( + _getThemeText(context, themeController.themeMode), + ), onTap: () => themeController.toggleThemeMode(), ); }, @@ -124,7 +127,7 @@ class DrawerMenu extends StatelessWidget { builder: (context, _) { final controller = GetIt.I(); return SwitchListTile( - title: const Text('屏幕常亮'), + title: Text(context.l10n.screenAlwaysOn), value: controller.enabled, onChanged: (_) => controller.toggle(), ); @@ -147,14 +150,14 @@ class DrawerMenu extends StatelessWidget { } } - String _getThemeText(ThemeMode mode) { + String _getThemeText(BuildContext context, ThemeMode mode) { switch (mode) { case ThemeMode.system: - return '跟随系统主题'; + return context.l10n.themeSystem; case ThemeMode.light: - return '浅色模式'; + return context.l10n.themeLight; case ThemeMode.dark: - return '深色模式'; + return context.l10n.themeDark; } } } diff --git a/lib/widgets/filter/filter_panel.dart b/lib/widgets/filter/filter_panel.dart index e18eb19..fba731a 100644 --- a/lib/widgets/filter/filter_panel.dart +++ b/lib/widgets/filter/filter_panel.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:asmrapp/l10n/l10n.dart'; class FilterPanel extends StatelessWidget { final bool expanded; @@ -20,30 +21,30 @@ class FilterPanel extends StatelessWidget { required this.onSortDirectionChanged, }); - String _getOrderFieldText(String field) { + String _getOrderFieldText(BuildContext context, String field) { switch (field) { case 'create_date': - return '收录时间'; + return context.l10n.orderFieldCollectionTime; case 'release': - return '发售日期'; + return context.l10n.orderFieldReleaseDate; case 'dl_count': - return '销量'; + return context.l10n.orderFieldSales; case 'price': - return '价格'; + return context.l10n.orderFieldPrice; case 'rate_average_2dp': - return '评价'; + return context.l10n.orderFieldRating; case 'review_count': - return '评论数量'; + return context.l10n.orderFieldReviewCount; case 'id': - return 'RJ号'; + return context.l10n.orderFieldId; case 'rating': - return '我的评价'; + return context.l10n.orderFieldMyRating; case 'nsfw': - return '全年龄'; + return context.l10n.orderFieldAllAges; case 'random': - return '随机'; + return context.l10n.orderFieldRandom; default: - return '排序'; + return context.l10n.orderLabel; } } @@ -58,7 +59,10 @@ class FilterPanel extends StatelessWidget { Container( decoration: BoxDecoration( border: Border.all( - color: Theme.of(context).colorScheme.outline.withOpacity(0.5), + color: Theme.of(context) + .colorScheme + .outline + .withValues(alpha: 0.5), ), borderRadius: BorderRadius.circular(8), ), @@ -68,23 +72,26 @@ class FilterPanel extends StatelessWidget { onTap: () => onSubtitleChanged(!hasSubtitle), borderRadius: BorderRadius.circular(7), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( - hasSubtitle ? Icons.check_box : Icons.check_box_outline_blank, + hasSubtitle + ? Icons.check_box + : Icons.check_box_outline_blank, size: 20, - color: hasSubtitle - ? Theme.of(context).colorScheme.primary + color: hasSubtitle + ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurfaceVariant, ), const SizedBox(width: 8), Text( - '有字幕', + context.l10n.subtitleAvailable, style: TextStyle( - color: hasSubtitle - ? Theme.of(context).colorScheme.primary + color: hasSubtitle + ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface, ), ), @@ -99,33 +106,37 @@ class FilterPanel extends StatelessWidget { Container( decoration: BoxDecoration( border: Border.all( - color: Theme.of(context).colorScheme.outline.withOpacity(0.5), + color: Theme.of(context) + .colorScheme + .outline + .withValues(alpha: 0.5), ), borderRadius: BorderRadius.circular(8), ), child: PopupMenuButton( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( mainAxisSize: MainAxisSize.min, children: [ - Text(_getOrderFieldText(orderField)), + Text(_getOrderFieldText(context, orderField)), const SizedBox(width: 4), const Icon(Icons.arrow_drop_down, size: 20), ], ), ), itemBuilder: (context) => [ - _buildOrderMenuItem('收录时间', 'create_date'), - _buildOrderMenuItem('发售日期', 'release'), - _buildOrderMenuItem('销量', 'dl_count'), - _buildOrderMenuItem('价格', 'price'), - _buildOrderMenuItem('评价', 'rate_average_2dp'), - _buildOrderMenuItem('评论数量', 'review_count'), - _buildOrderMenuItem('RJ号', 'id'), - _buildOrderMenuItem('我的评价', 'rating'), - _buildOrderMenuItem('全年龄', 'nsfw'), - _buildOrderMenuItem('随机', 'random'), + _buildOrderMenuItem(context, 'create_date'), + _buildOrderMenuItem(context, 'release'), + _buildOrderMenuItem(context, 'dl_count'), + _buildOrderMenuItem(context, 'price'), + _buildOrderMenuItem(context, 'rate_average_2dp'), + _buildOrderMenuItem(context, 'review_count'), + _buildOrderMenuItem(context, 'id'), + _buildOrderMenuItem(context, 'rating'), + _buildOrderMenuItem(context, 'nsfw'), + _buildOrderMenuItem(context, 'random'), ], ), ), @@ -134,7 +145,10 @@ class FilterPanel extends StatelessWidget { Container( decoration: BoxDecoration( border: Border.all( - color: Theme.of(context).colorScheme.outline.withOpacity(0.5), + color: Theme.of(context) + .colorScheme + .outline + .withValues(alpha: 0.5), ), borderRadius: BorderRadius.circular(8), ), @@ -144,14 +158,19 @@ class FilterPanel extends StatelessWidget { onTap: () => onSortDirectionChanged(!isDescending), borderRadius: BorderRadius.circular(7), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( mainAxisSize: MainAxisSize.min, children: [ - Text(isDescending ? '降序' : '升序'), + Text(isDescending + ? context.l10n.orderDirectionDesc + : context.l10n.orderDirectionAsc), const SizedBox(width: 4), Icon( - isDescending ? Icons.arrow_downward : Icons.arrow_upward, + isDescending + ? Icons.arrow_downward + : Icons.arrow_upward, size: 20, color: Theme.of(context).colorScheme.onSurface, ), @@ -167,10 +186,13 @@ class FilterPanel extends StatelessWidget { ); } - PopupMenuItem _buildOrderMenuItem(String text, String value) { + PopupMenuItem _buildOrderMenuItem( + BuildContext context, + String value, + ) { return PopupMenuItem( value: value, - child: Text(text), + child: Text(_getOrderFieldText(context, value)), ); } -} \ No newline at end of file +} diff --git a/lib/widgets/filter/filter_with_keyword.dart b/lib/widgets/filter/filter_with_keyword.dart index b240ef5..832256f 100644 --- a/lib/widgets/filter/filter_with_keyword.dart +++ b/lib/widgets/filter/filter_with_keyword.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:asmrapp/l10n/l10n.dart'; class FilterWithKeyword extends StatelessWidget { final bool hasSubtitle; @@ -32,7 +33,10 @@ class FilterWithKeyword extends StatelessWidget { Container( decoration: BoxDecoration( border: Border.all( - color: Theme.of(context).colorScheme.outline.withOpacity(0.5), + color: Theme.of(context) + .colorScheme + .outline + .withValues(alpha: 0.5), ), borderRadius: BorderRadius.circular(8), ), @@ -42,25 +46,26 @@ class FilterWithKeyword extends StatelessWidget { onTap: () => onSubtitleChanged(!hasSubtitle), borderRadius: BorderRadius.circular(7), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( - hasSubtitle - ? Icons.check_box + hasSubtitle + ? Icons.check_box : Icons.check_box_outline_blank, size: 20, - color: hasSubtitle - ? colorScheme.primary + color: hasSubtitle + ? colorScheme.primary : colorScheme.onSurfaceVariant, ), const SizedBox(width: 8), Text( - '有字幕', + context.l10n.subtitleAvailable, style: TextStyle( - color: hasSubtitle - ? colorScheme.primary + color: hasSubtitle + ? colorScheme.primary : colorScheme.onSurface, ), ), @@ -75,4 +80,4 @@ class FilterWithKeyword extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/widgets/lyrics/components/lyric_line.dart b/lib/widgets/lyrics/components/lyric_line.dart index 0b8227c..fe5f0ac 100644 --- a/lib/widgets/lyrics/components/lyric_line.dart +++ b/lib/widgets/lyrics/components/lyric_line.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:asmrapp/core/audio/models/subtitle.dart'; +import 'package:flutter/material.dart'; class LyricLine extends StatelessWidget { final Subtitle subtitle; @@ -29,13 +29,16 @@ class LyricLine extends StatelessWidget { child: Text( subtitle.text, style: Theme.of(context).textTheme.bodyLarge?.copyWith( - fontSize: 20, - height: 1.3, - color: isActive - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - fontWeight: isActive ? FontWeight.w600 : FontWeight.normal, - ), + fontSize: 20, + height: 1.3, + color: isActive + ? Theme.of(context).colorScheme.primary + : Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.7), + fontWeight: isActive ? FontWeight.w600 : FontWeight.normal, + ), textAlign: TextAlign.center, ), ), @@ -43,4 +46,4 @@ class LyricLine extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/widgets/lyrics/components/player_lyric_view.dart b/lib/widgets/lyrics/components/player_lyric_view.dart index b619b9e..c52fc66 100644 --- a/lib/widgets/lyrics/components/player_lyric_view.dart +++ b/lib/widgets/lyrics/components/player_lyric_view.dart @@ -7,6 +7,7 @@ import 'package:asmrapp/core/subtitle/i_subtitle_service.dart'; import 'package:asmrapp/core/audio/models/subtitle.dart'; import 'lyric_line.dart'; import 'package:asmrapp/presentation/viewmodels/player_viewmodel.dart'; +import 'package:asmrapp/l10n/l10n.dart'; class PlayerLyricView extends StatefulWidget { final bool immediateScroll; @@ -26,15 +27,16 @@ class _PlayerLyricViewState extends State { final ISubtitleService _subtitleService = GetIt.I(); final PlayerViewModel _viewModel = GetIt.I(); final ItemScrollController _itemScrollController = ItemScrollController(); - final ItemPositionsListener _itemPositionsListener = ItemPositionsListener.create(); - + final ItemPositionsListener _itemPositionsListener = + ItemPositionsListener.create(); + bool _isFirstBuild = true; Subtitle? _lastScrolledSubtitle; - + // 用于控制视图切换的计时器和状态 // 当用户手动滚动时,暂时禁用视图切换功能,防止切换到封面 Timer? _scrollDebounceTimer; - + // 用于控制自动滚动的计时器和状态 // 当用户手动滚动时,暂时禁用自动滚动功能,让用户可以自由浏览歌词 bool _allowAutoScroll = true; @@ -48,21 +50,21 @@ class _PlayerLyricViewState extends State { @override void dispose() { // 清理所有计时器 - _scrollDebounceTimer?.cancel(); // 视图切换计时器 + _scrollDebounceTimer?.cancel(); // 视图切换计时器 _autoScrollDebounceTimer?.cancel(); // 自动滚动计时器 super.dispose(); } void _scrollToCurrentLyric(SubtitleWithState current) { if (!_itemScrollController.isAttached) return; - + // 如果当前禁用了自动滚动(用户正在手动浏览),则不执行自动滚动 if (!_allowAutoScroll) return; - + // 避免重复滚动到同一句歌词 if (_lastScrolledSubtitle == current.subtitle) return; _lastScrolledSubtitle = current.subtitle; - + if (_isFirstBuild) { _isFirstBuild = false; // 首次加载时直接跳转,不使用动画 @@ -85,7 +87,7 @@ class _PlayerLyricViewState extends State { Widget build(BuildContext context) { final screenHeight = MediaQuery.of(context).size.height; final baseUnit = screenHeight * 0.04; - + return StreamBuilder( stream: _subtitleService.currentSubtitleWithStateStream, initialData: _subtitleService.currentSubtitleWithState, @@ -94,8 +96,8 @@ class _PlayerLyricViewState extends State { final subtitleList = _subtitleService.subtitleList; if (subtitleList == null || subtitleList.subtitles.isEmpty) { - return const Center( - child: Text('无歌词'), + return Center( + child: Text(context.l10n.lyricsEmpty), ); } @@ -107,35 +109,40 @@ class _PlayerLyricViewState extends State { return NotificationListener( onNotification: (notification) { - if (notification is ScrollStartNotification && - notification.dragDetails != null) { // 用户开始手动滚动 + if (notification is ScrollStartNotification && + notification.dragDetails != null) { + // 用户开始手动滚动 // 立即禁用视图切换功能 widget.onScrollStateChanged(false); - + // 禁用自动滚动功能 _allowAutoScroll = false; - + // 取消所有待执行的计时器 _scrollDebounceTimer?.cancel(); _autoScrollDebounceTimer?.cancel(); - } else if (notification is ScrollEndNotification) { // 用户结束滚动 + } else if (notification is ScrollEndNotification) { + // 用户结束滚动 // 延长视图切换的禁用时间到1秒 _scrollDebounceTimer?.cancel(); - _scrollDebounceTimer = Timer(const Duration(milliseconds: 1000), () { + _scrollDebounceTimer = + Timer(const Duration(milliseconds: 1000), () { if (mounted) { widget.onScrollStateChanged(true); } }); - + // 自动滚动计时器保持3秒 _autoScrollDebounceTimer?.cancel(); - _autoScrollDebounceTimer = Timer(const Duration(milliseconds: 3000), () { + _autoScrollDebounceTimer = + Timer(const Duration(milliseconds: 3000), () { if (mounted) { setState(() { _allowAutoScroll = true; // 恢复时立即滚动到当前播放位置 if (_subtitleService.currentSubtitleWithState != null) { - _scrollToCurrentLyric(_subtitleService.currentSubtitleWithState!); + _scrollToCurrentLyric( + _subtitleService.currentSubtitleWithState!); } }); } @@ -154,7 +161,7 @@ class _PlayerLyricViewState extends State { itemBuilder: (context, index) { final subtitle = subtitleList.subtitles[index]; final isActive = currentSubtitle?.subtitle == subtitle; - + return Padding( padding: EdgeInsets.symmetric( vertical: baseUnit * 0.35, @@ -165,9 +172,9 @@ class _PlayerLyricViewState extends State { opacity: isActive ? 1.0 : 0.5, onTap: () async { widget.onScrollStateChanged(false); - + await _viewModel.seek(subtitle.start); - + Future.delayed(const Duration(milliseconds: 500), () { if (mounted) { widget.onScrollStateChanged(true); @@ -182,4 +189,4 @@ class _PlayerLyricViewState extends State { }, ); } -} \ No newline at end of file +} diff --git a/lib/widgets/mini_player/mini_player.dart b/lib/widgets/mini_player/mini_player.dart index 1d5a60d..f9b6f2f 100644 --- a/lib/widgets/mini_player/mini_player.dart +++ b/lib/widgets/mini_player/mini_player.dart @@ -1,19 +1,38 @@ +import 'package:asmrapp/presentation/viewmodels/player_viewmodel.dart'; import 'package:asmrapp/screens/player_screen.dart'; import 'package:flutter/material.dart'; -import 'package:asmrapp/presentation/viewmodels/player_viewmodel.dart'; -import 'mini_player_controls.dart'; -import 'mini_player_progress.dart'; import 'package:get_it/get_it.dart'; +import 'package:asmrapp/l10n/l10n.dart'; + +import 'mini_player_controls.dart'; import 'mini_player_cover.dart'; +import 'mini_player_progress.dart'; class MiniPlayer extends StatelessWidget { static const height = 48.0; - - const MiniPlayer({super.key}); + + final bool respectSafeArea; + + const MiniPlayer({ + super.key, + this.respectSafeArea = true, + }); + + static double heightWithSafeArea( + BuildContext context, { + bool respectSafeArea = true, + }) { + final bottomPadding = + respectSafeArea ? MediaQuery.of(context).padding.bottom : 0.0; + return height + bottomPadding; + } @override Widget build(BuildContext context) { final viewModel = GetIt.I(); + final bottomPadding = + respectSafeArea ? MediaQuery.of(context).padding.bottom : 0.0; + return ListenableBuilder( listenable: viewModel, builder: (context, _) { @@ -24,13 +43,14 @@ class MiniPlayer extends StatelessWidget { pageBuilder: (context, animation, secondaryAnimation) { return const PlayerScreen(); }, - transitionsBuilder: (context, animation, secondaryAnimation, child) { + transitionsBuilder: + (context, animation, secondaryAnimation, child) { // 创建一个曲线动画 final curvedAnimation = CurvedAnimation( parent: animation, curve: Curves.easeOutQuart, ); - + return Stack( children: [ // 背景淡入效果 @@ -62,12 +82,13 @@ class MiniPlayer extends StatelessWidget { ); }, child: Container( - height: height, + height: height + bottomPadding, + padding: EdgeInsets.only(bottom: bottomPadding), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 4, offset: const Offset(0, -1), ), @@ -96,7 +117,8 @@ class MiniPlayer extends StatelessWidget { child: Material( color: Colors.transparent, child: Text( - viewModel.currentTrackInfo?.title ?? '未在播放', + viewModel.currentTrackInfo?.title ?? + context.l10n.noPlaying, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleSmall, diff --git a/lib/widgets/player/player_controls.dart b/lib/widgets/player/player_controls.dart index 8dc7e88..1f55f12 100644 --- a/lib/widgets/player/player_controls.dart +++ b/lib/widgets/player/player_controls.dart @@ -8,7 +8,7 @@ class PlayerControls extends StatelessWidget { @override Widget build(BuildContext context) { final viewModel = GetIt.I(); - + return ListenableBuilder( listenable: viewModel, builder: (context, _) { @@ -48,4 +48,4 @@ class PlayerControls extends StatelessWidget { }, ); } -} \ No newline at end of file +} diff --git a/lib/widgets/player/player_cover.dart b/lib/widgets/player/player_cover.dart index e86c89a..1f7d5e3 100644 --- a/lib/widgets/player/player_cover.dart +++ b/lib/widgets/player/player_cover.dart @@ -1,11 +1,11 @@ -import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; class PlayerCover extends StatelessWidget { final String? coverUrl; final double? maxWidth; - + const PlayerCover({ super.key, this.coverUrl, @@ -15,7 +15,7 @@ class PlayerCover extends StatelessWidget { @override Widget build(BuildContext context) { return AspectRatio( - aspectRatio: 4/3, + aspectRatio: 4 / 3, child: Container( constraints: BoxConstraints( maxWidth: maxWidth ?? 480, @@ -25,7 +25,7 @@ class PlayerCover extends StatelessWidget { borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 8), ), @@ -38,7 +38,8 @@ class PlayerCover extends StatelessWidget { imageUrl: coverUrl!, fit: BoxFit.cover, placeholder: (context, url) => Shimmer.fromColors( - baseColor: Theme.of(context).colorScheme.surfaceContainerHighest, + baseColor: + Theme.of(context).colorScheme.surfaceContainerHighest, highlightColor: Theme.of(context).colorScheme.surface, child: Container( color: Colors.white, @@ -60,4 +61,4 @@ class PlayerCover extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/widgets/player/player_progress.dart b/lib/widgets/player/player_progress.dart index 0f1108b..2706adb 100644 --- a/lib/widgets/player/player_progress.dart +++ b/lib/widgets/player/player_progress.dart @@ -1,6 +1,6 @@ +import 'package:asmrapp/presentation/viewmodels/player_viewmodel.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; -import 'package:asmrapp/presentation/viewmodels/player_viewmodel.dart'; class PlayerProgress extends StatelessWidget { const PlayerProgress({super.key}); @@ -22,7 +22,7 @@ class PlayerProgress extends StatelessWidget { @override Widget build(BuildContext context) { final viewModel = GetIt.I(); - + return ListenableBuilder( listenable: viewModel, builder: (context, _) { @@ -39,10 +39,9 @@ class PlayerProgress extends StatelessWidget { ), child: Slider( value: _ensureValueInRange( - viewModel.position?.inMilliseconds.toDouble() ?? 0, - 0, - viewModel.duration?.inMilliseconds.toDouble() ?? 1 - ), + viewModel.position?.inMilliseconds.toDouble() ?? 0, + 0, + viewModel.duration?.inMilliseconds.toDouble() ?? 1), min: 0, max: viewModel.duration?.inMilliseconds.toDouble() ?? 1, onChanged: (value) { @@ -58,14 +57,20 @@ class PlayerProgress extends StatelessWidget { Text( _formatDuration(viewModel.position), style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.7), + ), ), Text( _formatDuration(viewModel.duration), style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), - ), + color: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.7), + ), ), ], ), @@ -76,4 +81,4 @@ class PlayerProgress extends StatelessWidget { }, ); } -} \ No newline at end of file +} diff --git a/lib/widgets/player/player_seek_controls.dart b/lib/widgets/player/player_seek_controls.dart index 08831fd..5672261 100644 --- a/lib/widgets/player/player_seek_controls.dart +++ b/lib/widgets/player/player_seek_controls.dart @@ -8,7 +8,7 @@ class PlayerSeekControls extends StatelessWidget { @override Widget build(BuildContext context) { final viewModel = GetIt.I(); - + return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ @@ -71,4 +71,4 @@ class PlayerSeekControls extends StatelessWidget { ], ); } -} \ No newline at end of file +} diff --git a/lib/widgets/player/player_work_info.dart b/lib/widgets/player/player_work_info.dart index 2abed9d..c1a1f15 100644 --- a/lib/widgets/player/player_work_info.dart +++ b/lib/widgets/player/player_work_info.dart @@ -1,6 +1,7 @@ +import 'package:asmrapp/core/audio/models/playback_context.dart'; import 'package:flutter/material.dart'; import 'package:marquee/marquee.dart'; -import 'package:asmrapp/core/audio/models/playback_context.dart'; +import 'package:asmrapp/l10n/l10n.dart'; class PlayerWorkInfo extends StatelessWidget { final PlaybackContext? context; @@ -21,7 +22,7 @@ class PlayerWorkInfo extends StatelessWidget { SizedBox( height: Theme.of(context).textTheme.titleMedium!.fontSize! * 1.5, child: Marquee( - text: this.context?.work.title ?? '未知作品', + text: this.context?.work.title ?? context.l10n.unknownWorkTitle, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), @@ -39,13 +40,19 @@ class PlayerWorkInfo extends StatelessWidget { ), const SizedBox(height: 2), Text( - this.context?.work.vas + this + .context + ?.work + .vas ?.map((va) => va['name'] as String?) .where((name) => name != null) - .join('、') ?? - '未知演员', + .join('、') ?? + context.l10n.unknownArtist, style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + color: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.7), ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -54,4 +61,4 @@ class PlayerWorkInfo extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/widgets/work_card/components/work_cover_image.dart b/lib/widgets/work_card/components/work_cover_image.dart index 78a8a39..dfc5d69 100644 --- a/lib/widgets/work_card/components/work_cover_image.dart +++ b/lib/widgets/work_card/components/work_cover_image.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; class WorkCoverImage extends StatelessWidget { @@ -54,7 +54,7 @@ class WorkCoverImage extends StatelessWidget { child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: Colors.black.withOpacity(0.7), + color: Colors.black.withValues(alpha: 0.7), borderRadius: BorderRadius.circular(4), ), child: Text( diff --git a/lib/widgets/work_card/components/work_tags_panel.dart b/lib/widgets/work_card/components/work_tags_panel.dart index 6679f1a..81e8613 100644 --- a/lib/widgets/work_card/components/work_tags_panel.dart +++ b/lib/widgets/work_card/components/work_tags_panel.dart @@ -1,6 +1,7 @@ -import 'package:flutter/material.dart'; -import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/data/models/works/tag.dart'; +import 'package:asmrapp/data/models/works/work.dart'; +import 'package:flutter/material.dart'; +import 'package:asmrapp/l10n/l10n.dart'; class WorkTagsPanel extends StatelessWidget { final Work work; @@ -28,7 +29,7 @@ class WorkTagsPanel extends StatelessWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: Colors.orange.withOpacity(0.2), + color: Colors.orange.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), ), child: Text( @@ -42,7 +43,7 @@ class WorkTagsPanel extends StatelessWidget { ...?work.vas?.map((va) => Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: Colors.green.withOpacity(0.2), + color: Colors.green.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), ), child: Text( @@ -57,11 +58,11 @@ class WorkTagsPanel extends StatelessWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.2), + color: Colors.blue.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), ), child: Text( - '字幕', + context.l10n.subtitleTag, style: TextStyle( fontSize: 10, color: Colors.blue[700], diff --git a/lib/widgets/work_card/work_card.dart b/lib/widgets/work_card/work_card.dart index 6561dbc..1554d9b 100644 --- a/lib/widgets/work_card/work_card.dart +++ b/lib/widgets/work_card/work_card.dart @@ -1,5 +1,6 @@ -import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/works/work.dart'; +import 'package:flutter/material.dart'; + import 'components/work_cover_image.dart'; import 'components/work_info_section.dart'; @@ -16,12 +17,12 @@ class WorkCard extends StatelessWidget { @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; - + return Card( clipBehavior: Clip.antiAlias, elevation: isDark ? 0 : 1, - color: isDark - ? Theme.of(context).colorScheme.surfaceVariant + color: isDark + ? Theme.of(context).colorScheme.surfaceContainerHighest : Theme.of(context).colorScheme.surface, child: InkWell( onTap: onTap, diff --git a/lib/widgets/work_grid/components/grid_content.dart b/lib/widgets/work_grid/components/grid_content.dart index 46dc2c8..6174ad1 100644 --- a/lib/widgets/work_grid/components/grid_content.dart +++ b/lib/widgets/work_grid/components/grid_content.dart @@ -59,8 +59,8 @@ class GridContent extends StatelessWidget { }, ), ), - if (config?.enablePagination != false && - currentPage != null && + if (config?.enablePagination != false && + currentPage != null && totalPages != null) SliverToBoxAdapter( child: PaginationControls( @@ -78,4 +78,4 @@ class GridContent extends StatelessWidget { ], ); } -} \ No newline at end of file +} diff --git a/lib/widgets/work_grid/components/grid_empty.dart b/lib/widgets/work_grid/components/grid_empty.dart index fa04847..db87749 100644 --- a/lib/widgets/work_grid/components/grid_empty.dart +++ b/lib/widgets/work_grid/components/grid_empty.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:asmrapp/l10n/l10n.dart'; class GridEmpty extends StatelessWidget { final String? message; @@ -27,7 +28,7 @@ class GridEmpty extends StatelessWidget { ), const SizedBox(height: 16), Text( - message ?? '暂无内容', + message ?? context.l10n.emptyContent, style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: Theme.of(context).colorScheme.outline, ), @@ -36,4 +37,4 @@ class GridEmpty extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/widgets/work_grid/components/grid_error.dart b/lib/widgets/work_grid/components/grid_error.dart index e2a26f9..f7f73e4 100644 --- a/lib/widgets/work_grid/components/grid_error.dart +++ b/lib/widgets/work_grid/components/grid_error.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:asmrapp/l10n/l10n.dart'; class GridError extends StatelessWidget { final String error; @@ -32,11 +33,11 @@ class GridError extends StatelessWidget { FilledButton.icon( onPressed: onRetry, icon: const Icon(Icons.refresh), - label: const Text('重试'), + label: Text(context.l10n.retry), ), ], ], ), ); } -} \ No newline at end of file +} diff --git a/lib/widgets/work_grid/components/grid_loading.dart b/lib/widgets/work_grid/components/grid_loading.dart index a782249..95d463f 100644 --- a/lib/widgets/work_grid/components/grid_loading.dart +++ b/lib/widgets/work_grid/components/grid_loading.dart @@ -29,4 +29,4 @@ class GridLoading extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/widgets/work_grid/enhanced_work_grid_view.dart b/lib/widgets/work_grid/enhanced_work_grid_view.dart index b05145c..5dcf356 100644 --- a/lib/widgets/work_grid/enhanced_work_grid_view.dart +++ b/lib/widgets/work_grid/enhanced_work_grid_view.dart @@ -79,4 +79,4 @@ class EnhancedWorkGridView extends StatelessWidget { return content; } -} \ No newline at end of file +} diff --git a/lib/widgets/work_grid/models/grid_config.dart b/lib/widgets/work_grid/models/grid_config.dart index a77b6ba..6064491 100644 --- a/lib/widgets/work_grid/models/grid_config.dart +++ b/lib/widgets/work_grid/models/grid_config.dart @@ -18,4 +18,4 @@ class GridConfig { }); static const GridConfig defaultConfig = GridConfig(); -} \ No newline at end of file +} diff --git a/lib/widgets/work_grid_view.dart b/lib/widgets/work_grid_view.dart index 3336176..cb96168 100644 --- a/lib/widgets/work_grid_view.dart +++ b/lib/widgets/work_grid_view.dart @@ -3,6 +3,7 @@ import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/widgets/work_grid.dart'; import 'package:asmrapp/presentation/layouts/work_layout_strategy.dart'; import 'package:asmrapp/screens/detail_screen.dart'; +import 'package:asmrapp/l10n/l10n.dart'; class WorkGridView extends StatelessWidget { final List works; @@ -46,7 +47,7 @@ class WorkGridView extends StatelessWidget { const SizedBox(height: 16), ElevatedButton( onPressed: onRetry, - child: const Text('重试'), + child: Text(context.l10n.retry), ), ], ], diff --git a/lib/widgets/work_row.dart b/lib/widgets/work_row.dart index 4a8b34f..c2a5a79 100644 --- a/lib/widgets/work_row.dart +++ b/lib/widgets/work_row.dart @@ -22,10 +22,11 @@ class WorkRow extends StatelessWidget { children: [ // 第一个卡片 Expanded( - child: works.isNotEmpty + child: works.isNotEmpty ? WorkCard( work: works[0], - onTap: onWorkTap != null ? () => onWorkTap!(works[0]) : null, + onTap: + onWorkTap != null ? () => onWorkTap!(works[0]) : null, ) : const SizedBox.shrink(), ), @@ -35,7 +36,8 @@ class WorkRow extends StatelessWidget { child: works.length > 1 ? WorkCard( work: works[1], - onTap: onWorkTap != null ? () => onWorkTap!(works[1]) : null, + onTap: + onWorkTap != null ? () => onWorkTap!(works[1]) : null, ) : const SizedBox.shrink(), // 空占位符,保持两列布局 ), diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c3cc6ff..b24ab12 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,7 +9,6 @@ import audio_service import audio_session import just_audio import package_info_plus -import path_provider_foundation import shared_preferences_foundation import sqflite_darwin import wakelock_plus @@ -19,7 +18,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) diff --git a/pubspec.lock b/pubspec.lock index f68bb19..5f26ea4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,135 +5,114 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" url: "https://pub.dev" source: hosted - version: "76.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.3" + version: "93.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "10.0.1" archive: dependency: transitive description: name: archive - sha256: "08064924cbf0ab88280a0c3f60db9dd24fec693927e725ecb176f16c629d1cb8" + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.9" args: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" audio_service: dependency: "direct main" description: name: audio_service - sha256: "9dd5ba7e77567b290c35908b1950d61485b4dfdd3a0ac398e98cfeec04651b75" + sha256: cb122c7c2639d2a992421ef96b67948ad88c5221da3365ccef1031393a76e044 url: "https://pub.dev" source: hosted - version: "0.18.15" + version: "0.18.18" audio_service_platform_interface: dependency: transitive description: name: audio_service_platform_interface - sha256: "8431a455dac9916cc9ee6f7da5620a666436345c906ad2ebb7fa41d18b3c1bf4" + sha256: "6283782851f6c8b501b60904a32fc7199dc631172da0629d7301e66f672ab777" url: "https://pub.dev" source: hosted - version: "0.1.1" + version: "0.1.3" audio_service_web: dependency: transitive description: name: audio_service_web - sha256: "4cdc2127cd4562b957fb49227dc58e3303fafb09bde2573bc8241b938cf759d9" + sha256: b8ea9243201ee53383157fbccf13d5d2a866b5dda922ec19d866d1d5d70424df url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.1.4" audio_session: dependency: "direct main" description: name: audio_session - sha256: "343e83bc7809fbda2591a49e525d6b63213ade10c76f15813be9aed6657b3261" + sha256: "8f96a7fecbb718cb093070f868b4cdcb8a9b1053dce342ff8ab2fde10eb9afb7" url: "https://pub.dev" source: hosted - version: "0.1.21" + version: "0.2.2" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" build: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: "275bf6bb2a00a9852c28d4e0b410da1d833a734d57d39d44f94bfc895a484ec3" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "4.0.4" build_config: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 url: "https://pub.dev" source: hosted - version: "4.0.2" - build_resolvers: - dependency: transitive - description: - name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" - url: "https://pub.dev" - source: hosted - version: "2.4.2" + version: "4.1.1" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "39ad4ca8a2876779737c60e4228b4bcd35d4352ef7e14e47514093edc012c734" url: "https://pub.dev" source: hosted - version: "2.4.13" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 - url: "https://pub.dev" - source: hosted - version: "7.3.2" + version: "2.11.1" built_collection: dependency: transitive description: @@ -146,10 +125,10 @@ packages: dependency: transitive description: name: built_value - sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + sha256: "7931c90b84bc573fef103548e354258ae4c9d28d140e41961df6843c5d60d4d8" url: "https://pub.dev" source: hosted - version: "8.9.2" + version: "8.12.3" cached_network_image: dependency: "direct main" description: @@ -178,18 +157,18 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" checked_yaml: dependency: transitive description: name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.4" cli_util: dependency: transitive description: @@ -202,26 +181,34 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" code_builder: dependency: transitive description: name: code_builder - sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" url: "https://pub.dev" source: hosted - version: "4.10.1" + version: "4.11.1" collection: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" convert: dependency: transitive description: @@ -234,10 +221,10 @@ packages: dependency: "direct main" description: name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" cupertino_icons: dependency: "direct main" description: @@ -250,34 +237,34 @@ packages: dependency: transitive description: name: dart_style - sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + sha256: "15a7db352c8fc6a4d2bc475ba901c25b39fe7157541da4c16eacce6f8be83e49" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "3.1.5" dbus: dependency: transitive description: name: dbus - sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.12" dio: dependency: "direct main" description: name: dio - sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + sha256: b9d46faecab38fc8cc286f80bc4d61a3bb5d4ac49e51ed877b4d6706efe57b25 url: "https://pub.dev" source: hosted - version: "5.7.0" + version: "5.9.1" dio_web_adapter: dependency: transitive description: name: dio_web_adapter - sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.1" fading_edge_scrollview: dependency: transitive description: @@ -290,18 +277,18 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" ffi: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.0" file: dependency: transitive description: @@ -335,18 +322,23 @@ packages: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" url: "https://pub.dev" source: hosted - version: "0.13.1" + version: "0.14.4" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "6.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -361,42 +353,34 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" + sha256: f23ea33b3863f119b58ed1b586e881a46bd28715ddcc4dbc33104524e3434131 url: "https://pub.dev" source: hosted - version: "2.5.7" + version: "3.2.5" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 - url: "https://pub.dev" - source: hosted - version: "2.4.4" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "3.1.0" get_it: dependency: "direct main" description: name: get_it - sha256: c49895c1ecb0ee2a0ec568d39de882e2c299ba26355aa6744ab1001f98cebd15 + sha256: "1d648d2dd2047d7f7450d5727ca24ee435f240385753d90b49650e3cdff32e56" url: "https://pub.dev" source: hosted - version: "8.0.2" + version: "9.2.0" glob: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" graphs: dependency: transitive description: @@ -405,134 +389,158 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + hooks: + dependency: transitive + description: + name: hooks + sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" http: dependency: transitive description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.6.0" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" image: dependency: transitive description: name: image - sha256: "20842a5ad1555be624c314b0c0cc0566e8ece412f61e859a42efeb6d4101a26c" + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + url: "https://pub.dev" + source: hosted + version: "4.8.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "0.20.2" io: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: transitive description: name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "0.7.2" json_annotation: dependency: "direct main" description: name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df" + url: "https://pub.dev" + source: hosted + version: "4.10.0" + json_schema: + dependency: transitive + description: + name: json_schema + sha256: f37d9c3fdfe8c9aae55fdfd5af815d24ce63c3a0f6a2c1f0982c30f43643fa1a url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "5.2.2" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c + sha256: "93fba3ad139dab2b1ce59ecc6fdce6da46a42cdb6c4399ecda30f1e7e725760d" url: "https://pub.dev" source: hosted - version: "6.9.0" + version: "6.12.0" just_audio: dependency: "direct main" description: name: just_audio - sha256: a49e7120b95600bd357f37a2bb04cd1e88252f7cdea8f3368803779b925b1049 + sha256: "9694e4734f515f2a052493d1d7e0d6de219ee0427c7c29492e246ff32a219908" url: "https://pub.dev" source: hosted - version: "0.9.42" + version: "0.10.5" just_audio_platform_interface: dependency: transitive description: name: just_audio_platform_interface - sha256: "0243828cce503c8366cc2090cefb2b3c871aa8ed2f520670d76fd47aa1ab2790" + sha256: "2532c8d6702528824445921c5ff10548b518b13f808c2e34c2fd54793b999a6a" url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.6.0" just_audio_web: dependency: transitive description: name: just_audio_web - sha256: "9a98035b8b24b40749507687520ec5ab404e291d2b0937823ff45d92cb18d448" + sha256: "6ba8a2a7e87d57d32f0f7b42856ade3d6a9fbe0f1a11fabae0a4f00bb73f0663" url: "https://pub.dev" source: hosted - version: "0.4.13" + version: "0.4.16" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "6.1.0" logger: dependency: "direct main" description: name: logger - sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 + sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.2" logging: dependency: transitive description: @@ -541,14 +549,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - macros: - dependency: transitive - description: - name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" - url: "https://pub.dev" - source: hosted - version: "0.1.3-main.0" marquee: dependency: "direct main" description: @@ -561,26 +561,26 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.17.0" mime: dependency: transitive description: @@ -589,6 +589,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" + url: "https://pub.dev" + source: hosted + version: "0.17.4" nested: dependency: transitive description: @@ -597,6 +605,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" octo_image: dependency: transitive description: @@ -609,34 +625,34 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" package_info_plus: dependency: transitive description: name: package_info_plus - sha256: "70c421fe9d9cc1a9a7f3b05ae56befd469fe4f8daa3b484823141a55442d858d" + sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d url: "https://pub.dev" source: hosted - version: "8.1.2" + version: "9.0.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.2.1" path: dependency: "direct overridden" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider: dependency: "direct main" description: @@ -649,18 +665,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "8c4967f8b7cb46dc914e178daa29813d83ae502e0529d7b0478330616a691ef7" + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e url: "https://pub.dev" source: hosted - version: "2.2.14" + version: "2.2.22" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.6.0" path_provider_linux: dependency: transitive description: @@ -689,26 +705,26 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 url: "https://pub.dev" source: hosted - version: "11.3.1" + version: "12.0.1" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1" + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" url: "https://pub.dev" source: hosted - version: "12.0.13" + version: "13.0.1" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0 + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 url: "https://pub.dev" source: hosted - version: "9.4.5" + version: "9.4.7" permission_handler_html: dependency: transitive description: @@ -721,10 +737,10 @@ packages: dependency: transitive description: name: permission_handler_platform_interface - sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 url: "https://pub.dev" source: hosted - version: "4.2.3" + version: "4.3.0" permission_handler_windows: dependency: transitive description: @@ -737,10 +753,10 @@ packages: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "7.0.2" platform: dependency: transitive description: @@ -761,42 +777,58 @@ packages: dependency: transitive description: name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" url: "https://pub.dev" source: hosted - version: "1.5.1" + version: "1.5.2" posix: dependency: transitive description: name: posix - sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" url: "https://pub.dev" source: hosted - version: "6.0.1" + version: "6.0.3" provider: dependency: "direct main" description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.5+1" pub_semver: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + rfc_6901: + dependency: transitive + description: + name: rfc_6901 + sha256: "6a43b1858dca2febaf93e15639aa6b0c49ccdfd7647775f15a499f872b018154" + url: "https://pub.dev" + source: hosted + version: "0.2.1" rxdart: dependency: "direct main" description: @@ -817,26 +849,26 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82" + sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.5.4" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "7f172d1b06de5da47b6264c2692ee2ead20bbbc246690427cdb4fc301cd0c549" + sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.4.20" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.6" shared_preferences_linux: dependency: transitive description: @@ -857,10 +889,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" shared_preferences_windows: dependency: transitive description: @@ -873,18 +905,18 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.0" shimmer: dependency: "direct main" description: @@ -902,66 +934,58 @@ packages: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "1d562a3c1f713904ebbed50d2760217fd8a51ca170ac4b05b0db490699dbac17" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "4.2.0" source_helper: dependency: transitive description: name: source_helper - sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + sha256: "4a85e90b50694e652075cbe4575665539d253e6ec10e46e76b45368ab5e3caae" url: "https://pub.dev" source: hosted - version: "1.3.4" + version: "1.3.10" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.dev" - source: hosted - version: "1.10.0" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "1.10.2" sqflite: dependency: transitive description: name: sqflite - sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" sqflite_android: dependency: transitive description: name: sqflite_android - sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.2+2" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" url: "https://pub.dev" source: hosted - version: "2.5.4+6" + version: "2.5.6" sqflite_darwin: dependency: transitive description: name: sqflite_darwin - sha256: "96a698e2bc82bd770a4d6aab00b42396a7c63d9e33513a56945cbccb594c2474" + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" sqflite_platform_interface: dependency: transitive description: @@ -974,66 +998,58 @@ packages: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" synchronized: dependency: transitive description: name: synchronized - sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 url: "https://pub.dev" source: hosted - version: "3.3.0+3" + version: "3.4.0" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" - url: "https://pub.dev" - source: hosted - version: "0.7.3" - timing: - dependency: transitive - description: - name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "0.7.9" typed_data: dependency: transitive description: @@ -1042,86 +1058,94 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + uri: + dependency: transitive + description: + name: uri + sha256: "889eea21e953187c6099802b7b4cf5219ba8f3518f604a1033064d45b1b8268a" + url: "https://pub.dev" + source: hosted + version: "1.0.0" uuid: dependency: transitive description: name: uuid - sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 url: "https://pub.dev" source: hosted - version: "4.5.1" + version: "4.5.2" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "15.0.2" wakelock_plus: dependency: "direct main" description: name: wakelock_plus - sha256: bf4ee6f17a2fa373ed3753ad0e602b7603f8c75af006d5b9bdade263928c0484 + sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228" url: "https://pub.dev" source: hosted - version: "1.2.8" + version: "1.4.0" wakelock_plus_platform_interface: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: "422d1cdbb448079a8a62a5a770b69baa489f8f7ca21aef47800c726d404f9d16" + sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" watcher: dependency: transitive description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.1" web: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web_socket: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" win32: dependency: transitive description: name: win32 - sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69" + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e url: "https://pub.dev" source: hosted - version: "5.9.0" + version: "5.15.0" xdg_directories: dependency: transitive description: @@ -1134,18 +1158,18 @@ packages: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.6.1" yaml: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" sdks: - dart: ">=3.5.0 <4.0.0" - flutter: ">=3.24.0" + dart: ">=3.10.3 <4.0.0" + flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index 710a7d4..3233885 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,7 +30,10 @@ environment: dependencies: flutter: sdk: flutter - freezed_annotation: ^2.4.1 + flutter_localizations: + sdk: flutter + intl: ^0.20.2 + freezed_annotation: ^3.1.0 json_annotation: ^4.9.0 # The following adds the Cupertino Icons font to your application. @@ -41,16 +44,16 @@ dependencies: cached_network_image: ^3.3.0 logger: ^2.5.0 shimmer: ^3.0.0 - just_audio: ^0.9.36 - audio_session: ^0.1.18 - get_it: ^8.0.2 + just_audio: ^0.10.5 + audio_session: ^0.2.2 + get_it: ^9.2.0 audio_service: ^0.18.12 rxdart: ^0.28.0 path_provider: ^2.1.5 crypto: ^3.0.6 shared_preferences: ^2.2.2 flutter_cache_manager: ^3.4.1 - permission_handler: ^11.3.1 + permission_handler: ^12.0.1 scrollable_positioned_list: ^0.3.8 marquee: ^2.3.0 wakelock_plus: ^1.2.8 @@ -59,16 +62,16 @@ dev_dependencies: flutter_test: sdk: flutter build_runner: ^2.4.7 - freezed: ^2.4.6 + freezed: ^3.2.5 json_serializable: ^6.7.1 - flutter_launcher_icons: ^0.13.1 + flutter_launcher_icons: ^0.14.4 # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^2.0.0 + flutter_lints: ^6.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec @@ -80,6 +83,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: