From 888298250296b16efd4f7e19fbe17efac2c75e33 Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Sat, 29 Nov 2025 23:08:28 +0900 Subject: [PATCH 01/30] Refactor code structure for improved readability and maintainability; removed redundant code blocks and optimized function calls. --- lib/core/audio/audio_player_handler.dart | 2 +- lib/core/audio/audio_player_service.dart | 11 +- lib/core/audio/cache/audio_cache_manager.dart | 31 +- .../controllers/playback_controller.dart | 41 +- lib/core/audio/events/playback_event.dart | 2 +- lib/core/audio/events/playback_event_hub.dart | 37 +- lib/core/audio/i_audio_player_service.dart | 2 +- lib/core/audio/models/file_path.dart | 39 +- lib/core/audio/models/play_mode.dart | 8 +- lib/core/audio/models/playback_context.dart | 53 ++- lib/core/audio/models/subtitle.dart | 49 ++- .../audio/state/playback_state_manager.dart | 44 +- .../storage/i_playback_state_repository.dart | 2 +- .../storage/playback_state_repository.dart | 2 +- lib/core/audio/utils/audio_error_handler.dart | 17 +- lib/core/audio/utils/playlist_builder.dart | 12 +- lib/core/audio/utils/track_info_creator.dart | 4 +- .../cache/recommendation_cache_manager.dart | 17 +- lib/core/di/service_locator.dart | 12 +- .../dummy_lyric_overlay_controller.dart | 21 +- .../platform/i_lyric_overlay_controller.dart | 16 +- .../platform/lyric_overlay_controller.dart | 18 +- lib/core/platform/lyric_overlay_manager.dart | 49 ++- lib/core/platform/wakelock_controller.dart | 6 +- .../cache/subtitle_cache_manager.dart | 4 +- lib/core/subtitle/i_subtitle_service.dart | 20 +- .../managers/subtitle_state_manager.dart | 16 +- lib/core/subtitle/parsers/lrc_parser.dart | 26 +- .../subtitle/parsers/subtitle_parser.dart | 8 +- .../parsers/subtitle_parser_factory.dart | 4 +- lib/core/subtitle/parsers/vtt_parser.dart | 27 +- lib/core/subtitle/subtitle_loader.dart | 37 +- lib/core/subtitle/subtitle_service.dart | 22 +- lib/core/subtitle/utils/subtitle_matcher.dart | 30 +- lib/core/theme/app_colors.dart | 20 +- lib/core/theme/app_theme.dart | 79 ++-- lib/core/theme/theme_controller.dart | 10 +- lib/data/models/mark_status.dart | 2 +- lib/data/models/playback/playback_state.dart | 8 +- lib/data/repositories/auth_repository.dart | 2 +- lib/data/services/api_service.dart | 17 +- lib/data/services/auth_service.dart | 16 +- .../interceptors/auth_interceptor.dart | 10 +- lib/presentation/models/filter_state.dart | 17 +- .../viewmodels/auth_viewmodel.dart | 9 +- .../base/paginated_works_viewmodel.dart | 9 +- .../viewmodels/detail_viewmodel.dart | 25 +- .../viewmodels/favorites_viewmodel.dart | 2 +- .../viewmodels/home_viewmodel.dart | 4 +- .../viewmodels/player_viewmodel.dart | 19 +- .../viewmodels/playlist_works_viewmodel.dart | 11 +- .../viewmodels/playlists_viewmodel.dart | 33 +- .../viewmodels/popular_viewmodel.dart | 8 +- .../viewmodels/recommend_viewmodel.dart | 10 +- .../settings/cache_manager_viewmodel.dart | 14 +- .../viewmodels/similar_works_viewmodel.dart | 5 +- .../widgets/auth/login_dialog.dart | 6 +- lib/screens/contents/home_content.dart | 8 +- .../playlists/playlist_works_view.dart | 4 +- .../playlists/playlists_list_view.dart | 2 +- lib/screens/contents/playlists_content.dart | 21 +- lib/screens/contents/popular_content.dart | 8 +- lib/screens/contents/recommend_content.dart | 17 +- lib/screens/detail_screen.dart | 3 +- lib/screens/favorites_screen.dart | 2 +- lib/screens/main_screen.dart | 24 +- lib/screens/player_screen.dart | 36 +- lib/screens/search_screen.dart | 12 +- .../settings/cache_manager_screen.dart | 32 +- lib/screens/similar_works_screen.dart | 8 +- lib/widgets/common/tag_chip.dart | 6 +- lib/widgets/detail/mark_selection_dialog.dart | 40 +- .../detail/playlist_selection_dialog.dart | 20 +- lib/widgets/detail/work_action_buttons.dart | 14 +- lib/widgets/detail/work_cover.dart | 7 +- lib/widgets/detail/work_file_item.dart | 12 +- lib/widgets/detail/work_files_list.dart | 10 +- lib/widgets/detail/work_folder_item.dart | 12 +- lib/widgets/detail/work_info_header.dart | 12 +- lib/widgets/detail/work_stats_info.dart | 2 +- lib/widgets/drawer_menu.dart | 17 +- lib/widgets/filter/filter_panel.dart | 42 +- lib/widgets/filter/filter_with_keyword.dart | 22 +- lib/widgets/lyrics/components/lyric_line.dart | 21 +- .../lyrics/components/player_lyric_view.dart | 50 ++- lib/widgets/mini_player/mini_player.dart | 16 +- lib/widgets/player/player_controls.dart | 4 +- lib/widgets/player/player_cover.dart | 13 +- lib/widgets/player/player_progress.dart | 27 +- lib/widgets/player/player_seek_controls.dart | 4 +- lib/widgets/player/player_work_info.dart | 16 +- .../components/work_cover_image.dart | 4 +- .../work_card/components/work_tags_panel.dart | 10 +- lib/widgets/work_card/work_card.dart | 9 +- .../work_grid/components/grid_content.dart | 6 +- .../work_grid/components/grid_empty.dart | 2 +- .../work_grid/components/grid_error.dart | 2 +- .../work_grid/components/grid_loading.dart | 2 +- .../work_grid/enhanced_work_grid_view.dart | 2 +- lib/widgets/work_grid/models/grid_config.dart | 2 +- lib/widgets/work_row.dart | 8 +- pubspec.lock | 413 +++++++++--------- 102 files changed, 1037 insertions(+), 992 deletions(-) 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/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..ce91f49 100644 --- a/lib/presentation/viewmodels/detail_viewmodel.dart +++ b/lib/presentation/viewmodels/detail_viewmodel.dart @@ -23,7 +23,7 @@ class DetailViewModel extends ChangeNotifier { bool _isLoading = false; String? _error; bool _disposed = false; - + bool _hasRecommendations = false; bool _checkingRecommendations = false; @@ -63,10 +63,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; @@ -158,7 +159,7 @@ class DetailViewModel extends ChangeNotifier { workId: work.id.toString(), page: page, ); - + _playlists = response.playlists; _playlistsPagination = response.pagination; AppLogger.info('收藏夹列表加载成功: ${_playlists?.length ?? 0}个收藏夹'); @@ -174,14 +175,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( @@ -222,7 +223,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 +231,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 +249,7 @@ class DetailViewModel extends ChangeNotifier { work.id.toString(), _apiService.convertMarkStatusToApi(status), ); - + _currentMarkStatus = status; AppLogger.info('更新标记状态成功: ${status.label}'); } catch (e) { 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..1cbb10e 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; @@ -144,4 +147,4 @@ class PlaylistsViewModel extends ChangeNotifier { 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..9721900 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 = []; @@ -19,7 +20,8 @@ class RecommendViewModel extends ChangeNotifier { bool _hasSubtitle = false; bool _filterPanelExpanded = false; - RecommendViewModel(this._authViewModel) : _apiService = GetIt.I() { + RecommendViewModel(this._authViewModel) + : _apiService = GetIt.I() { _loadFilterState(); } @@ -84,7 +86,7 @@ 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) { @@ -126,4 +128,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..02c4df0 100644 --- a/lib/presentation/viewmodels/settings/cache_manager_viewmodel.dart +++ b/lib/presentation/viewmodels/settings/cache_manager_viewmodel.dart @@ -31,12 +31,12 @@ class CacheManagerViewModel extends ChangeNotifier { try { _isLoading = true; notifyListeners(); - + // 获取音频缓存大小 _audioCacheSize = await AudioCacheManager.getCacheSize(); // 获取字幕缓存大小 _subtitleCacheSize = await SubtitleCacheManager.getSize(); - + _error = null; } catch (e) { AppLogger.error('加载缓存大小失败', e); @@ -52,7 +52,7 @@ class CacheManagerViewModel extends ChangeNotifier { try { _isLoading = true; notifyListeners(); - + await AudioCacheManager.cleanCache(); await loadCacheSize(); _error = null; @@ -70,7 +70,7 @@ class CacheManagerViewModel extends ChangeNotifier { try { _isLoading = true; notifyListeners(); - + await SubtitleCacheManager.clearCache(); await loadCacheSize(); _error = null; @@ -88,12 +88,12 @@ class CacheManagerViewModel extends ChangeNotifier { try { _isLoading = true; notifyListeners(); - + await Future.wait([ AudioCacheManager.cleanCache(), SubtitleCacheManager.clearCache(), ]); - + await loadCacheSize(); _error = null; } catch (e) { @@ -104,4 +104,4 @@ class CacheManagerViewModel extends ChangeNotifier { notifyListeners(); } } -} \ No newline at end of file +} 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..d5d7840 100644 --- a/lib/presentation/widgets/auth/login_dialog.dart +++ b/lib/presentation/widgets/auth/login_dialog.dart @@ -25,10 +25,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: 登录成功,关闭对话框'); @@ -115,4 +115,4 @@ class _LoginDialogState extends State { ], ); } -} \ 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..3faced6 100644 --- a/lib/screens/contents/playlists/playlist_works_view.dart +++ b/lib/screens/contents/playlists/playlist_works_view.dart @@ -20,7 +20,7 @@ class PlaylistWorksView extends StatelessWidget { @override Widget build(BuildContext context) { final playlistsViewModel = context.read(); - + return ChangeNotifierProvider( create: (_) => PlaylistWorksViewModel(playlist)..loadWorks(), child: Consumer( @@ -68,4 +68,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..3bf297c 100644 --- a/lib/screens/contents/playlists/playlists_list_view.dart +++ b/lib/screens/contents/playlists/playlists_list_view.dart @@ -67,4 +67,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..fe0ddc5 100644 --- a/lib/screens/contents/recommend_content.dart +++ b/lib/screens/contents/recommend_content.dart @@ -1,10 +1,9 @@ -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'; class RecommendContent extends StatefulWidget { const RecommendContent({super.key}); @@ -13,7 +12,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 +21,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(); @@ -85,4 +86,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..8fafc28 100644 --- a/lib/screens/detail_screen.dart +++ b/lib/screens/detail_screen.dart @@ -52,7 +52,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; diff --git a/lib/screens/favorites_screen.dart b/lib/screens/favorites_screen.dart index 0d98662..1674577 100644 --- a/lib/screens/favorites_screen.dart +++ b/lib/screens/favorites_screen.dart @@ -80,4 +80,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..de5c555 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -1,17 +1,17 @@ -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/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 都在这里初始化和提供。 @@ -112,7 +112,7 @@ class _MainScreenState extends State { // 构建标题文本 final title = totalCount != null - ? '${_titles[_currentIndex]} (${totalCount})' + ? '${_titles[_currentIndex]} ($totalCount)' : _titles[_currentIndex]; return Scaffold( @@ -192,4 +192,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..1f9a3d9 100644 --- a/lib/screens/player_screen.dart +++ b/lib/screens/player_screen.dart @@ -1,14 +1,14 @@ 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'; class PlayerScreen extends StatefulWidget { const PlayerScreen({super.key}); @@ -35,7 +35,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( @@ -103,7 +103,10 @@ class _PlayerScreenState extends State { color: Colors.transparent, child: Text( _viewModel.currentTrackInfo?.title ?? '未在播放', - style: Theme.of(context).textTheme.titleLarge?.copyWith( + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith( fontWeight: FontWeight.w600, ), textAlign: TextAlign.center, @@ -114,11 +117,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,9 +181,9 @@ 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 ? '关闭屏幕常亮' : '开启屏幕常亮', onPressed: () => wakeLockController.toggle(), @@ -206,8 +212,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..e57e6f5 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -1,10 +1,10 @@ -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/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 +44,7 @@ class _SearchScreenContentState extends State { void initState() { super.initState(); _searchController = TextEditingController(text: widget.initialKeyword); - + // 如果有初始关键词,自动执行搜索 if (widget.initialKeyword?.isNotEmpty == true) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -126,7 +126,7 @@ class _SearchScreenContentState extends State { fillColor: Theme.of(context) .colorScheme .surfaceContainerHighest - .withOpacity(0.5), + .withValues(alpha: 0.5), border: OutlineInputBorder( borderRadius: BorderRadius.circular(24), borderSide: BorderSide.none, diff --git a/lib/screens/settings/cache_manager_screen.dart b/lib/screens/settings/cache_manager_screen.dart index 7054a73..26c8d08 100644 --- a/lib/screens/settings/cache_manager_screen.dart +++ b/lib/screens/settings/cache_manager_screen.dart @@ -35,47 +35,45 @@ class CacheManagerScreen extends StatelessWidget { title: const Text('音频缓存'), subtitle: Text(viewModel.audioCacheSizeFormatted), trailing: TextButton( - onPressed: viewModel.isLoading - ? null - : () => viewModel.clearAudioCache(), + onPressed: viewModel.isLoading + ? null + : () => viewModel.clearAudioCache(), child: const Text('清理'), ), ), const Divider(), - + // 字幕缓存 ListTile( title: const Text('字幕缓存'), subtitle: Text(viewModel.subtitleCacheSizeFormatted), trailing: TextButton( - onPressed: viewModel.isLoading - ? null - : () => viewModel.clearSubtitleCache(), + onPressed: viewModel.isLoading + ? null + : () => viewModel.clearSubtitleCache(), child: const Text('清理'), ), ), const Divider(), - + // 总缓存大小 ListTile( title: const Text('总缓存大小'), subtitle: Text(viewModel.totalCacheSizeFormatted), trailing: TextButton( - onPressed: viewModel.isLoading - ? null - : () => viewModel.clearAllCache(), + onPressed: viewModel.isLoading + ? null + : () => viewModel.clearAllCache(), child: const Text('清理全部'), ), ), const Divider(), - + // 缓存说明 const ListTile( title: Text('缓存说明'), - subtitle: Text( - '缓存用于存储最近播放的音频文件和字幕文件,以提高再次播放时的加载速度。' - '系统会自动清理过期和超量的缓存。' - ), + subtitle: Text('缓存用于存储最近播放的音频文件和字幕文件,以提高再次播放时的加载速度。' + '系统会自动清理过期和超量的缓存。'), ), ], ); @@ -84,4 +82,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..e171bb2 100644 --- a/lib/screens/similar_works_screen.dart +++ b/lib/screens/similar_works_screen.dart @@ -39,7 +39,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(); } @@ -111,7 +112,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 +124,4 @@ class _SimilarWorksScreenState extends State { ), ); } -} \ No newline at end of file +} 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..92f6a22 100644 --- a/lib/widgets/detail/mark_selection_dialog.dart +++ b/lib/widgets/detail/mark_selection_dialog.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/mark_status.dart'; +import 'package:flutter/material.dart'; class MarkSelectionDialog extends StatelessWidget { final MarkStatus? currentStatus; @@ -16,7 +16,7 @@ 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( @@ -34,17 +34,19 @@ 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; @@ -60,12 +62,14 @@ 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) + onTap: loading + ? null + : () { + onMarkSelected(status); + Navigator.of(context).pop(); + }, + hoverColor: isDark + ? Colors.white.withOpacity(0.05) : Colors.black.withOpacity(0.05), ); }).toList(), @@ -75,4 +79,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..8e55558 100644 --- a/lib/widgets/detail/playlist_selection_dialog.dart +++ b/lib/widgets/detail/playlist_selection_dialog.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/playlists_with_exist_statu/playlist.dart'; +import 'package:flutter/material.dart'; class PlaylistSelectionDialog extends StatefulWidget { final List? playlists; @@ -18,7 +18,8 @@ class PlaylistSelectionDialog extends StatefulWidget { }); @override - State createState() => _PlaylistSelectionDialogState(); + State createState() => + _PlaylistSelectionDialogState(); } class _PlaylistSelectionDialogState extends State { @@ -40,7 +41,7 @@ class _PlaylistSelectionDialogState extends State { void _updateItemStates() { if (widget.playlists == null) return; - + final newStates = {}; for (final playlist in widget.playlists!) { newStates[playlist.id!] = _PlaylistItemState( @@ -135,12 +136,12 @@ 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, @@ -155,13 +156,14 @@ class _PlaylistSelectionDialogState extends State { ), ), 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 { @@ -230,9 +232,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..88589f2 100644 --- a/lib/widgets/detail/work_action_buttons.dart +++ b/lib/widgets/detail/work_action_buttons.dart @@ -51,7 +51,9 @@ class WorkActionButtons extends StatelessWidget { ), _ActionButton( icon: Icons.recommend, - label: checkingRecommendations ? '检查中' : (hasRecommendations ? '相关推荐' : '暂无推荐'), + label: checkingRecommendations + ? '检查中' + : (hasRecommendations ? '相关推荐' : '暂无推荐'), onTap: hasRecommendations ? onRecommendationsTap : null, loading: checkingRecommendations, ), @@ -78,7 +80,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 +101,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 +110,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 +119,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..1be2d1b 100644 --- a/lib/widgets/detail/work_files_list.dart +++ b/lib/widgets/detail/work_files_list.dart @@ -1,8 +1,8 @@ -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'; class WorkFilesList extends StatelessWidget { final Files files; @@ -18,7 +18,7 @@ class WorkFilesList extends StatelessWidget { Widget build(BuildContext context) { // 重置文件夹展开状态 WorkFolderItem.resetExpandState(); - + return Card( margin: const EdgeInsets.all(8), child: Column( @@ -35,7 +35,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..ae9476d 100644 --- a/lib/widgets/detail/work_info_header.dart +++ b/lib/widgets/detail/work_info_header.dart @@ -1,8 +1,8 @@ -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'; class WorkInfoHeader extends StatelessWidget { final Work work; @@ -42,14 +42,14 @@ 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'] ?? ''), ), @@ -57,7 +57,7 @@ class WorkInfoHeader extends StatelessWidget { if (work.hasSubtitle == true) TagChip( text: '字幕', - backgroundColor: Colors.blue.withOpacity(0.2), + backgroundColor: Colors.blue.withValues(alpha: 0.2), textColor: Colors.blue[700], ), ], @@ -65,4 +65,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..b561aab 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/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}); @@ -63,7 +63,6 @@ class DrawerMenu extends StatelessWidget { ); }, ), - ListTile( leading: const Icon(Icons.favorite), title: const Text(Strings.favorites), @@ -106,10 +105,10 @@ 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( diff --git a/lib/widgets/filter/filter_panel.dart b/lib/widgets/filter/filter_panel.dart index e18eb19..07e3f3a 100644 --- a/lib/widgets/filter/filter_panel.dart +++ b/lib/widgets/filter/filter_panel.dart @@ -58,7 +58,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 +71,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( '有字幕', style: TextStyle( - color: hasSubtitle - ? Theme.of(context).colorScheme.primary + color: hasSubtitle + ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurface, ), ), @@ -99,13 +105,17 @@ 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: [ @@ -134,7 +144,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 +157,17 @@ 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 ? '降序' : '升序'), 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, ), @@ -173,4 +189,4 @@ class FilterPanel extends StatelessWidget { child: Text(text), ); } -} \ 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..170ea33 100644 --- a/lib/widgets/filter/filter_with_keyword.dart +++ b/lib/widgets/filter/filter_with_keyword.dart @@ -32,7 +32,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 +45,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( '有字幕', style: TextStyle( - color: hasSubtitle - ? colorScheme.primary + color: hasSubtitle + ? colorScheme.primary : colorScheme.onSurface, ), ), @@ -75,4 +79,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..bead592 100644 --- a/lib/widgets/lyrics/components/player_lyric_view.dart +++ b/lib/widgets/lyrics/components/player_lyric_view.dart @@ -26,15 +26,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 +49,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 +86,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, @@ -107,35 +108,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 +160,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 +171,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 +188,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..56a6e9a 100644 --- a/lib/widgets/mini_player/mini_player.dart +++ b/lib/widgets/mini_player/mini_player.dart @@ -1,14 +1,15 @@ +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 '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}); @override @@ -24,13 +25,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: [ // 背景淡入效果 @@ -67,7 +69,7 @@ class MiniPlayer extends StatelessWidget { 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), ), 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..9efd46a 100644 --- a/lib/widgets/player/player_work_info.dart +++ b/lib/widgets/player/player_work_info.dart @@ -1,6 +1,6 @@ +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'; class PlayerWorkInfo extends StatelessWidget { final PlaybackContext? context; @@ -39,13 +39,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('、') ?? '未知演员', 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 +60,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..80c32a1 100644 --- a/lib/widgets/work_card/components/work_tags_panel.dart +++ b/lib/widgets/work_card/components/work_tags_panel.dart @@ -1,6 +1,6 @@ -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'; class WorkTagsPanel extends StatelessWidget { final Work work; @@ -28,7 +28,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 +42,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,7 +57,7 @@ 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( 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..814fc96 100644 --- a/lib/widgets/work_grid/components/grid_empty.dart +++ b/lib/widgets/work_grid/components/grid_empty.dart @@ -36,4 +36,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..cc6bd99 100644 --- a/lib/widgets/work_grid/components/grid_error.dart +++ b/lib/widgets/work_grid/components/grid_error.dart @@ -39,4 +39,4 @@ class GridError extends StatelessWidget { ), ); } -} \ 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_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/pubspec.lock b/pubspec.lock index f68bb19..30051f4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,135 +5,130 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f url: "https://pub.dev" source: hosted - version: "76.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.3" + version: "85.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d" url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "7.7.1" archive: dependency: transitive description: name: archive - sha256: "08064924cbf0ab88280a0c3f60db9dd24fec693927e725ecb176f16c629d1cb8" + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.7" 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: "2b7fff16a552486d078bfc09a8cde19f426dc6d6329262b684182597bec5b1ac" url: "https://pub.dev" source: hosted - version: "0.1.21" + version: "0.1.25" 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: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.5.4" build_config: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.5.4" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" url: "https://pub.dev" source: hosted - version: "7.3.2" + version: "9.1.2" built_collection: dependency: transitive description: @@ -146,10 +141,10 @@ packages: dependency: transitive description: name: built_value - sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + sha256: "426cf75afdb23aa74bd4e471704de3f9393f3c7b04c1e2d9c6f1073ae0b8b139" url: "https://pub.dev" source: hosted - version: "8.9.2" + version: "8.12.1" cached_network_image: dependency: "direct main" description: @@ -178,18 +173,18 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" 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 +197,26 @@ 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_builder: dependency: transitive description: name: code_builder - sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" url: "https://pub.dev" source: hosted - version: "4.10.1" + version: "4.11.0" 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 +229,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 +245,34 @@ packages: dependency: transitive description: name: dart_style - sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "3.1.1" dbus: dependency: transitive description: name: dbus - sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" dio: dependency: "direct main" description: name: dio - sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 url: "https://pub.dev" source: hosted - version: "5.7.0" + version: "5.9.0" 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 +285,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: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" file: dependency: transitive description: @@ -361,10 +356,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" + sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c" url: "https://pub.dev" source: hosted - version: "2.5.7" + version: "2.5.8" freezed_annotation: dependency: "direct main" description: @@ -385,18 +380,18 @@ packages: dependency: "direct main" description: name: get_it - sha256: c49895c1ecb0ee2a0ec568d39de882e2c299ba26355aa6744ab1001f98cebd15 + sha256: ae78de7c3f2304b8d81f2bb6e320833e5e81de942188542328f074978cc0efa9 url: "https://pub.dev" source: hosted - version: "8.0.2" + version: "8.3.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: @@ -409,50 +404,50 @@ packages: 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: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.5.4" 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: @@ -465,58 +460,58 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c + sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c url: "https://pub.dev" source: hosted - version: "6.9.0" + version: "6.9.5" just_audio: dependency: "direct main" description: name: just_audio - sha256: a49e7120b95600bd357f37a2bb04cd1e88252f7cdea8f3368803779b925b1049 + sha256: f978d5b4ccea08f267dae0232ec5405c1b05d3f3cd63f82097ea46c015d5c09e url: "https://pub.dev" source: hosted - version: "0.9.42" + version: "0.9.46" 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: @@ -529,10 +524,10 @@ packages: 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 +536,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,10 +548,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -577,10 +564,10 @@ packages: 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: @@ -609,34 +596,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 +636,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: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.5.1" path_provider_linux: dependency: transitive description: @@ -689,26 +676,26 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" + sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" url: "https://pub.dev" source: hosted - version: "11.3.1" + version: "11.4.0" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1" + sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc url: "https://pub.dev" source: hosted - version: "12.0.13" + version: "12.1.0" 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 +708,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 +724,10 @@ packages: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "7.0.1" platform: dependency: transitive description: @@ -761,42 +748,42 @@ 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" rxdart: dependency: "direct main" description: @@ -817,26 +804,26 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82" + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.5.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "7f172d1b06de5da47b6264c2692ee2ead20bbbc246690427cdb4fc301cd0c549" + sha256: "46a46fd64659eff15f4638bbe19de43f9483f0e0bf024a9fb6b3582064bacc7b" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.4.17" 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 +844,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 +860,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 +889,58 @@ packages: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "2.0.0" source_helper: dependency: transitive description: name: source_helper - sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca url: "https://pub.dev" source: hosted - version: "1.3.4" + version: "1.3.7" 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: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "1.10.1" 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 +953,66 @@ 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" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.7" timing: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" typed_data: dependency: transitive description: @@ -1046,82 +1025,82 @@ packages: 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: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.4" 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 +1113,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.9.0 <4.0.0" + flutter: ">=3.35.0" From 5607b6a28ddb24abb424a87131d7cafef54a3d13 Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Sat, 29 Nov 2025 23:44:07 +0900 Subject: [PATCH 02/30] Add Japanese and Chinese localizations for app strings - Created `app_localizations_ja.dart` for Japanese translations. - Created `app_localizations_zh.dart` for Chinese translations. - Added corresponding ARB file `app_zh.arb` for Chinese localization. - Updated `l10n.dart` to include localization extension for easy access. --- l10n.yaml | 3 + .../extensions/mark_status_localizations.dart | 19 + lib/common/utils/playlist_localizations.dart | 12 + lib/l10n/app_ja.arb | 186 +++++ lib/l10n/app_localizations.dart | 751 ++++++++++++++++++ lib/l10n/app_localizations_ja.dart | 342 ++++++++ lib/l10n/app_localizations_zh.dart | 342 ++++++++ lib/l10n/app_zh.arb | 186 +++++ lib/l10n/l10n.dart | 6 + lib/main.dart | 13 +- .../viewmodels/detail_viewmodel.dart | 51 +- .../viewmodels/playlists_viewmodel.dart | 12 - .../viewmodels/recommend_viewmodel.dart | 6 +- .../settings/cache_manager_viewmodel.dart | 42 +- .../widgets/auth/login_dialog.dart | 15 +- .../playlists/playlist_works_view.dart | 9 +- .../playlists/playlists_list_view.dart | 13 +- lib/screens/contents/recommend_content.dart | 5 +- lib/screens/detail_screen.dart | 23 +- lib/screens/favorites_screen.dart | 3 +- lib/screens/main_screen.dart | 25 +- lib/screens/player_screen.dart | 8 +- lib/screens/search_screen.dart | 89 ++- .../settings/cache_manager_screen.dart | 31 +- lib/screens/similar_works_screen.dart | 3 +- lib/widgets/detail/mark_selection_dialog.dart | 6 +- .../detail/playlist_selection_dialog.dart | 47 +- lib/widgets/detail/work_action_buttons.dart | 15 +- lib/widgets/detail/work_files_list.dart | 3 +- lib/widgets/detail/work_info_header.dart | 3 +- lib/widgets/drawer_menu.dart | 32 +- lib/widgets/filter/filter_panel.dart | 60 +- lib/widgets/filter/filter_with_keyword.dart | 3 +- .../lyrics/components/player_lyric_view.dart | 5 +- lib/widgets/mini_player/mini_player.dart | 4 +- lib/widgets/player/player_work_info.dart | 5 +- .../work_card/components/work_tags_panel.dart | 3 +- .../work_grid/components/grid_empty.dart | 3 +- .../work_grid/components/grid_error.dart | 3 +- lib/widgets/work_grid_view.dart | 3 +- pubspec.lock | 13 + pubspec.yaml | 4 + 42 files changed, 2203 insertions(+), 204 deletions(-) create mode 100644 l10n.yaml create mode 100644 lib/common/extensions/mark_status_localizations.dart create mode 100644 lib/common/utils/playlist_localizations.dart create mode 100644 lib/l10n/app_ja.arb create mode 100644 lib/l10n/app_localizations.dart create mode 100644 lib/l10n/app_localizations_ja.dart create mode 100644 lib/l10n/app_localizations_zh.dart create mode 100644 lib/l10n/app_zh.arb create mode 100644 lib/l10n/l10n.dart 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/l10n/app_ja.arb b/lib/l10n/app_ja.arb new file mode 100644 index 0000000..f7e4e15 --- /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..15e5a37 --- /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..7b01e41 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:asmrapp/common/constants/strings.dart'; import 'package:asmrapp/presentation/viewmodels/auth_viewmodel.dart'; import 'core/di/service_locator.dart'; import 'package:provider/provider.dart'; @@ -7,6 +6,9 @@ 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'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:asmrapp/l10n/app_localizations.dart'; +import 'package:asmrapp/l10n/l10n.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -34,7 +36,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/viewmodels/detail_viewmodel.dart b/lib/presentation/viewmodels/detail_viewmodel.dart index ce91f49..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; @@ -119,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 { @@ -142,7 +166,11 @@ class DetailViewModel extends ChangeNotifier { if (!_disposed) { AppLogger.error('播放失败', e); } - rethrow; + throw PlaybackException( + PlaybackError.failed, + detail: e.toString(), + originalError: e, + ); } } @@ -196,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()), + ), + ), ); } } @@ -273,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, ), @@ -282,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/playlists_viewmodel.dart b/lib/presentation/viewmodels/playlists_viewmodel.dart index 1cbb10e..44f7d33 100644 --- a/lib/presentation/viewmodels/playlists_viewmodel.dart +++ b/lib/presentation/viewmodels/playlists_viewmodel.dart @@ -130,18 +130,6 @@ 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'); diff --git a/lib/presentation/viewmodels/recommend_viewmodel.dart b/lib/presentation/viewmodels/recommend_viewmodel.dart index 9721900..f470018 100644 --- a/lib/presentation/viewmodels/recommend_viewmodel.dart +++ b/lib/presentation/viewmodels/recommend_viewmodel.dart @@ -19,6 +19,7 @@ class RecommendViewModel extends ChangeNotifier { int _currentPage = 1; bool _hasSubtitle = false; bool _filterPanelExpanded = false; + bool _loginRequired = false; RecommendViewModel(this._authViewModel) : _apiService = GetIt.I() { @@ -59,6 +60,7 @@ class RecommendViewModel extends ChangeNotifier { : null; bool get hasSubtitle => _hasSubtitle; bool get filterPanelExpanded => _filterPanelExpanded; + bool get loginRequired => _loginRequired; Pagination? get pagination => _pagination; @@ -90,13 +92,15 @@ class RecommendViewModel extends ChangeNotifier { // 检查是否已登录 final uuid = _authViewModel.recommenderUuid; if (uuid == null) { - _error = '请先登录'; + _loginRequired = true; + _error = null; notifyListeners(); return; } _isLoading = true; _error = null; + _loginRequired = false; notifyListeners(); try { diff --git a/lib/presentation/viewmodels/settings/cache_manager_viewmodel.dart b/lib/presentation/viewmodels/settings/cache_manager_viewmodel.dart index 02c4df0..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,6 +33,8 @@ class CacheManagerViewModel extends ChangeNotifier { Future loadCacheSize() async { try { _isLoading = true; + _errorDetail = null; + _lastFailedOperation = null; notifyListeners(); // 获取音频缓存大小 @@ -37,10 +42,11 @@ class CacheManagerViewModel extends ChangeNotifier { // 获取字幕缓存大小 _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,6 +99,8 @@ class CacheManagerViewModel extends ChangeNotifier { Future clearAllCache() async { try { _isLoading = true; + _errorDetail = null; + _lastFailedOperation = null; notifyListeners(); await Future.wait([ @@ -95,13 +109,21 @@ class CacheManagerViewModel extends ChangeNotifier { ]); await loadCacheSize(); - _error = null; + _errorDetail = null; } catch (e) { AppLogger.error('清理缓存失败', e); - _error = '清理失败: $e'; + _errorDetail = e.toString(); + _lastFailedOperation = CacheOperation.clearAll; } finally { _isLoading = false; notifyListeners(); } } } + +enum CacheOperation { + load, + clearAudio, + clearSubtitle, + clearAll, +} diff --git a/lib/presentation/widgets/auth/login_dialog.dart b/lib/presentation/widgets/auth/login_dialog.dart index d5d7840..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}); @@ -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,7 +109,7 @@ class _LoginDialogState extends State { strokeWidth: 2, ), ) - : const Text('登录'), + : Text(context.l10n.loginAction), ); }, ), diff --git a/lib/screens/contents/playlists/playlist_works_view.dart b/lib/screens/contents/playlists/playlist_works_view.dart index 3faced6..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, ), ), ], diff --git a/lib/screens/contents/playlists/playlists_list_view.dart b/lib/screens/contents/playlists/playlists_list_view.dart index 3bf297c..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), ); }, diff --git a/lib/screens/contents/recommend_content.dart b/lib/screens/contents/recommend_content.dart index fe0ddc5..307acfc 100644 --- a/lib/screens/contents/recommend_content.dart +++ b/lib/screens/contents/recommend_content.dart @@ -4,6 +4,7 @@ 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}); @@ -57,7 +58,9 @@ class _RecommendContentState extends State 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), diff --git a/lib/screens/detail_screen.dart b/lib/screens/detail_screen.dart index 8fafc28..9471914 100644 --- a/lib/screens/detail_screen.dart +++ b/lib/screens/detail_screen.dart @@ -9,6 +9,7 @@ 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/l10n/l10n.dart'; class DetailScreen extends StatelessWidget { final Work work; @@ -100,7 +101,11 @@ class DetailScreen extends StatelessWidget { } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('播放失败: $e')), + SnackBar( + content: Text( + _playbackErrorMessage(context, e), + ), + ), ); } } @@ -118,4 +123,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 1674577..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( diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index de5c555..7431cf9 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -12,6 +12,7 @@ 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/l10n/l10n.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( @@ -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: '收藏', + label: context.l10n.navigationFavorites, ), NavigationDestination( icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), - label: '主页', + label: context.l10n.navigationHome, ), NavigationDestination( icon: Icon(Icons.recommend_outlined), selectedIcon: Icon(Icons.recommend), - label: '推荐', + label: context.l10n.navigationRecommend, ), NavigationDestination( icon: Icon(Icons.trending_up_outlined), selectedIcon: Icon(Icons.trending_up), - label: '热门', + label: context.l10n.navigationPopularWorks, ), ], ), diff --git a/lib/screens/player_screen.dart b/lib/screens/player_screen.dart index 1f9a3d9..630fa57 100644 --- a/lib/screens/player_screen.dart +++ b/lib/screens/player_screen.dart @@ -9,6 +9,7 @@ import 'package:asmrapp/widgets/player/player_progress.dart'; import 'package:asmrapp/widgets/player/player_work_info.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}); @@ -102,7 +103,8 @@ class _PlayerScreenState extends State { child: Material( color: Colors.transparent, child: Text( - _viewModel.currentTrackInfo?.title ?? '未在播放', + _viewModel.currentTrackInfo?.title ?? + context.l10n.noPlaying, style: Theme.of(context) .textTheme .titleLarge @@ -185,7 +187,9 @@ class _PlayerScreenState extends State { ? Icons.lightbulb : Icons.lightbulb_outline, ), - tooltip: wakeLockController.enabled ? '关闭屏幕常亮' : '开启屏幕常亮', + tooltip: wakeLockController.enabled + ? context.l10n.screenOnDisable + : context.l10n.screenOnEnable, onPressed: () => wakeLockController.toggle(), ); }, diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index e57e6f5..5520201 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -5,6 +5,7 @@ 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'; +import 'package:asmrapp/l10n/l10n.dart'; class SearchScreen extends StatelessWidget { final String? initialKeyword; @@ -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,7 +129,7 @@ class _SearchScreenContentState extends State { child: TextField( controller: _searchController, decoration: InputDecoration( - hintText: '搜索...', + hintText: context.l10n.searchHint, filled: true, fillColor: Theme.of(context) .colorScheme @@ -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 26c8d08..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,48 +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('清理'), + 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('清理'), + 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('清理全部'), + child: Text(context.l10n.cacheClearAll), ), ), const Divider(), // 缓存说明 - const ListTile( - title: Text('缓存说明'), - subtitle: Text('缓存用于存储最近播放的音频文件和字幕文件,以提高再次播放时的加载速度。' - '系统会自动清理过期和超量的缓存。'), + ListTile( + title: Text(context.l10n.cacheInfoTitle), + subtitle: Text(context.l10n.cacheDescription), ), ], ); diff --git a/lib/screens/similar_works_screen.dart b/lib/screens/similar_works_screen.dart index e171bb2..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; @@ -63,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( diff --git a/lib/widgets/detail/mark_selection_dialog.dart b/lib/widgets/detail/mark_selection_dialog.dart index 92f6a22..bbb33a5 100644 --- a/lib/widgets/detail/mark_selection_dialog.dart +++ b/lib/widgets/detail/mark_selection_dialog.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 MarkSelectionDialog extends StatelessWidget { final MarkStatus? currentStatus; @@ -20,7 +22,7 @@ class MarkSelectionDialog extends StatelessWidget { return AlertDialog( backgroundColor: isDark ? const Color(0xFF2C2C2C) : Colors.white, title: Text( - '标记状态', + context.l10n.markStatusTitle, style: TextStyle( color: isDark ? Colors.white70 : Colors.black87, ), @@ -53,7 +55,7 @@ class MarkSelectionDialog extends StatelessWidget { }), ), title: Text( - status.label, + status.localizedLabel(context.l10n), style: TextStyle( color: loading ? (isDark ? Colors.white38 : Colors.black38) diff --git a/lib/widgets/detail/playlist_selection_dialog.dart b/lib/widgets/detail/playlist_selection_dialog.dart index 8e55558..6c7a155 100644 --- a/lib/widgets/detail/playlist_selection_dialog.dart +++ b/lib/widgets/detail/playlist_selection_dialog.dart @@ -1,5 +1,7 @@ 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; @@ -68,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), @@ -99,7 +101,7 @@ class _PlaylistSelectionDialogState extends State { const SizedBox(height: 8), ElevatedButton( onPressed: widget.onRetry, - child: const Text('重试'), + child: Text(context.l10n.retry), ), ], ], @@ -108,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), ); } @@ -147,10 +149,15 @@ class _PlaylistSelectionDialogState extends State { 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, ), @@ -174,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 { @@ -196,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, diff --git a/lib/widgets/detail/work_action_buttons.dart b/lib/widgets/detail/work_action_buttons.dart index 88589f2..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,19 +34,20 @@ 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: 实现评分功能 }, @@ -52,8 +55,10 @@ class WorkActionButtons extends StatelessWidget { _ActionButton( icon: Icons.recommend, label: checkingRecommendations - ? '检查中' - : (hasRecommendations ? '相关推荐' : '暂无推荐'), + ? context.l10n.workActionChecking + : (hasRecommendations + ? context.l10n.workActionRecommend + : context.l10n.workActionNoRecommendation), onTap: hasRecommendations ? onRecommendationsTap : null, loading: checkingRecommendations, ), diff --git a/lib/widgets/detail/work_files_list.dart b/lib/widgets/detail/work_files_list.dart index 1be2d1b..5e900f5 100644 --- a/lib/widgets/detail/work_files_list.dart +++ b/lib/widgets/detail/work_files_list.dart @@ -3,6 +3,7 @@ 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; @@ -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, ), diff --git a/lib/widgets/detail/work_info_header.dart b/lib/widgets/detail/work_info_header.dart index ae9476d..72a7153 100644 --- a/lib/widgets/detail/work_info_header.dart +++ b/lib/widgets/detail/work_info_header.dart @@ -3,6 +3,7 @@ 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:flutter/material.dart'; +import 'package:asmrapp/l10n/l10n.dart'; class WorkInfoHeader extends StatelessWidget { final Work work; @@ -56,7 +57,7 @@ class WorkInfoHeader extends StatelessWidget { ), if (work.hasSubtitle == true) TagChip( - text: '字幕', + text: context.l10n.subtitleTag, backgroundColor: Colors.blue.withValues(alpha: 0.2), textColor: Colors.blue[700], ), diff --git a/lib/widgets/drawer_menu.dart b/lib/widgets/drawer_menu.dart index b561aab..b0c629a 100644 --- a/lib/widgets/drawer_menu.dart +++ b/lib/widgets/drawer_menu.dart @@ -1,4 +1,3 @@ -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/presentation/viewmodels/auth_viewmodel.dart'; @@ -8,6 +7,7 @@ import 'package:asmrapp/screens/settings/cache_manager_screen.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:provider/provider.dart'; +import 'package:asmrapp/l10n/l10n.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( + child: DrawerHeader( decoration: 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); @@ -65,7 +67,7 @@ class DrawerMenu extends StatelessWidget { ), ListTile( leading: const Icon(Icons.favorite), - title: const Text(Strings.favorites), + title: Text(context.l10n.favoritesTitle), onTap: () { Navigator.pop(context); // 检查用户是否已登录 @@ -86,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: 导航到设置页面 @@ -94,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( @@ -113,7 +115,9 @@ class DrawerMenu extends StatelessWidget { builder: (context, themeController, _) { return ListTile( leading: Icon(_getThemeIcon(themeController.themeMode)), - title: Text(_getThemeText(themeController.themeMode)), + title: Text( + _getThemeText(context, themeController.themeMode), + ), onTap: () => themeController.toggleThemeMode(), ); }, @@ -123,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(), ); @@ -146,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 07e3f3a..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; } } @@ -87,7 +88,7 @@ class FilterPanel extends StatelessWidget { ), const SizedBox(width: 8), Text( - '有字幕', + context.l10n.subtitleAvailable, style: TextStyle( color: hasSubtitle ? Theme.of(context).colorScheme.primary @@ -119,23 +120,23 @@ class FilterPanel extends StatelessWidget { 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'), ], ), ), @@ -162,7 +163,9 @@ class FilterPanel extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Text(isDescending ? '降序' : '升序'), + Text(isDescending + ? context.l10n.orderDirectionDesc + : context.l10n.orderDirectionAsc), const SizedBox(width: 4), Icon( isDescending @@ -183,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)), ); } } diff --git a/lib/widgets/filter/filter_with_keyword.dart b/lib/widgets/filter/filter_with_keyword.dart index 170ea33..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; @@ -61,7 +62,7 @@ class FilterWithKeyword extends StatelessWidget { ), const SizedBox(width: 8), Text( - '有字幕', + context.l10n.subtitleAvailable, style: TextStyle( color: hasSubtitle ? colorScheme.primary diff --git a/lib/widgets/lyrics/components/player_lyric_view.dart b/lib/widgets/lyrics/components/player_lyric_view.dart index bead592..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; @@ -95,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), ); } diff --git a/lib/widgets/mini_player/mini_player.dart b/lib/widgets/mini_player/mini_player.dart index 56a6e9a..7e42693 100644 --- a/lib/widgets/mini_player/mini_player.dart +++ b/lib/widgets/mini_player/mini_player.dart @@ -2,6 +2,7 @@ import 'package:asmrapp/presentation/viewmodels/player_viewmodel.dart'; import 'package:asmrapp/screens/player_screen.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; +import 'package:asmrapp/l10n/l10n.dart'; import 'mini_player_controls.dart'; import 'mini_player_cover.dart'; @@ -98,7 +99,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_work_info.dart b/lib/widgets/player/player_work_info.dart index 9efd46a..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/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, ), @@ -46,7 +47,7 @@ class PlayerWorkInfo extends StatelessWidget { ?.map((va) => va['name'] as String?) .where((name) => name != null) .join('、') ?? - '未知演员', + context.l10n.unknownArtist, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context) .colorScheme diff --git a/lib/widgets/work_card/components/work_tags_panel.dart b/lib/widgets/work_card/components/work_tags_panel.dart index 80c32a1..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: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; @@ -61,7 +62,7 @@ class WorkTagsPanel extends StatelessWidget { borderRadius: BorderRadius.circular(4), ), child: Text( - '字幕', + context.l10n.subtitleTag, style: TextStyle( fontSize: 10, color: Colors.blue[700], diff --git a/lib/widgets/work_grid/components/grid_empty.dart b/lib/widgets/work_grid/components/grid_empty.dart index 814fc96..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, ), diff --git a/lib/widgets/work_grid/components/grid_error.dart b/lib/widgets/work_grid/components/grid_error.dart index cc6bd99..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,7 +33,7 @@ class GridError extends StatelessWidget { FilledButton.icon( onPressed: onRetry, icon: const Icon(Icons.refresh), - label: const Text('重试'), + label: Text(context.l10n.retry), ), ], ], 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/pubspec.lock b/pubspec.lock index 30051f4..2526060 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -342,6 +342,11 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -432,6 +437,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.4" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" io: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 710a7d4..67afda5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,9 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter + intl: ^0.20.2 freezed_annotation: ^2.4.1 json_annotation: ^4.9.0 @@ -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: From 8ce61ee3ad5045ac66c5882da48c6685a0cee3a1 Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Sat, 29 Nov 2025 23:54:29 +0900 Subject: [PATCH 03/30] fix: update Japanese localization strings for consistency and accuracy --- lib/l10n/app_ja.arb | 198 ++++++++++++++--------------- lib/l10n/app_localizations_ja.dart | 198 ++++++++++++++--------------- lib/main.dart | 15 ++- 3 files changed, 206 insertions(+), 205 deletions(-) diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index f7e4e15..3d16eb3 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -1,22 +1,22 @@ { "@@locale": "ja", "appName": "asmr.one", - "retry": "重试", - "cancel": "取消", - "confirm": "确认", - "login": "登录", - "favorites": "我的收藏", - "settings": "设置", - "cacheManager": "缓存管理", - "screenAlwaysOn": "屏幕常亮", - "themeSystem": "跟随系统主题", - "themeLight": "浅色模式", - "themeDark": "深色模式", - "navigationFavorites": "收藏", - "navigationHome": "主页", - "navigationForYou": "为你推荐", - "navigationPopularWorks": "热门作品", - "navigationRecommend": "推荐", + "retry": "再試行", + "cancel": "キャンセル", + "confirm": "確認", + "login": "ログイン", + "favorites": "お気に入り", + "settings": "設定", + "cacheManager": "キャッシュ管理", + "screenAlwaysOn": "画面常時オン", + "themeSystem": "システムと同じ", + "themeLight": "ライトモード", + "themeDark": "ダークモード", + "navigationFavorites": "お気に入り", + "navigationHome": "ホーム", + "navigationForYou": "あなた向け", + "navigationPopularWorks": "人気作品", + "navigationRecommend": "おすすめ", "titleWithCount": "{title} ({count})", "@titleWithCount": { "placeholders": { @@ -28,46 +28,46 @@ } } }, - "search": "搜索", - "searchHint": "搜索...", - "searchPromptInitial": "输入关键词开始搜索", - "searchNoResults": "没有找到相关结果", + "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}", + "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": { @@ -75,7 +75,7 @@ } } }, - "playlistRemoveSuccess": "移除成功: {name}", + "playlistRemoveSuccess": "削除しました: {name}", "@playlistRemoveSuccess": { "placeholders": { "name": { @@ -83,9 +83,9 @@ } } }, - "playlistSystemMarked": "我标记的", - "playlistSystemLiked": "我喜欢的", - "playlistWorksCount": "{count} 个作品", + "playlistSystemMarked": "自分のマーク", + "playlistSystemLiked": "いいねした", + "playlistWorksCount": "{count} 件の作品", "@playlistWorksCount": { "placeholders": { "count": { @@ -93,19 +93,19 @@ } } }, - "workActionFavorite": "收藏", - "workActionMark": "标记", - "workActionRate": "评分", - "workActionChecking": "检查中", - "workActionRecommend": "相关推荐", - "workActionNoRecommendation": "暂无推荐", - "markStatusTitle": "标记状态", - "markStatusWantToListen": "想听", - "markStatusListening": "在听", - "markStatusListened": "听过", - "markStatusRelistening": "重听", - "markStatusOnHold": "搁置", - "markUpdated": "已标记为{status}", + "workActionFavorite": "お気に入り", + "workActionMark": "マーク", + "workActionRate": "評価", + "workActionChecking": "確認中", + "workActionRecommend": "おすすめ", + "workActionNoRecommendation": "おすすめはありません", + "markStatusTitle": "マーク状態", + "markStatusWantToListen": "聴きたい", + "markStatusListening": "聴いている", + "markStatusListened": "聴いた", + "markStatusRelistening": "聴き直し", + "markStatusOnHold": "保留", + "markUpdated": "状態を{status}に変更", "@markUpdated": { "placeholders": { "status": { @@ -113,7 +113,7 @@ } } }, - "markFailed": "标记失败: {error}", + "markFailed": "マーク失敗: {error}", "@markFailed": { "placeholders": { "error": { @@ -121,8 +121,8 @@ } } }, - "workFilesTitle": "文件列表", - "playUnsupportedFileType": "不支持的文件类型: {type}", + "workFilesTitle": "ファイル一覧", + "playUnsupportedFileType": "未対応形式: {type}", "@playUnsupportedFileType": { "placeholders": { "type": { @@ -130,9 +130,9 @@ } } }, - "playUrlMissing": "无法播放:文件URL不存在", - "playFilesNotLoaded": "文件列表未加载", - "playFailed": "播放失败: {error}", + "playUrlMissing": "再生できません: URLがありません", + "playFilesNotLoaded": "ファイル一覧未読み込み", + "playFailed": "再生失敗: {error}", "@playFailed": { "placeholders": { "error": { @@ -140,7 +140,7 @@ } } }, - "operationFailed": "操作失败: {error}", + "operationFailed": "操作失敗: {error}", "@operationFailed": { "placeholders": { "error": { @@ -148,15 +148,15 @@ } } }, - "cacheManagerTitle": "缓存管理", - "cacheAudio": "音频缓存", - "cacheSubtitle": "字幕缓存", - "cacheTotal": "总缓存大小", - "cacheClear": "清理", - "cacheClearAll": "清理全部", - "cacheInfoTitle": "缓存说明", - "cacheDescription": "缓存用于存储最近播放的音频文件和字幕文件,以提高再次播放时的加载速度。系统会自动清理过期和超量的缓存。", - "cacheLoadFailed": "加载失败: {error}", + "cacheManagerTitle": "キャッシュ管理", + "cacheAudio": "音声キャッシュ", + "cacheSubtitle": "字幕キャッシュ", + "cacheTotal": "キャッシュ総量", + "cacheClear": "削除", + "cacheClearAll": "全て削除", + "cacheInfoTitle": "キャッシュについて", + "cacheDescription": "キャッシュは直近の音声と字幕を保存し、次回再生を速くします。期限切れや容量超過は自動で整理されます。", + "cacheLoadFailed": "読み込み失敗: {error}", "@cacheLoadFailed": { "placeholders": { "error": { @@ -164,7 +164,7 @@ } } }, - "cacheClearFailed": "清理失败: {error}", + "cacheClearFailed": "削除失敗: {error}", "@cacheClearFailed": { "placeholders": { "error": { @@ -173,14 +173,14 @@ } }, "subtitleTag": "字幕", - "noPlaying": "未在播放", - "screenOnDisable": "关闭屏幕常亮", - "screenOnEnable": "开启屏幕常亮", - "unknownWorkTitle": "未知作品", - "unknownArtist": "未知演员", - "lyricsEmpty": "无歌词", - "loginTitle": "登录", - "loginUsernameLabel": "用户名", - "loginPasswordLabel": "密码", - "loginAction": "登录" + "noPlaying": "再生中なし", + "screenOnDisable": "画面常時オンをオフ", + "screenOnEnable": "画面常時オンをオン", + "unknownWorkTitle": "不明な作品", + "unknownArtist": "不明な出演者", + "lyricsEmpty": "歌詞なし", + "loginTitle": "ログイン", + "loginUsernameLabel": "ユーザー名", + "loginPasswordLabel": "パスワード", + "loginAction": "ログイン" } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 15e5a37..21e3fbe 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -12,52 +12,52 @@ class AppLocalizationsJa extends AppLocalizations { String get appName => 'asmr.one'; @override - String get retry => '重试'; + String get retry => '再試行'; @override - String get cancel => '取消'; + String get cancel => 'キャンセル'; @override - String get confirm => '确认'; + String get confirm => '確認'; @override - String get login => '登录'; + String get login => 'ログイン'; @override - String get favorites => '我的收藏'; + String get favorites => 'お気に入り'; @override - String get settings => '设置'; + String get settings => '設定'; @override - String get cacheManager => '缓存管理'; + String get cacheManager => 'キャッシュ管理'; @override - String get screenAlwaysOn => '屏幕常亮'; + String get screenAlwaysOn => '画面常時オン'; @override - String get themeSystem => '跟随系统主题'; + String get themeSystem => 'システムと同じ'; @override - String get themeLight => '浅色模式'; + String get themeLight => 'ライトモード'; @override - String get themeDark => '深色模式'; + String get themeDark => 'ダークモード'; @override - String get navigationFavorites => '收藏'; + String get navigationFavorites => 'お気に入り'; @override - String get navigationHome => '主页'; + String get navigationHome => 'ホーム'; @override - String get navigationForYou => '为你推荐'; + String get navigationForYou => 'あなた向け'; @override - String get navigationPopularWorks => '热门作品'; + String get navigationPopularWorks => '人気作品'; @override - String get navigationRecommend => '推荐'; + String get navigationRecommend => 'おすすめ'; @override String titleWithCount(String title, int count) { @@ -65,278 +65,278 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get search => '搜索'; + String get search => '検索'; @override - String get searchHint => '搜索...'; + String get searchHint => '検索...'; @override - String get searchPromptInitial => '输入关键词开始搜索'; + String get searchPromptInitial => 'キーワードを入力して検索'; @override - String get searchNoResults => '没有找到相关结果'; + String get searchNoResults => '該当する結果がありません'; @override String get subtitle => '字幕'; @override - String get subtitleAvailable => '有字幕'; + String get subtitleAvailable => '字幕あり'; @override - String get orderFieldCollectionTime => '收录时间'; + String get orderFieldCollectionTime => '収録日時'; @override - String get orderFieldReleaseDate => '发售日期'; + String get orderFieldReleaseDate => '発売日'; @override - String get orderFieldSales => '销量'; + String get orderFieldSales => '売上'; @override - String get orderFieldPrice => '价格'; + String get orderFieldPrice => '価格'; @override - String get orderFieldRating => '评价'; + String get orderFieldRating => '評価'; @override - String get orderFieldReviewCount => '评论数量'; + String get orderFieldReviewCount => 'レビュー数'; @override - String get orderFieldId => 'RJ号'; + String get orderFieldId => 'RJ番号'; @override - String get orderFieldMyRating => '我的评价'; + String get orderFieldMyRating => '自分の評価'; @override - String get orderFieldAllAges => '全年龄'; + String get orderFieldAllAges => '全年齢'; @override - String get orderFieldRandom => '随机'; + String get orderFieldRandom => 'ランダム'; @override - String get orderLabel => '排序'; + String get orderLabel => '並び替え'; @override - String get orderDirectionDesc => '降序'; + String get orderDirectionDesc => '降順'; @override - String get orderDirectionAsc => '升序'; + String get orderDirectionAsc => '昇順'; @override - String get searchOrderNewest => '最新收录'; + String get searchOrderNewest => '最新収録'; @override - String get searchOrderOldest => '最早收录'; + String get searchOrderOldest => '最古の収録'; @override - String get searchOrderReleaseDesc => '发售日期倒序'; + String get searchOrderReleaseDesc => '発売日(新しい順)'; @override - String get searchOrderReleaseAsc => '发售日期顺序'; + String get searchOrderReleaseAsc => '発売日(古い順)'; @override - String get searchOrderSalesDesc => '销量倒序'; + String get searchOrderSalesDesc => '売上(高い順)'; @override - String get searchOrderSalesAsc => '销量顺序'; + String get searchOrderSalesAsc => '売上(低い順)'; @override - String get searchOrderPriceDesc => '价格倒序'; + String get searchOrderPriceDesc => '価格(高い順)'; @override - String get searchOrderPriceAsc => '价格顺序'; + String get searchOrderPriceAsc => '価格(低い順)'; @override - String get searchOrderRatingDesc => '评价倒序'; + String get searchOrderRatingDesc => '評価(高い順)'; @override - String get searchOrderReviewCountDesc => '评论数量倒序'; + String get searchOrderReviewCountDesc => 'レビュー数(多い順)'; @override - String get searchOrderIdDesc => 'RJ号倒序'; + String get searchOrderIdDesc => 'RJ番号(大きい順)'; @override - String get searchOrderIdAsc => 'RJ号顺序'; + String get searchOrderIdAsc => 'RJ番号(小さい順)'; @override - String get searchOrderRandom => '随机排序'; + String get searchOrderRandom => 'ランダム順'; @override - String get favoritesTitle => '我的收藏'; + String get favoritesTitle => 'お気に入り'; @override - String get pleaseLogin => '请先登录'; + String get pleaseLogin => '先にログインしてください'; @override - String get emptyContent => '暂无内容'; + String get emptyContent => '内容がありません'; @override - String get emptyWorks => '暂无作品'; + String get emptyWorks => '作品がありません'; @override - String get similarWorksTitle => '相关推荐'; + String get similarWorksTitle => '関連作品'; @override - String get playlistAddToFavorites => '添加到收藏夹'; + String get playlistAddToFavorites => 'お気に入りに追加'; @override - String get playlistEmpty => '暂无收藏夹'; + String get playlistEmpty => 'リストがありません'; @override String playlistAddSuccess(String name) { - return '添加成功: $name'; + return '追加しました: $name'; } @override String playlistRemoveSuccess(String name) { - return '移除成功: $name'; + return '削除しました: $name'; } @override - String get playlistSystemMarked => '我标记的'; + String get playlistSystemMarked => '自分のマーク'; @override - String get playlistSystemLiked => '我喜欢的'; + String get playlistSystemLiked => 'いいねした'; @override String playlistWorksCount(int count) { - return '$count 个作品'; + return '$count 件の作品'; } @override - String get workActionFavorite => '收藏'; + String get workActionFavorite => 'お気に入り'; @override - String get workActionMark => '标记'; + String get workActionMark => 'マーク'; @override - String get workActionRate => '评分'; + String get workActionRate => '評価'; @override - String get workActionChecking => '检查中'; + String get workActionChecking => '確認中'; @override - String get workActionRecommend => '相关推荐'; + String get workActionRecommend => 'おすすめ'; @override - String get workActionNoRecommendation => '暂无推荐'; + String get workActionNoRecommendation => 'おすすめはありません'; @override - String get markStatusTitle => '标记状态'; + String get markStatusTitle => 'マーク状態'; @override - String get markStatusWantToListen => '想听'; + String get markStatusWantToListen => '聴きたい'; @override - String get markStatusListening => '在听'; + String get markStatusListening => '聴いている'; @override - String get markStatusListened => '听过'; + String get markStatusListened => '聴いた'; @override - String get markStatusRelistening => '重听'; + String get markStatusRelistening => '聴き直し'; @override - String get markStatusOnHold => '搁置'; + String get markStatusOnHold => '保留'; @override String markUpdated(String status) { - return '已标记为$status'; + return '状態を$statusに変更'; } @override String markFailed(String error) { - return '标记失败: $error'; + return 'マーク失敗: $error'; } @override - String get workFilesTitle => '文件列表'; + String get workFilesTitle => 'ファイル一覧'; @override String playUnsupportedFileType(String type) { - return '不支持的文件类型: $type'; + return '未対応形式: $type'; } @override - String get playUrlMissing => '无法播放:文件URL不存在'; + String get playUrlMissing => '再生できません: URLがありません'; @override - String get playFilesNotLoaded => '文件列表未加载'; + String get playFilesNotLoaded => 'ファイル一覧未読み込み'; @override String playFailed(String error) { - return '播放失败: $error'; + return '再生失敗: $error'; } @override String operationFailed(String error) { - return '操作失败: $error'; + return '操作失敗: $error'; } @override - String get cacheManagerTitle => '缓存管理'; + String get cacheManagerTitle => 'キャッシュ管理'; @override - String get cacheAudio => '音频缓存'; + String get cacheAudio => '音声キャッシュ'; @override - String get cacheSubtitle => '字幕缓存'; + String get cacheSubtitle => '字幕キャッシュ'; @override - String get cacheTotal => '总缓存大小'; + String get cacheTotal => 'キャッシュ総量'; @override - String get cacheClear => '清理'; + String get cacheClear => '削除'; @override - String get cacheClearAll => '清理全部'; + String get cacheClearAll => '全て削除'; @override - String get cacheInfoTitle => '缓存说明'; + String get cacheInfoTitle => 'キャッシュについて'; @override String get cacheDescription => - '缓存用于存储最近播放的音频文件和字幕文件,以提高再次播放时的加载速度。系统会自动清理过期和超量的缓存。'; + 'キャッシュは直近の音声と字幕を保存し、次回再生を速くします。期限切れや容量超過は自動で整理されます。'; @override String cacheLoadFailed(String error) { - return '加载失败: $error'; + return '読み込み失敗: $error'; } @override String cacheClearFailed(String error) { - return '清理失败: $error'; + return '削除失敗: $error'; } @override String get subtitleTag => '字幕'; @override - String get noPlaying => '未在播放'; + String get noPlaying => '再生中なし'; @override - String get screenOnDisable => '关闭屏幕常亮'; + String get screenOnDisable => '画面常時オンをオフ'; @override - String get screenOnEnable => '开启屏幕常亮'; + String get screenOnEnable => '画面常時オンをオン'; @override - String get unknownWorkTitle => '未知作品'; + String get unknownWorkTitle => '不明な作品'; @override - String get unknownArtist => '未知演员'; + String get unknownArtist => '不明な出演者'; @override - String get lyricsEmpty => '无歌词'; + String get lyricsEmpty => '歌詞なし'; @override - String get loginTitle => '登录'; + String get loginTitle => 'ログイン'; @override - String get loginUsernameLabel => '用户名'; + String get loginUsernameLabel => 'ユーザー名'; @override - String get loginPasswordLabel => '密码'; + String get loginPasswordLabel => 'パスワード'; @override - String get loginAction => '登录'; + String get loginAction => 'ログイン'; } diff --git a/lib/main.dart b/lib/main.dart index 7b01e41..3f0cbd3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,14 +1,15 @@ -import 'package:flutter/material.dart'; -import 'package:asmrapp/presentation/viewmodels/auth_viewmodel.dart'; -import 'core/di/service_locator.dart'; -import 'package:provider/provider.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'; -import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:asmrapp/l10n/app_localizations.dart'; import 'package:asmrapp/l10n/l10n.dart'; +import 'package:asmrapp/presentation/viewmodels/auth_viewmodel.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 'screens/search_screen.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); From e57a01a7e1fdfbdc4246e0b439d894fc67466f33 Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Sun, 30 Nov 2025 16:50:25 +0900 Subject: [PATCH 04/30] fix: reorder imports and enhance MiniPlayer for safe area handling --- lib/screens/detail_screen.dart | 20 +++++++++-------- lib/screens/main_screen.dart | 20 ++++++++--------- lib/screens/search_screen.dart | 2 +- lib/utils/logger.dart | 2 +- lib/widgets/detail/mark_selection_dialog.dart | 8 +++---- lib/widgets/drawer_menu.dart | 4 ++-- lib/widgets/mini_player/mini_player.dart | 22 +++++++++++++++++-- 7 files changed, 49 insertions(+), 29 deletions(-) diff --git a/lib/screens/detail_screen.dart b/lib/screens/detail_screen.dart index 9471914..555670b 100644 --- a/lib/screens/detail_screen.dart +++ b/lib/screens/detail_screen.dart @@ -1,15 +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/l10n/l10n.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; @@ -32,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: [ diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 7431cf9..17fcfc1 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -1,3 +1,4 @@ +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'; @@ -12,7 +13,6 @@ 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/l10n/l10n.dart'; /// MainScreen 是应用的主界面,负责管理底部导航栏和对应的内容页面。 /// 它采用了集中式的状态管理架构,所有子页面的 ViewModel 都在这里初始化和提供。 @@ -161,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, @@ -171,23 +171,23 @@ class _MainScreenState extends State { onDestinationSelected: _onTabTapped, destinations: [ NavigationDestination( - icon: Icon(Icons.favorite_outline), - selectedIcon: Icon(Icons.favorite), + 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), + 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), + 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), + icon: const Icon(Icons.trending_up_outlined), + selectedIcon: const Icon(Icons.trending_up), label: context.l10n.navigationPopularWorks, ), ], diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index 5520201..6c5c628 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -1,3 +1,4 @@ +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'; @@ -5,7 +6,6 @@ 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'; -import 'package:asmrapp/l10n/l10n.dart'; class SearchScreen extends StatelessWidget { final String? initialKeyword; 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/detail/mark_selection_dialog.dart b/lib/widgets/detail/mark_selection_dialog.dart index bbb33a5..d03246a 100644 --- a/lib/widgets/detail/mark_selection_dialog.dart +++ b/lib/widgets/detail/mark_selection_dialog.dart @@ -1,7 +1,7 @@ +import 'package:asmrapp/common/extensions/mark_status_localizations.dart'; 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'; +import 'package:flutter/material.dart'; class MarkSelectionDialog extends StatelessWidget { final MarkStatus? currentStatus; @@ -71,8 +71,8 @@ class MarkSelectionDialog extends StatelessWidget { Navigator.of(context).pop(); }, hoverColor: isDark - ? Colors.white.withOpacity(0.05) - : Colors.black.withOpacity(0.05), + ? Colors.white.withValues(alpha: 0.05) + : Colors.black.withValues(alpha: 0.05), ); }).toList(), ), diff --git a/lib/widgets/drawer_menu.dart b/lib/widgets/drawer_menu.dart index b0c629a..b56ce0e 100644 --- a/lib/widgets/drawer_menu.dart +++ b/lib/widgets/drawer_menu.dart @@ -1,5 +1,6 @@ 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'; @@ -7,7 +8,6 @@ import 'package:asmrapp/screens/settings/cache_manager_screen.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:provider/provider.dart'; -import 'package:asmrapp/l10n/l10n.dart'; class DrawerMenu extends StatelessWidget { const DrawerMenu({super.key}); @@ -33,7 +33,7 @@ class DrawerMenu extends StatelessWidget { dividerTheme: const DividerThemeData(color: Colors.transparent), ), child: DrawerHeader( - decoration: BoxDecoration( + decoration: const BoxDecoration( color: Colors.deepPurple, ), child: Text( diff --git a/lib/widgets/mini_player/mini_player.dart b/lib/widgets/mini_player/mini_player.dart index 7e42693..f9b6f2f 100644 --- a/lib/widgets/mini_player/mini_player.dart +++ b/lib/widgets/mini_player/mini_player.dart @@ -11,11 +11,28 @@ 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, _) { @@ -65,7 +82,8 @@ 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: [ From 20f5f6562957d86a45de3200a096af78f0ae3cee Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Wed, 18 Feb 2026 18:10:31 +0900 Subject: [PATCH 05/30] fix: update dependencies and add .gitignore entries for local configuration --- .gitignore | 3 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 - pubspec.lock | 248 ++++++++++-------- pubspec.yaml | 16 +- 4 files changed, 151 insertions(+), 118 deletions(-) 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/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 2526060..5f26ea4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f + sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" url: "https://pub.dev" source: hosted - version: "85.0.0" + version: "93.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d" + sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b url: "https://pub.dev" source: hosted - version: "7.7.1" + version: "10.0.1" archive: dependency: transitive description: name: archive - sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff url: "https://pub.dev" source: hosted - version: "4.0.7" + version: "4.0.9" args: dependency: transitive description: @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: audio_session - sha256: "2b7fff16a552486d078bfc09a8cde19f426dc6d6329262b684182597bec5b1ac" + sha256: "8f96a7fecbb718cb093070f868b4cdcb8a9b1053dce342ff8ab2fde10eb9afb7" url: "https://pub.dev" source: hosted - version: "0.1.25" + version: "0.2.2" boolean_selector: dependency: transitive description: @@ -85,18 +85,18 @@ packages: dependency: transitive description: name: build - sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" + sha256: "275bf6bb2a00a9852c28d4e0b410da1d833a734d57d39d44f94bfc895a484ec3" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "4.0.4" build_config: dependency: transitive description: name: build_config - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.0" build_daemon: dependency: transitive description: @@ -105,30 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.1" - build_resolvers: - dependency: transitive - description: - name: build_resolvers - sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 - url: "https://pub.dev" - source: hosted - version: "2.5.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" + sha256: "39ad4ca8a2876779737c60e4228b4bcd35d4352ef7e14e47514093edc012c734" url: "https://pub.dev" source: hosted - version: "2.5.4" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" - url: "https://pub.dev" - source: hosted - version: "9.1.2" + version: "2.11.1" built_collection: dependency: transitive description: @@ -141,10 +125,10 @@ packages: dependency: transitive description: name: built_value - sha256: "426cf75afdb23aa74bd4e471704de3f9393f3c7b04c1e2d9c6f1073ae0b8b139" + sha256: "7931c90b84bc573fef103548e354258ae4c9d28d140e41961df6843c5d60d4d8" url: "https://pub.dev" source: hosted - version: "8.12.1" + version: "8.12.3" cached_network_image: dependency: "direct main" description: @@ -173,10 +157,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -201,14 +185,22 @@ packages: url: "https://pub.dev" source: hosted 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: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" url: "https://pub.dev" source: hosted - version: "4.11.0" + version: "4.11.1" collection: dependency: transitive description: @@ -245,26 +237,26 @@ packages: dependency: transitive description: name: dart_style - sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb" + sha256: "15a7db352c8fc6a4d2bc475ba901c25b39fe7157541da4c16eacce6f8be83e49" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.5" dbus: dependency: transitive description: name: dbus - sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.12" dio: dependency: "direct main" description: name: dio - sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + sha256: b9d46faecab38fc8cc286f80bc4d61a3bb5d4ac49e51ed877b4d6706efe57b25 url: "https://pub.dev" source: hosted - version: "5.9.0" + version: "5.9.1" dio_web_adapter: dependency: transitive description: @@ -293,10 +285,10 @@ packages: dependency: transitive description: name: ffi - sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" file: dependency: transitive description: @@ -330,18 +322,18 @@ 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 @@ -361,34 +353,26 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c" + sha256: f23ea33b3863f119b58ed1b586e881a46bd28715ddcc4dbc33104524e3434131 url: "https://pub.dev" source: hosted - version: "2.5.8" + 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: ae78de7c3f2304b8d81f2bb6e320833e5e81de942188542328f074978cc0efa9 + sha256: "1d648d2dd2047d7f7450d5727ca24ee435f240385753d90b49650e3cdff32e56" url: "https://pub.dev" source: hosted - version: "8.3.0" + version: "9.2.0" glob: dependency: transitive description: @@ -405,6 +389,14 @@ 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: @@ -433,10 +425,10 @@ packages: dependency: transitive description: name: image - sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce url: "https://pub.dev" source: hosted - version: "4.5.4" + version: "4.8.0" intl: dependency: "direct main" description: @@ -465,26 +457,34 @@ packages: dependency: "direct main" description: name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df" url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "4.10.0" + json_schema: + dependency: transitive + description: + name: json_schema + sha256: f37d9c3fdfe8c9aae55fdfd5af815d24ce63c3a0f6a2c1f0982c30f43643fa1a + url: "https://pub.dev" + source: hosted + version: "5.2.2" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c + sha256: "93fba3ad139dab2b1ce59ecc6fdce6da46a42cdb6c4399ecda30f1e7e725760d" url: "https://pub.dev" source: hosted - version: "6.9.5" + version: "6.12.0" just_audio: dependency: "direct main" description: name: just_audio - sha256: f978d5b4ccea08f267dae0232ec5405c1b05d3f3cd63f82097ea46c015d5c09e + sha256: "9694e4734f515f2a052493d1d7e0d6de219ee0427c7c29492e246ff32a219908" url: "https://pub.dev" source: hosted - version: "0.9.46" + version: "0.10.5" just_audio_platform_interface: dependency: transitive description: @@ -529,10 +529,10 @@ packages: 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: @@ -561,18 +561,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + 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: @@ -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: @@ -657,10 +673,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "2.6.0" path_provider_linux: dependency: transitive description: @@ -689,18 +705,18 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 url: "https://pub.dev" source: hosted - version: "11.4.0" + version: "12.0.1" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" url: "https://pub.dev" source: hosted - version: "12.1.0" + version: "13.0.1" permission_handler_apple: dependency: transitive description: @@ -737,10 +753,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.2" platform: dependency: transitive description: @@ -797,6 +813,22 @@ packages: url: "https://pub.dev" source: hosted 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,18 +849,18 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "46a46fd64659eff15f4638bbe19de43f9483f0e0bf024a9fb6b3582064bacc7b" + sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f url: "https://pub.dev" source: hosted - version: "2.4.17" + version: "2.4.20" shared_preferences_foundation: dependency: transitive description: @@ -902,26 +934,26 @@ packages: dependency: transitive description: name: source_gen - sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" + sha256: "1d562a3c1f713904ebbed50d2760217fd8a51ca170ac4b05b0db490699dbac17" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "4.2.0" source_helper: dependency: transitive description: name: source_helper - sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca + sha256: "4a85e90b50694e652075cbe4575665539d253e6ec10e46e76b45368ab5e3caae" url: "https://pub.dev" source: hosted - version: "1.3.7" + version: "1.3.10" source_span: dependency: transitive description: name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" url: "https://pub.dev" source: hosted - version: "1.10.1" + version: "1.10.2" sqflite: dependency: transitive description: @@ -1014,18 +1046,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 - url: "https://pub.dev" - source: hosted - version: "0.7.7" - timing: - dependency: transitive - description: - name: timing - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "0.7.9" typed_data: dependency: transitive description: @@ -1034,6 +1058,14 @@ 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: @@ -1078,10 +1110,10 @@ packages: dependency: transitive description: name: watcher - sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.2.1" web: dependency: transitive description: @@ -1139,5 +1171,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.9.0 <4.0.0" - flutter: ">=3.35.0" + dart: ">=3.10.3 <4.0.0" + flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index 67afda5..3233885 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,7 +33,7 @@ dependencies: flutter_localizations: sdk: flutter intl: ^0.20.2 - freezed_annotation: ^2.4.1 + freezed_annotation: ^3.1.0 json_annotation: ^4.9.0 # The following adds the Cupertino Icons font to your application. @@ -44,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 @@ -62,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 From 590fb55d0f99284191ee838d7091d48f5d0b368c Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Wed, 18 Feb 2026 20:59:13 +0900 Subject: [PATCH 06/30] chore: update Gradle and Kotlin versions, add just_audio_media_kit and media_kit dependencies, and enhance audio player service initialization --- android/app/build.gradle | 6 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- android/settings.gradle | 4 +- lib/core/audio/audio_player_service.dart | 89 +++++++++++++++---- lib/main.dart | 6 ++ linux/CMakeLists.txt | 5 ++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + pubspec.lock | 64 ++++++++++++- pubspec.yaml | 3 + test/widget_test.dart | 3 +- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 13 files changed, 161 insertions(+), 30 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index d2a7e5b..e449cd0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -17,12 +17,12 @@ android { ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8 + jvmTarget = JavaVersion.VERSION_17.toString() } defaultConfig { diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 7bb2df6..e4ef43f 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index b9e43bd..ff03f22 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.1.0" apply false - id "org.jetbrains.kotlin.android" version "1.8.22" apply false + id "com.android.application" version "8.11.1" apply false + id "org.jetbrains.kotlin.android" version "2.2.20" apply false } include ":app" diff --git a/lib/core/audio/audio_player_service.dart b/lib/core/audio/audio_player_service.dart index e8411ad..b4f61ad 100644 --- a/lib/core/audio/audio_player_service.dart +++ b/lib/core/audio/audio_player_service.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:asmrapp/utils/logger.dart'; import 'package:just_audio/just_audio.dart'; import 'package:audio_session/audio_session.dart'; @@ -14,10 +15,11 @@ import './events/playback_event_hub.dart'; class AudioPlayerService implements IAudioPlayerService { late final AudioPlayer _player; - late final AudioNotificationService _notificationService; + AudioNotificationService? _notificationService; late final ConcatenatingAudioSource _playlist; late final PlaybackStateManager _stateManager; late final PlaybackController _playbackController; + late final Future _initialization; final PlaybackEventHub _eventHub; final IPlaybackStateRepository _stateRepository; @@ -26,7 +28,7 @@ class AudioPlayerService implements IAudioPlayerService { required IPlaybackStateRepository stateRepository, }) : _eventHub = eventHub, _stateRepository = stateRepository { - _init(); + _initialization = _init(); } static AudioPlayerService? _instance; @@ -42,13 +44,23 @@ class AudioPlayerService implements IAudioPlayerService { return _instance!; } + bool get _supportsAudioSession => + Platform.isAndroid || Platform.isIOS || Platform.isMacOS; + + bool get _supportsNotificationService => + Platform.isAndroid || Platform.isIOS || Platform.isMacOS; + + Future _ensureInitialized() => _initialization; + Future _init() async { try { _player = AudioPlayer(); - _notificationService = AudioNotificationService( - _player, - _eventHub, - ); + if (_supportsNotificationService) { + _notificationService = AudioNotificationService( + _player, + _eventHub, + ); + } _playlist = ConcatenatingAudioSource(children: []); _stateManager = PlaybackStateManager( @@ -63,12 +75,27 @@ class AudioPlayerService implements IAudioPlayerService { playlist: _playlist, ); - final session = await AudioSession.instance; - await session.configure(const AudioSessionConfiguration.music()); - await _notificationService.init(); + if (_supportsAudioSession) { + try { + final session = await AudioSession.instance; + await session.configure(const AudioSessionConfiguration.music()); + } catch (e, stack) { + AppLogger.warning('音频会话初始化失败,将继续无会话模式运行'); + AppLogger.error('音频会话初始化失败', e, stack); + } + } + + if (_notificationService != null) { + try { + await _notificationService!.init(); + } catch (e, stack) { + AppLogger.warning('通知服务初始化失败,将继续无通知模式运行'); + AppLogger.error('通知服务初始化失败', e, stack); + } + } _stateManager.initStateListeners(); - await restorePlaybackState(); + await _restorePlaybackStateInternal(); } catch (e, stack) { AudioErrorHandler.handleError( AudioErrorType.init, @@ -86,29 +113,46 @@ class AudioPlayerService implements IAudioPlayerService { // 基础播放控制 @override - Future pause() => _playbackController.pause(); + Future pause() async { + await _ensureInitialized(); + await _playbackController.pause(); + } @override - Future resume() => _playbackController.play(); + Future resume() async { + await _ensureInitialized(); + await _playbackController.play(); + } @override Future stop() async { + await _ensureInitialized(); await _playbackController.stop(); _stateManager.clearState(); } @override - Future seek(Duration position) => _playbackController.seek(position); + Future seek(Duration position) async { + await _ensureInitialized(); + await _playbackController.seek(position); + } @override - Future previous() => _playbackController.previous(); + Future previous() async { + await _ensureInitialized(); + await _playbackController.previous(); + } @override - Future next() => _playbackController.next(); + Future next() async { + await _ensureInitialized(); + await _playbackController.next(); + } // 上下文管理 @override Future playWithContext(PlaybackContext context) async { + await _ensureInitialized(); await _playbackController.setPlaybackContext(context); // 添加自动播放 await resume(); @@ -123,10 +167,18 @@ class AudioPlayerService implements IAudioPlayerService { // 状态持久化 @override - Future savePlaybackState() => _stateManager.saveState(); + Future savePlaybackState() async { + await _ensureInitialized(); + await _stateManager.saveState(); + } @override Future restorePlaybackState() async { + await _ensureInitialized(); + await _restorePlaybackStateInternal(); + } + + Future _restorePlaybackStateInternal() async { try { AppLogger.debug('开始恢复播放状态'); final state = await _stateManager.loadState(); @@ -174,7 +226,8 @@ class AudioPlayerService implements IAudioPlayerService { @override Future dispose() async { - _player.dispose(); - _notificationService.dispose(); + await _ensureInitialized(); + await _player.dispose(); + await _notificationService?.dispose(); } } diff --git a/lib/main.dart b/lib/main.dart index 3f0cbd3..27e71e8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:asmrapp/l10n/l10n.dart'; import 'package:asmrapp/presentation/viewmodels/auth_viewmodel.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:just_audio_media_kit/just_audio_media_kit.dart'; import 'package:provider/provider.dart'; import 'core/di/service_locator.dart'; @@ -13,6 +14,11 @@ import 'screens/search_screen.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + JustAudioMediaKit.ensureInitialized( + android: false, + iOS: false, + macOS: false, + ); // 初始化服务定位器 await setupServiceLocator(); diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 76c5153..af97a5a 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -91,6 +91,11 @@ set_target_properties(${BINARY_NAME} # them to the application. include(flutter/generated_plugins.cmake) +# Use mimalloc provided by media_kit when available to reduce allocator issues. +if(MIMALLOC_LIB) + target_link_libraries(${BINARY_NAME} PRIVATE ${MIMALLOC_LIB}) +endif() + # === Installation === # By default, "installing" just makes a relocatable bundle in the build diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..d2d6405 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); + media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..2333885 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + media_kit_libs_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/pubspec.lock b/pubspec.lock index 5f26ea4..280225d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -485,6 +485,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.10.5" + just_audio_media_kit: + dependency: "direct main" + description: + name: just_audio_media_kit + sha256: f3cf04c3a50339709e87e90b4e841eef4364ab4be2bdbac0c54cc48679f84d23 + url: "https://pub.dev" + source: hosted + version: "2.1.0" just_audio_platform_interface: dependency: transitive description: @@ -573,6 +581,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.0" + media_kit: + dependency: transitive + description: + name: media_kit + sha256: ae9e79597500c7ad6083a3c7b7b7544ddabfceacce7ae5c9709b0ec16a5d6643 + url: "https://pub.dev" + source: hosted + version: "1.2.6" + media_kit_libs_linux: + dependency: "direct main" + description: + name: media_kit_libs_linux + sha256: "2b473399a49ec94452c4d4ae51cfc0f6585074398d74216092bf3d54aac37ecf" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + media_kit_libs_windows_audio: + dependency: "direct main" + description: + name: media_kit_libs_windows_audio + sha256: c2fd558cc87b9d89a801141fcdffe02e338a3b21a41a18fbd63d5b221a1b8e53 + url: "https://pub.dev" + source: hosted + version: "1.0.9" meta: dependency: transitive description: @@ -837,6 +869,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + safe_local_storage: + dependency: transitive + description: + name: safe_local_storage + sha256: "287ea1f667c0b93cdc127dccc707158e2d81ee59fba0459c31a0c7da4d09c755" + url: "https://pub.dev" + source: hosted + version: "2.0.3" scrollable_positioned_list: dependency: "direct main" description: @@ -1046,10 +1086,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.8" typed_data: dependency: transitive description: @@ -1058,6 +1098,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" uri: dependency: transitive description: @@ -1066,6 +1114,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + uri_parser: + dependency: transitive + description: + name: uri_parser + sha256: "051c62e5f693de98ca9f130ee707f8916e2266945565926be3ff20659f7853ce" + url: "https://pub.dev" + source: hosted + version: "3.0.2" uuid: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3233885..e6ffcd2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,6 +45,9 @@ dependencies: logger: ^2.5.0 shimmer: ^3.0.0 just_audio: ^0.10.5 + just_audio_media_kit: ^2.1.0 + media_kit_libs_linux: ^1.2.0 + media_kit_libs_windows_audio: ^1.0.9 audio_session: ^0.2.2 get_it: ^9.2.0 audio_service: ^0.18.12 diff --git a/test/widget_test.dart b/test/widget_test.dart index d2f885a..f76a716 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -5,11 +5,10 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. +import 'package:asmrapp/main.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:asmrapp/main.dart'; - void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 48de52b..773a900 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,12 @@ #include "generated_plugin_registrant.h" +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("MediaKitLibsWindowsAudioPluginCApi")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 0e69e40..6cff779 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + media_kit_libs_windows_audio permission_handler_windows ) From a1f51aeb660e19b69d6aa21d03b175d2b40ab062 Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Wed, 18 Feb 2026 22:54:30 +0900 Subject: [PATCH 07/30] feat: implement file preview functionality with support for audio, image, and text files --- lib/common/utils/file_preview_utils.dart | 67 +++++++++ lib/data/services/api_service.dart | 35 +++++ .../viewmodels/detail_viewmodel.dart | 53 ++++++- lib/screens/detail_screen.dart | 69 ++++++--- lib/widgets/detail/file_preview_dialog.dart | 137 ++++++++++++++++++ lib/widgets/detail/work_file_item.dart | 31 +++- 6 files changed, 364 insertions(+), 28 deletions(-) create mode 100644 lib/common/utils/file_preview_utils.dart create mode 100644 lib/widgets/detail/file_preview_dialog.dart diff --git a/lib/common/utils/file_preview_utils.dart b/lib/common/utils/file_preview_utils.dart new file mode 100644 index 0000000..443fc8c --- /dev/null +++ b/lib/common/utils/file_preview_utils.dart @@ -0,0 +1,67 @@ +import 'package:asmrapp/data/models/files/child.dart'; + +class FilePreviewUtils { + static const Set _audioExtensions = { + '.mp3', + '.wav', + '.flac', + '.m4a', + '.aac', + '.ogg', + '.opus', + }; + + static const Set _imageExtensions = { + '.jpg', + '.jpeg', + '.png', + '.webp', + '.gif', + '.bmp', + }; + + static const Set _textExtensions = { + '.txt', + '.md', + '.json', + '.xml', + '.csv', + '.log', + '.yaml', + '.yml', + '.lrc', + '.vtt', + '.srt', + '.ass', + '.ssa', + }; + + static bool isAudio(Child file) { + if (file.type?.toLowerCase() == 'audio') return true; + return _audioExtensions.contains(_extension(file.title)); + } + + static bool isImage(Child file) { + if (file.type?.toLowerCase() == 'image') return true; + return _imageExtensions.contains(_extension(file.title)); + } + + static bool isText(Child file) { + final type = file.type?.toLowerCase(); + if (type == 'text' || type == 'subtitle' || type == 'lyrics') return true; + return _textExtensions.contains(_extension(file.title)); + } + + static bool canPreview(Child file) { + return isImage(file) || isText(file); + } + + static String? _extension(String? fileName) { + if (fileName == null || fileName.isEmpty) return null; + + final dotIndex = fileName.lastIndexOf('.'); + if (dotIndex < 0 || dotIndex == fileName.length - 1) return null; + + return fileName.substring(dotIndex).toLowerCase(); + } +} diff --git a/lib/data/services/api_service.dart b/lib/data/services/api_service.dart index 5aedce7..28f7636 100644 --- a/lib/data/services/api_service.dart +++ b/lib/data/services/api_service.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:asmrapp/core/cache/recommendation_cache_manager.dart'; import 'package:asmrapp/data/models/mark_status.dart'; import 'package:asmrapp/data/models/playlists_with_exist_statu/playlists_with_exist_statu.dart'; @@ -59,6 +61,39 @@ class ApiService { } } + /// 获取文本文件内容 + Future getTextFileContent( + String url, { + CancelToken? cancelToken, + }) async { + try { + final response = await _dio.get( + url, + options: Options(responseType: ResponseType.bytes), + cancelToken: cancelToken, + ); + + if (response.statusCode != 200) { + throw Exception('获取文本文件失败: ${response.statusCode}'); + } + + final data = response.data; + if (data == null) return ''; + if (data is String) return data; + if (data is List) { + return utf8.decode(data, allowMalformed: true); + } + + return data.toString(); + } on DioException catch (e) { + AppLogger.error('文本文件请求失败', e, e.stackTrace); + throw Exception('网络请求失败: ${e.message}'); + } catch (e, stackTrace) { + AppLogger.error('文本文件解析失败', e, stackTrace); + throw Exception('文本文件解析失败: $e'); + } + } + /// 获取作品列表 Future getWorks({ int page = 1, diff --git a/lib/presentation/viewmodels/detail_viewmodel.dart b/lib/presentation/viewmodels/detail_viewmodel.dart index 96b1e8c..af315a0 100644 --- a/lib/presentation/viewmodels/detail_viewmodel.dart +++ b/lib/presentation/viewmodels/detail_viewmodel.dart @@ -2,6 +2,7 @@ import 'package:asmrapp/data/models/playlists_with_exist_statu/pagination.dart'; import 'package:asmrapp/data/models/playlists_with_exist_statu/playlist.dart'; import 'package:get_it/get_it.dart'; import 'package:flutter/material.dart'; +import 'package:asmrapp/common/utils/file_preview_utils.dart'; import 'package:asmrapp/data/models/files/files.dart'; import 'package:asmrapp/data/models/files/child.dart'; import 'package:asmrapp/data/models/works/work.dart'; @@ -23,6 +24,12 @@ enum PlaybackError { failed, } +enum FilePreviewError { + unsupportedType, + missingUrl, + failed, +} + class PlaybackException implements Exception { final PlaybackError error; final String? detail; @@ -35,6 +42,18 @@ class PlaybackException implements Exception { }); } +class FilePreviewException implements Exception { + final FilePreviewError error; + final String? detail; + final Object? originalError; + + const FilePreviewException( + this.error, { + this.detail, + this.originalError, + }); +} + class DetailViewModel extends ChangeNotifier { late final ApiService _apiService; late final IAudioPlayerService _audioService; @@ -139,10 +158,10 @@ class DetailViewModel extends ChangeNotifier { } Future playFile(Child file, BuildContext context) async { - if (file.type?.toLowerCase() != 'audio') { + if (!FilePreviewUtils.isAudio(file)) { throw PlaybackException( PlaybackError.unsupportedType, - detail: file.type, + detail: file.type ?? file.title, ); } @@ -174,6 +193,36 @@ class DetailViewModel extends ChangeNotifier { } } + bool canPreviewFile(Child file) { + return FilePreviewUtils.canPreview(file); + } + + Future loadTextPreview(Child file) async { + if (!FilePreviewUtils.isText(file)) { + throw FilePreviewException( + FilePreviewError.unsupportedType, + detail: file.type ?? file.title, + ); + } + + if (file.mediaDownloadUrl == null || file.mediaDownloadUrl!.isEmpty) { + throw const FilePreviewException(FilePreviewError.missingUrl); + } + + try { + return await _apiService.getTextFileContent( + file.mediaDownloadUrl!, + cancelToken: _cancelToken, + ); + } catch (e) { + throw FilePreviewException( + FilePreviewError.failed, + detail: e.toString(), + originalError: e, + ); + } + } + /// 加载收藏夹列表 Future loadPlaylists({int page = 1}) async { if (_loadingPlaylists) return; diff --git a/lib/screens/detail_screen.dart b/lib/screens/detail_screen.dart index 555670b..ba0d085 100644 --- a/lib/screens/detail_screen.dart +++ b/lib/screens/detail_screen.dart @@ -1,7 +1,10 @@ +import 'package:asmrapp/common/utils/file_preview_utils.dart'; +import 'package:asmrapp/data/models/files/child.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/file_preview_dialog.dart'; import 'package:asmrapp/widgets/detail/work_action_buttons.dart'; import 'package:asmrapp/widgets/detail/work_cover.dart'; import 'package:asmrapp/widgets/detail/work_files_list.dart'; @@ -32,9 +35,6 @@ class DetailScreen extends StatelessWidget { title: Text(work.sourceId ?? ''), ), body: SingleChildScrollView( - padding: EdgeInsets.only( - bottom: MiniPlayer.heightWithSafeArea(context), - ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -97,21 +97,8 @@ class DetailScreen extends StatelessWidget { if (viewModel.files != null) { return WorkFilesList( files: viewModel.files!, - onFileTap: (file) async { - try { - await viewModel.playFile(file, context); - } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - _playbackErrorMessage(context, e), - ), - ), - ); - } - } - }, + onFileTap: (file) => + _handleFileTap(context, viewModel, file), ); } @@ -121,7 +108,51 @@ class DetailScreen extends StatelessWidget { ], ), ), - bottomSheet: const MiniPlayer(), + bottomNavigationBar: const MiniPlayer(), + ), + ); + } + + Future _handleFileTap( + BuildContext context, + DetailViewModel viewModel, + Child file, + ) async { + if (FilePreviewUtils.isAudio(file)) { + try { + await viewModel.playFile(file, context); + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + _playbackErrorMessage(context, e), + ), + ), + ); + } + } + return; + } + + if (viewModel.canPreviewFile(file)) { + if (!context.mounted) return; + await showDialog( + context: context, + builder: (dialogContext) => FilePreviewDialog( + file: file, + loadTextPreview: viewModel.loadTextPreview, + ), + ); + return; + } + + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.playUnsupportedFileType(file.type ?? file.title ?? ''), + ), ), ); } diff --git a/lib/widgets/detail/file_preview_dialog.dart b/lib/widgets/detail/file_preview_dialog.dart new file mode 100644 index 0000000..e045226 --- /dev/null +++ b/lib/widgets/detail/file_preview_dialog.dart @@ -0,0 +1,137 @@ +import 'package:asmrapp/common/utils/file_preview_utils.dart'; +import 'package:asmrapp/data/models/files/child.dart'; +import 'package:asmrapp/l10n/l10n.dart'; +import 'package:flutter/material.dart'; + +class FilePreviewDialog extends StatelessWidget { + final Child file; + final Future Function(Child file) loadTextPreview; + + const FilePreviewDialog({ + super.key, + required this.file, + required this.loadTextPreview, + }); + + @override + Widget build(BuildContext context) { + final title = file.title ?? ''; + + return Dialog.fullscreen( + child: Scaffold( + appBar: AppBar( + title: Text(title), + ), + body: SafeArea( + child: FilePreviewUtils.isImage(file) + ? _ImagePreview(file: file) + : _TextPreview( + file: file, + loadTextPreview: loadTextPreview, + ), + ), + ), + ); + } +} + +class _ImagePreview extends StatelessWidget { + final Child file; + + const _ImagePreview({required this.file}); + + @override + Widget build(BuildContext context) { + final url = file.mediaDownloadUrl; + if (url == null || url.isEmpty) { + return _PreviewMessage(message: context.l10n.playUrlMissing); + } + + return InteractiveViewer( + minScale: 1, + maxScale: 4, + child: Center( + child: Image.network( + url, + fit: BoxFit.contain, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return const Center(child: CircularProgressIndicator()); + }, + errorBuilder: (context, error, stackTrace) { + return _PreviewMessage( + message: context.l10n.operationFailed(error.toString()), + ); + }, + ), + ), + ); + } +} + +class _TextPreview extends StatelessWidget { + final Child file; + final Future Function(Child file) loadTextPreview; + + const _TextPreview({ + required this.file, + required this.loadTextPreview, + }); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: loadTextPreview(file), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (snapshot.hasError) { + return _PreviewMessage( + message: context.l10n.operationFailed(snapshot.error.toString()), + ); + } + + final text = snapshot.data ?? ''; + if (text.trim().isEmpty) { + return _PreviewMessage(message: context.l10n.emptyContent); + } + + return Scrollbar( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: SelectableText( + text, + style: const TextStyle( + height: 1.5, + fontFamily: 'monospace', + ), + ), + ), + ); + }, + ); + } +} + +class _PreviewMessage extends StatelessWidget { + final String message; + + const _PreviewMessage({required this.message}); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + message, + textAlign: TextAlign.center, + style: TextStyle(color: colorScheme.onSurfaceVariant), + ), + ), + ); + } +} diff --git a/lib/widgets/detail/work_file_item.dart b/lib/widgets/detail/work_file_item.dart index e085fc8..d71daa2 100644 --- a/lib/widgets/detail/work_file_item.dart +++ b/lib/widgets/detail/work_file_item.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/files/child.dart'; +import 'package:asmrapp/common/utils/file_preview_utils.dart'; import 'package:asmrapp/utils/logger.dart'; import 'package:asmrapp/utils/file_size_formatter.dart'; @@ -17,9 +18,28 @@ class WorkFileItem extends StatelessWidget { @override Widget build(BuildContext context) { - final bool isAudio = file.type?.toLowerCase() == 'audio'; + final isAudio = FilePreviewUtils.isAudio(file); + final isImage = FilePreviewUtils.isImage(file); + final isText = FilePreviewUtils.isText(file); + final isInteractive = isAudio || isImage || isText; final colorScheme = Theme.of(context).colorScheme; + final IconData iconData; + final Color iconColor; + if (isAudio) { + iconData = Icons.audio_file; + iconColor = Colors.green; + } else if (isImage) { + iconData = Icons.image; + iconColor = Colors.orange; + } else if (isText) { + iconData = Icons.text_snippet; + iconColor = Colors.teal; + } else { + iconData = Icons.insert_drive_file; + iconColor = Colors.blue; + } + return Padding( padding: EdgeInsets.only(left: indentation), child: ListTile( @@ -35,14 +55,11 @@ class WorkFileItem extends StatelessWidget { color: colorScheme.onSurfaceVariant, ), ), - leading: Icon( - isAudio ? Icons.audio_file : Icons.insert_drive_file, - color: isAudio ? Colors.green : Colors.blue, - ), + leading: Icon(iconData, color: iconColor), dense: true, - onTap: isAudio + onTap: isInteractive ? () { - AppLogger.debug('点击音频文件: ${file.title}'); + AppLogger.debug('点击文件: ${file.title}, type=${file.type}'); onFileTap?.call(file); } : null, From 3548ae0a730a54cffec968159a9ea078ffb4cf57 Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Wed, 18 Feb 2026 22:59:46 +0900 Subject: [PATCH 08/30] feat: add logout confirmation dialog and localizations for logout actions --- lib/l10n/app_ja.arb | 3 +++ lib/l10n/app_localizations.dart | 18 ++++++++++++++++++ lib/l10n/app_localizations_ja.dart | 9 +++++++++ lib/l10n/app_localizations_zh.dart | 9 +++++++++ lib/l10n/app_zh.arb | 3 +++ lib/widgets/drawer_menu.dart | 29 +++++++++++++++++++++++++++-- 6 files changed, 69 insertions(+), 2 deletions(-) diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 3d16eb3..01064f5 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -4,6 +4,9 @@ "retry": "再試行", "cancel": "キャンセル", "confirm": "確認", + "logoutAction": "ログアウト", + "logoutConfirmTitle": "ログアウト", + "logoutConfirmMessage": "本当にログアウトしますか?", "login": "ログイン", "favorites": "お気に入り", "settings": "設定", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index c18fe6b..dbb7b13 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -122,6 +122,24 @@ abstract class AppLocalizations { /// **'确认'** String get confirm; + /// No description provided for @logoutAction. + /// + /// In zh, this message translates to: + /// **'退出登录'** + String get logoutAction; + + /// No description provided for @logoutConfirmTitle. + /// + /// In zh, this message translates to: + /// **'退出登录'** + String get logoutConfirmTitle; + + /// No description provided for @logoutConfirmMessage. + /// + /// In zh, this message translates to: + /// **'确定要退出登录吗?'** + String get logoutConfirmMessage; + /// No description provided for @login. /// /// In zh, this message translates to: diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 21e3fbe..3c6af03 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -20,6 +20,15 @@ class AppLocalizationsJa extends AppLocalizations { @override String get confirm => '確認'; + @override + String get logoutAction => 'ログアウト'; + + @override + String get logoutConfirmTitle => 'ログアウト'; + + @override + String get logoutConfirmMessage => '本当にログアウトしますか?'; + @override String get login => 'ログイン'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 631df50..79c96ac 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -20,6 +20,15 @@ class AppLocalizationsZh extends AppLocalizations { @override String get confirm => '确认'; + @override + String get logoutAction => '退出登录'; + + @override + String get logoutConfirmTitle => '退出登录'; + + @override + String get logoutConfirmMessage => '确定要退出登录吗?'; + @override String get login => '登录'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index b4de91f..b7785d5 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -4,6 +4,9 @@ "retry": "重试", "cancel": "取消", "confirm": "确认", + "logoutAction": "退出登录", + "logoutConfirmTitle": "退出登录", + "logoutConfirmMessage": "确定要退出登录吗?", "login": "登录", "favorites": "我的收藏", "settings": "设置", diff --git a/lib/widgets/drawer_menu.dart b/lib/widgets/drawer_menu.dart index b56ce0e..aa0b5bd 100644 --- a/lib/widgets/drawer_menu.dart +++ b/lib/widgets/drawer_menu.dart @@ -19,6 +19,28 @@ class DrawerMenu extends StatelessWidget { ); } + Future _showLogoutConfirmDialog(BuildContext context) async { + final navigator = Navigator.of(context, rootNavigator: true); + return await showDialog( + context: navigator.context, + builder: (context) => AlertDialog( + title: Text(context.l10n.logoutConfirmTitle), + content: Text(context.l10n.logoutConfirmMessage), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: Text(context.l10n.cancel), + ), + FilledButton( + onPressed: () => Navigator.pop(context, true), + child: Text(context.l10n.logoutAction), + ), + ], + ), + ) ?? + false; + } + @override Widget build(BuildContext context) { return Drawer( @@ -54,10 +76,13 @@ class DrawerMenu extends StatelessWidget { ? authVM.username ?? '' : context.l10n.login, ), - onTap: () { + onTap: () async { Navigator.pop(context); if (authVM.isLoggedIn) { - authVM.logout(); + final shouldLogout = await _showLogoutConfirmDialog(context); + if (shouldLogout) { + await authVM.logout(); + } } else { _showLoginDialog(context); } From 8b9ec612eb26b701dfd226523bddc373edd48a61 Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Wed, 18 Feb 2026 23:18:24 +0900 Subject: [PATCH 09/30] feat: add URL launcher functionality to open DLsite in browser and update localization files --- lib/l10n/app_ja.arb | 1 + lib/l10n/app_localizations.dart | 6 ++ lib/l10n/app_localizations_ja.dart | 3 + lib/l10n/app_localizations_zh.dart | 3 + lib/l10n/app_zh.arb | 1 + lib/screens/detail_screen.dart | 45 ++++++++++++ linux/flutter/generated_plugin_registrant.cc | 4 ++ linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 72 +++++++++++++++++-- pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 13 files changed, 139 insertions(+), 4 deletions(-) diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 01064f5..b5a3b71 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -102,6 +102,7 @@ "workActionChecking": "確認中", "workActionRecommend": "おすすめ", "workActionNoRecommendation": "おすすめはありません", + "openDlsiteInBrowser": "DLsiteをブラウザで開く", "markStatusTitle": "マーク状態", "markStatusWantToListen": "聴きたい", "markStatusListening": "聴いている", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index dbb7b13..e7a277a 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -524,6 +524,12 @@ abstract class AppLocalizations { /// **'暂无推荐'** String get workActionNoRecommendation; + /// No description provided for @openDlsiteInBrowser. + /// + /// In zh, this message translates to: + /// **'在浏览器中打开DLsite'** + String get openDlsiteInBrowser; + /// No description provided for @markStatusTitle. /// /// In zh, this message translates to: diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 3c6af03..5c1bec8 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -229,6 +229,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get workActionNoRecommendation => 'おすすめはありません'; + @override + String get openDlsiteInBrowser => 'DLsiteをブラウザで開く'; + @override String get markStatusTitle => 'マーク状態'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 79c96ac..073450d 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -229,6 +229,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get workActionNoRecommendation => '暂无推荐'; + @override + String get openDlsiteInBrowser => '在浏览器中打开DLsite'; + @override String get markStatusTitle => '标记状态'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index b7785d5..8e6dbaa 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -102,6 +102,7 @@ "workActionChecking": "检查中", "workActionRecommend": "相关推荐", "workActionNoRecommendation": "暂无推荐", + "openDlsiteInBrowser": "在浏览器中打开DLsite", "markStatusTitle": "标记状态", "markStatusWantToListen": "想听", "markStatusListening": "在听", diff --git a/lib/screens/detail_screen.dart b/lib/screens/detail_screen.dart index ba0d085..66e9d5d 100644 --- a/lib/screens/detail_screen.dart +++ b/lib/screens/detail_screen.dart @@ -13,10 +13,12 @@ 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'; +import 'package:url_launcher/url_launcher.dart'; class DetailScreen extends StatelessWidget { final Work work; final bool fromPlayer; + static final RegExp _rjCodePattern = RegExp(r'(RJ\d+)', caseSensitive: false); const DetailScreen({ super.key, @@ -26,6 +28,8 @@ class DetailScreen extends StatelessWidget { @override Widget build(BuildContext context) { + final rjCode = _extractRjCode(); + return ChangeNotifierProvider( create: (_) => DetailViewModel( work: work, @@ -33,6 +37,14 @@ class DetailScreen extends StatelessWidget { child: Scaffold( appBar: AppBar( title: Text(work.sourceId ?? ''), + actions: [ + if (rjCode != null) + IconButton( + tooltip: context.l10n.openDlsiteInBrowser, + icon: const Icon(Icons.open_in_new), + onPressed: () => _openDlsiteInBrowser(context, rjCode), + ), + ], ), body: SingleChildScrollView( child: Column( @@ -172,4 +184,37 @@ class DetailScreen extends StatelessWidget { } return context.l10n.playFailed(error.toString()); } + + String? _extractRjCode() { + final candidates = [ + work.sourceId, + work.originalWorkno, + work.translationInfo?.originalWorkno, + ]; + + for (final candidate in candidates) { + if (candidate == null || candidate.trim().isEmpty) continue; + final match = _rjCodePattern.firstMatch(candidate); + final rjCode = match?.group(1); + if (rjCode != null && rjCode.isNotEmpty) { + return rjCode.toUpperCase(); + } + } + + return null; + } + + Future _openDlsiteInBrowser(BuildContext context, String rjCode) async { + final url = 'https://www.dlsite.com/maniax/work/=/product_id/$rjCode.html'; + final opened = await launchUrl( + Uri.parse(url), + mode: LaunchMode.externalApplication, + ); + + if (!opened && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.operationFailed(url))), + ); + } + } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index d2d6405..4132f88 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2333885..4332e43 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST media_kit_libs_linux + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b24ab12..54f3c3f 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -11,6 +11,7 @@ import just_audio import package_info_plus import shared_preferences_foundation import sqflite_darwin +import url_launcher_macos import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { @@ -20,5 +21,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 280225d..559ebb5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -1086,10 +1086,10 @@ packages: dependency: transitive description: name: test_api - sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.9" typed_data: dependency: transitive description: @@ -1122,6 +1122,70 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + url: "https://pub.dev" + source: hosted + version: "6.3.28" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + url: "https://pub.dev" + source: hosted + version: "2.4.2" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" uuid: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e6ffcd2..8ac64b9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: scrollable_positioned_list: ^0.3.8 marquee: ^2.3.0 wakelock_plus: ^1.2.8 + url_launcher: ^6.3.2 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 773a900..c515fc3 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,10 +8,13 @@ #include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MediaKitLibsWindowsAudioPluginCApi")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 6cff779..8e73ea7 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST media_kit_libs_windows_audio permission_handler_windows + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST From b0f591ecee984bb3c12f0a34dd0d43a518aca432 Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Thu, 19 Feb 2026 00:02:57 +0900 Subject: [PATCH 10/30] feat: implement file download functionality and related UI components --- lib/data/services/api_service.dart | 26 ++ lib/l10n/app_ja.arb | 45 +++ lib/l10n/app_localizations.dart | 66 +++++ lib/l10n/app_localizations_ja.dart | 41 +++ lib/l10n/app_localizations_zh.dart | 41 +++ lib/l10n/app_zh.arb | 45 +++ .../viewmodels/detail_viewmodel.dart | 164 +++++++++- lib/screens/detail_screen.dart | 146 +++++++-- .../download_file_selection_dialog.dart | 280 ++++++++++++++++++ lib/widgets/detail/related_works_section.dart | 119 ++++++++ lib/widgets/detail/work_action_buttons.dart | 28 +- 11 files changed, 955 insertions(+), 46 deletions(-) create mode 100644 lib/widgets/detail/download_file_selection_dialog.dart create mode 100644 lib/widgets/detail/related_works_section.dart diff --git a/lib/data/services/api_service.dart b/lib/data/services/api_service.dart index 28f7636..818a2aa 100644 --- a/lib/data/services/api_service.dart +++ b/lib/data/services/api_service.dart @@ -94,6 +94,32 @@ class ApiService { } } + /// 下载文件到指定路径 + Future downloadFileToPath( + String url, + String savePath, { + CancelToken? cancelToken, + }) async { + try { + final response = await _dio.download( + url, + savePath, + cancelToken: cancelToken, + ); + + final statusCode = response.statusCode ?? 0; + if (statusCode < 200 || statusCode >= 300) { + throw Exception('下载失败: $statusCode'); + } + } on DioException catch (e) { + AppLogger.error('文件下载失败', e, e.stackTrace); + throw Exception('网络请求失败: ${e.message}'); + } catch (e, stackTrace) { + AppLogger.error('文件下载异常', e, stackTrace); + throw Exception('文件下载失败: $e'); + } + } + /// 获取作品列表 Future getWorks({ int page = 1, diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index b5a3b71..b8538f8 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -68,6 +68,7 @@ "emptyContent": "内容がありません", "emptyWorks": "作品がありません", "similarWorksTitle": "関連作品", + "similarWorksSeeAll": "すべて見る", "playlistAddToFavorites": "お気に入りに追加", "playlistEmpty": "リストがありません", "playlistAddSuccess": "追加しました: {name}", @@ -99,9 +100,53 @@ "workActionFavorite": "お気に入り", "workActionMark": "マーク", "workActionRate": "評価", + "workActionDownload": "ダウンロード", "workActionChecking": "確認中", "workActionRecommend": "おすすめ", "workActionNoRecommendation": "おすすめはありません", + "downloadDialogTitle": "ダウンロードするファイルを選択", + "downloadDialogNoFiles": "ダウンロード可能なファイルがありません", + "downloadSelectedCount": "選択中: {count}件", + "@downloadSelectedCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadSelectAll": "すべて選択", + "downloadClearSelection": "選択解除", + "downloadNoFilesSelected": "ダウンロードするファイルを選択してください", + "downloadSuccess": "{count}件のダウンロードが完了しました: {path}", + "@downloadSuccess": { + "placeholders": { + "count": { + "type": "int" + }, + "path": { + "type": "String" + } + } + }, + "downloadPartial": "ダウンロード完了 {successCount}件 / 失敗 {failedCount}件", + "@downloadPartial": { + "placeholders": { + "successCount": { + "type": "int" + }, + "failedCount": { + "type": "int" + } + } + }, + "downloadAllFailed": "ダウンロードに失敗しました({failedCount}件)", + "@downloadAllFailed": { + "placeholders": { + "failedCount": { + "type": "int" + } + } + }, "openDlsiteInBrowser": "DLsiteをブラウザで開く", "markStatusTitle": "マーク状態", "markStatusWantToListen": "聴きたい", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e7a277a..5a2630a 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -446,6 +446,12 @@ abstract class AppLocalizations { /// **'相关推荐'** String get similarWorksTitle; + /// No description provided for @similarWorksSeeAll. + /// + /// In zh, this message translates to: + /// **'查看全部'** + String get similarWorksSeeAll; + /// No description provided for @playlistAddToFavorites. /// /// In zh, this message translates to: @@ -506,6 +512,12 @@ abstract class AppLocalizations { /// **'评分'** String get workActionRate; + /// No description provided for @workActionDownload. + /// + /// In zh, this message translates to: + /// **'下载'** + String get workActionDownload; + /// No description provided for @workActionChecking. /// /// In zh, this message translates to: @@ -524,6 +536,60 @@ abstract class AppLocalizations { /// **'暂无推荐'** String get workActionNoRecommendation; + /// No description provided for @downloadDialogTitle. + /// + /// In zh, this message translates to: + /// **'选择要下载的文件'** + String get downloadDialogTitle; + + /// No description provided for @downloadDialogNoFiles. + /// + /// In zh, this message translates to: + /// **'没有可下载的文件'** + String get downloadDialogNoFiles; + + /// No description provided for @downloadSelectedCount. + /// + /// In zh, this message translates to: + /// **'已选择 {count} 个'** + String downloadSelectedCount(int count); + + /// No description provided for @downloadSelectAll. + /// + /// In zh, this message translates to: + /// **'全选'** + String get downloadSelectAll; + + /// No description provided for @downloadClearSelection. + /// + /// In zh, this message translates to: + /// **'清空选择'** + String get downloadClearSelection; + + /// No description provided for @downloadNoFilesSelected. + /// + /// In zh, this message translates to: + /// **'请选择要下载的文件'** + String get downloadNoFilesSelected; + + /// No description provided for @downloadSuccess. + /// + /// In zh, this message translates to: + /// **'已下载 {count} 个文件: {path}'** + String downloadSuccess(int count, String path); + + /// No description provided for @downloadPartial. + /// + /// In zh, this message translates to: + /// **'下载完成 {successCount} 个,失败 {failedCount} 个'** + String downloadPartial(int successCount, int failedCount); + + /// No description provided for @downloadAllFailed. + /// + /// In zh, this message translates to: + /// **'下载失败({failedCount} 个)'** + String downloadAllFailed(int failedCount); + /// No description provided for @openDlsiteInBrowser. /// /// In zh, this message translates to: diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 5c1bec8..0da82d7 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -184,6 +184,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get similarWorksTitle => '関連作品'; + @override + String get similarWorksSeeAll => 'すべて見る'; + @override String get playlistAddToFavorites => 'お気に入りに追加'; @@ -220,6 +223,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get workActionRate => '評価'; + @override + String get workActionDownload => 'ダウンロード'; + @override String get workActionChecking => '確認中'; @@ -229,6 +235,41 @@ class AppLocalizationsJa extends AppLocalizations { @override String get workActionNoRecommendation => 'おすすめはありません'; + @override + String get downloadDialogTitle => 'ダウンロードするファイルを選択'; + + @override + String get downloadDialogNoFiles => 'ダウンロード可能なファイルがありません'; + + @override + String downloadSelectedCount(int count) { + return '選択中: $count件'; + } + + @override + String get downloadSelectAll => 'すべて選択'; + + @override + String get downloadClearSelection => '選択解除'; + + @override + String get downloadNoFilesSelected => 'ダウンロードするファイルを選択してください'; + + @override + String downloadSuccess(int count, String path) { + return '$count件のダウンロードが完了しました: $path'; + } + + @override + String downloadPartial(int successCount, int failedCount) { + return 'ダウンロード完了 $successCount件 / 失敗 $failedCount件'; + } + + @override + String downloadAllFailed(int failedCount) { + return 'ダウンロードに失敗しました($failedCount件)'; + } + @override String get openDlsiteInBrowser => 'DLsiteをブラウザで開く'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 073450d..1cad9a0 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -184,6 +184,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get similarWorksTitle => '相关推荐'; + @override + String get similarWorksSeeAll => '查看全部'; + @override String get playlistAddToFavorites => '添加到收藏夹'; @@ -220,6 +223,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get workActionRate => '评分'; + @override + String get workActionDownload => '下载'; + @override String get workActionChecking => '检查中'; @@ -229,6 +235,41 @@ class AppLocalizationsZh extends AppLocalizations { @override String get workActionNoRecommendation => '暂无推荐'; + @override + String get downloadDialogTitle => '选择要下载的文件'; + + @override + String get downloadDialogNoFiles => '没有可下载的文件'; + + @override + String downloadSelectedCount(int count) { + return '已选择 $count 个'; + } + + @override + String get downloadSelectAll => '全选'; + + @override + String get downloadClearSelection => '清空选择'; + + @override + String get downloadNoFilesSelected => '请选择要下载的文件'; + + @override + String downloadSuccess(int count, String path) { + return '已下载 $count 个文件: $path'; + } + + @override + String downloadPartial(int successCount, int failedCount) { + return '下载完成 $successCount 个,失败 $failedCount 个'; + } + + @override + String downloadAllFailed(int failedCount) { + return '下载失败($failedCount 个)'; + } + @override String get openDlsiteInBrowser => '在浏览器中打开DLsite'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 8e6dbaa..6fd9b56 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -68,6 +68,7 @@ "emptyContent": "暂无内容", "emptyWorks": "暂无作品", "similarWorksTitle": "相关推荐", + "similarWorksSeeAll": "查看全部", "playlistAddToFavorites": "添加到收藏夹", "playlistEmpty": "暂无收藏夹", "playlistAddSuccess": "添加成功: {name}", @@ -99,9 +100,53 @@ "workActionFavorite": "收藏", "workActionMark": "标记", "workActionRate": "评分", + "workActionDownload": "下载", "workActionChecking": "检查中", "workActionRecommend": "相关推荐", "workActionNoRecommendation": "暂无推荐", + "downloadDialogTitle": "选择要下载的文件", + "downloadDialogNoFiles": "没有可下载的文件", + "downloadSelectedCount": "已选择 {count} 个", + "@downloadSelectedCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadSelectAll": "全选", + "downloadClearSelection": "清空选择", + "downloadNoFilesSelected": "请选择要下载的文件", + "downloadSuccess": "已下载 {count} 个文件: {path}", + "@downloadSuccess": { + "placeholders": { + "count": { + "type": "int" + }, + "path": { + "type": "String" + } + } + }, + "downloadPartial": "下载完成 {successCount} 个,失败 {failedCount} 个", + "@downloadPartial": { + "placeholders": { + "successCount": { + "type": "int" + }, + "failedCount": { + "type": "int" + } + } + }, + "downloadAllFailed": "下载失败({failedCount} 个)", + "@downloadAllFailed": { + "placeholders": { + "failedCount": { + "type": "int" + } + } + }, "openDlsiteInBrowser": "在浏览器中打开DLsite", "markStatusTitle": "标记状态", "markStatusWantToListen": "想听", diff --git a/lib/presentation/viewmodels/detail_viewmodel.dart b/lib/presentation/viewmodels/detail_viewmodel.dart index af315a0..d5ce881 100644 --- a/lib/presentation/viewmodels/detail_viewmodel.dart +++ b/lib/presentation/viewmodels/detail_viewmodel.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:asmrapp/data/models/playlists_with_exist_statu/pagination.dart'; import 'package:asmrapp/data/models/playlists_with_exist_statu/playlist.dart'; import 'package:get_it/get_it.dart'; @@ -16,6 +18,7 @@ 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'; +import 'package:path_provider/path_provider.dart'; enum PlaybackError { unsupportedType, @@ -54,6 +57,18 @@ class FilePreviewException implements Exception { }); } +class DownloadBatchResult { + final int successCount; + final int failedCount; + final String saveDirectoryPath; + + const DownloadBatchResult({ + required this.successCount, + required this.failedCount, + required this.saveDirectoryPath, + }); +} + class DetailViewModel extends ChangeNotifier { late final ApiService _apiService; late final IAudioPlayerService _audioService; @@ -64,8 +79,10 @@ class DetailViewModel extends ChangeNotifier { String? _error; bool _disposed = false; + List _recommendedWorks = []; bool _hasRecommendations = false; - bool _checkingRecommendations = false; + bool _loadingRecommendations = false; + String? _recommendationsError; // 收藏夹相关状态 bool _loadingPlaylists = false; @@ -81,6 +98,8 @@ class DetailViewModel extends ChangeNotifier { bool _loadingMark = false; bool get loadingMark => _loadingMark; + bool _downloadingFiles = false; + bool get downloadingFiles => _downloadingFiles; // 添加取消标记 final _cancelToken = CancelToken(); @@ -90,14 +109,16 @@ class DetailViewModel extends ChangeNotifier { }) { _audioService = GetIt.I(); _apiService = GetIt.I(); - _checkRecommendations(); + loadRecommendationsPreview(); } Files? get files => _files; bool get isLoading => _isLoading; String? get error => _error; + List get recommendedWorks => _recommendedWorks; bool get hasRecommendations => _hasRecommendations; - bool get checkingRecommendations => _checkingRecommendations; + bool get loadingRecommendations => _loadingRecommendations; + String? get recommendationsError => _recommendationsError; // 收藏夹相关 getters bool get loadingPlaylists => _loadingPlaylists; @@ -109,8 +130,11 @@ class DetailViewModel extends ChangeNotifier { .ceil() : null; - Future _checkRecommendations() async { - _checkingRecommendations = true; + Future loadRecommendationsPreview() async { + if (_loadingRecommendations) return; + + _loadingRecommendations = true; + _recommendationsError = null; notifyListeners(); try { @@ -118,13 +142,19 @@ class DetailViewModel extends ChangeNotifier { itemId: work.id.toString(), page: 1, ); - _hasRecommendations = (response.pagination.totalCount ?? 0) > 0; + _recommendedWorks = response.works + .where((recommendedWork) => recommendedWork.id != work.id) + .toList(); + _hasRecommendations = (response.pagination.totalCount ?? 0) > 0 || + _recommendedWorks.isNotEmpty; } catch (e) { AppLogger.error('检查相关推荐失败', e); + _recommendedWorks = []; _hasRecommendations = false; + _recommendationsError = e.toString(); } finally { if (!_disposed) { - _checkingRecommendations = false; + _loadingRecommendations = false; notifyListeners(); } } @@ -342,6 +372,126 @@ class DetailViewModel extends ChangeNotifier { } } + Future downloadFiles(List files) async { + _downloadingFiles = true; + notifyListeners(); + + var successCount = 0; + var failedCount = 0; + var saveDirectoryPath = ''; + + try { + final saveDirectory = await _resolveDownloadDirectory(); + saveDirectoryPath = saveDirectory.path; + + for (final file in files) { + final downloadUrl = file.mediaDownloadUrl; + if (downloadUrl == null || downloadUrl.isEmpty) { + failedCount++; + continue; + } + + final safeFileName = _sanitizeFileName(file.title); + final savePath = + await _createUniqueSavePath(saveDirectory, safeFileName); + + try { + await _apiService.downloadFileToPath( + downloadUrl, + savePath, + cancelToken: _cancelToken, + ); + successCount++; + } catch (e) { + failedCount++; + AppLogger.error('下载文件失败: ${file.title}', e); + } + } + } finally { + _downloadingFiles = false; + if (!_disposed) { + notifyListeners(); + } + } + + return DownloadBatchResult( + successCount: successCount, + failedCount: failedCount, + saveDirectoryPath: saveDirectoryPath, + ); + } + + Future _resolveDownloadDirectory() async { + final candidateBaseDirectories = []; + + try { + candidateBaseDirectories.add(await getDownloadsDirectory()); + } catch (_) { + candidateBaseDirectories.add(null); + } + candidateBaseDirectories.add(await getApplicationDocumentsDirectory()); + + final workFolderName = _sanitizeFileName( + (work.sourceId?.trim().isNotEmpty ?? false) + ? work.sourceId! + : 'work_${work.id ?? 'unknown'}', + ); + + for (final baseDirectory in candidateBaseDirectories) { + if (baseDirectory == null) continue; + + try { + final rootDirectory = Directory( + '${baseDirectory.path}${Platform.pathSeparator}asmr_downloads', + ); + if (!await rootDirectory.exists()) { + await rootDirectory.create(recursive: true); + } + + final workDirectory = Directory( + '${rootDirectory.path}${Platform.pathSeparator}$workFolderName', + ); + if (!await workDirectory.exists()) { + await workDirectory.create(recursive: true); + } + + return workDirectory; + } catch (e) { + AppLogger.error('创建下载目录失败: ${baseDirectory.path}', e); + } + } + + throw Exception('无法创建下载目录'); + } + + Future _createUniqueSavePath( + Directory directory, + String fileName, + ) async { + final dotIndex = fileName.lastIndexOf('.'); + final baseName = dotIndex > 0 ? fileName.substring(0, dotIndex) : fileName; + final extension = dotIndex > 0 ? fileName.substring(dotIndex) : ''; + + var candidateName = fileName; + var counter = 1; + while (await File( + '${directory.path}${Platform.pathSeparator}$candidateName', + ).exists()) { + candidateName = '$baseName ($counter)$extension'; + counter++; + } + + return '${directory.path}${Platform.pathSeparator}$candidateName'; + } + + String _sanitizeFileName(String? original) { + final normalized = (original ?? '').trim(); + if (normalized.isEmpty) { + return 'file'; + } + return normalized.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_'); + } + void showMarkDialog(BuildContext context) { showDialog( context: context, diff --git a/lib/screens/detail_screen.dart b/lib/screens/detail_screen.dart index 66e9d5d..e38bae4 100644 --- a/lib/screens/detail_screen.dart +++ b/lib/screens/detail_screen.dart @@ -5,6 +5,8 @@ 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/file_preview_dialog.dart'; +import 'package:asmrapp/widgets/detail/download_file_selection_dialog.dart'; +import 'package:asmrapp/widgets/detail/related_works_section.dart'; import 'package:asmrapp/widgets/detail/work_action_buttons.dart'; import 'package:asmrapp/widgets/detail/work_cover.dart'; import 'package:asmrapp/widgets/detail/work_files_list.dart'; @@ -60,34 +62,15 @@ class DetailScreen extends StatelessWidget { WorkInfo(work: work), Consumer( builder: (context, viewModel, _) => WorkActionButtons( - hasRecommendations: viewModel.hasRecommendations, - checkingRecommendations: viewModel.checkingRecommendations, - onRecommendationsTap: () { - Navigator.of(context).push( - PageRouteBuilder( - pageBuilder: (context, animation, secondaryAnimation) => - SimilarWorksScreen(work: work), - transitionsBuilder: - (context, animation, secondaryAnimation, child) { - const begin = Offset(1.0, 0.0); - const end = Offset.zero; - const curve = Curves.easeInOut; - var tween = Tween(begin: begin, end: end).chain( - CurveTween(curve: curve), - ); - return SlideTransition( - position: animation.drive(tween), - child: child, - ); - }, - ), - ); - }, onFavoriteTap: () => viewModel.showPlaylistsDialog(context), loadingFavorite: viewModel.loadingFavorite, onMarkTap: () => viewModel.showMarkDialog(context), currentMarkStatus: viewModel.currentMarkStatus, loadingMark: viewModel.loadingMark, + onDownloadTap: viewModel.files == null + ? null + : () => _showDownloadDialog(context, viewModel), + loadingDownload: viewModel.downloadingFiles, ), ), Consumer( @@ -117,6 +100,18 @@ class DetailScreen extends StatelessWidget { return const SizedBox.shrink(); }, ), + Consumer( + builder: (context, viewModel, _) => RelatedWorksSection( + works: viewModel.recommendedWorks, + isLoading: viewModel.loadingRecommendations, + error: viewModel.recommendationsError, + hasRecommendations: viewModel.hasRecommendations, + onSeeAll: () => _openSimilarWorksScreen(context), + onRetry: viewModel.loadRecommendationsPreview, + onWorkTap: (relatedWork) => + _openWorkDetail(context, relatedWork), + ), + ), ], ), ), @@ -169,6 +164,82 @@ class DetailScreen extends StatelessWidget { ); } + Future _showDownloadDialog( + BuildContext context, + DetailViewModel viewModel, + ) async { + final files = viewModel.files?.children; + if (files == null || files.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.playFilesNotLoaded)), + ); + return; + } + + final selectedFiles = await showDialog>( + context: context, + builder: (dialogContext) => DownloadFileSelectionDialog( + rootFiles: files, + ), + ); + + if (selectedFiles == null) return; + if (selectedFiles.isEmpty) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.downloadNoFilesSelected)), + ); + return; + } + + late final DownloadBatchResult result; + try { + result = await viewModel.downloadFiles(selectedFiles); + } catch (e) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.operationFailed(e.toString()))), + ); + return; + } + + if (!context.mounted) return; + + if (result.successCount > 0 && result.failedCount == 0) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.downloadSuccess( + result.successCount, + result.saveDirectoryPath, + ), + ), + ), + ); + return; + } + + if (result.successCount > 0 && result.failedCount > 0) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.downloadPartial( + result.successCount, + result.failedCount, + ), + ), + ), + ); + return; + } + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.downloadAllFailed(result.failedCount)), + ), + ); + } + String _playbackErrorMessage(BuildContext context, Object error) { if (error is PlaybackException) { switch (error.error) { @@ -185,6 +256,35 @@ class DetailScreen extends StatelessWidget { return context.l10n.playFailed(error.toString()); } + void _openWorkDetail(BuildContext context, Work targetWork) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => DetailScreen(work: targetWork), + ), + ); + } + + void _openSimilarWorksScreen(BuildContext context) { + Navigator.of(context).push( + PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => + SimilarWorksScreen(work: work), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + const begin = Offset(1.0, 0.0); + const end = Offset.zero; + const curve = Curves.easeInOut; + final tween = + Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + return SlideTransition( + position: animation.drive(tween), + child: child, + ); + }, + ), + ); + } + String? _extractRjCode() { final candidates = [ work.sourceId, diff --git a/lib/widgets/detail/download_file_selection_dialog.dart b/lib/widgets/detail/download_file_selection_dialog.dart new file mode 100644 index 0000000..4064eaa --- /dev/null +++ b/lib/widgets/detail/download_file_selection_dialog.dart @@ -0,0 +1,280 @@ +import 'package:asmrapp/common/utils/file_preview_utils.dart'; +import 'package:asmrapp/data/models/files/child.dart'; +import 'package:asmrapp/l10n/l10n.dart'; +import 'package:asmrapp/utils/file_size_formatter.dart'; +import 'package:flutter/material.dart'; + +class DownloadFileSelectionDialog extends StatefulWidget { + final List rootFiles; + + const DownloadFileSelectionDialog({ + super.key, + required this.rootFiles, + }); + + @override + State createState() => + _DownloadFileSelectionDialogState(); +} + +class _DownloadFileSelectionDialogState + extends State { + final Set _selectedPaths = {}; + late final Map _downloadableFiles; + + @override + void initState() { + super.initState(); + _downloadableFiles = _collectDownloadableFiles(widget.rootFiles); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final hasDownloadableFiles = _downloadableFiles.isNotEmpty; + + return AlertDialog( + title: Text(l10n.downloadDialogTitle), + content: SizedBox( + width: double.maxFinite, + height: 460, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + l10n.downloadSelectedCount(_selectedPaths.length), + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + if (hasDownloadableFiles && + _selectedPaths.length < _downloadableFiles.length) + TextButton( + onPressed: () { + setState(() { + _selectedPaths + ..clear() + ..addAll(_downloadableFiles.keys); + }); + }, + child: Text(l10n.downloadSelectAll), + ), + if (_selectedPaths.isNotEmpty) + TextButton( + onPressed: () { + setState(_selectedPaths.clear); + }, + child: Text(l10n.downloadClearSelection), + ), + ], + ), + const SizedBox(height: 8), + Expanded( + child: hasDownloadableFiles + ? Scrollbar( + child: ListView( + children: _buildTreeNodes( + widget.rootFiles, + parentPath: '', + indentation: 0, + ), + ), + ) + : Center( + child: Text( + l10n.downloadDialogNoFiles, + textAlign: TextAlign.center, + ), + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(l10n.cancel), + ), + FilledButton.icon( + onPressed: _selectedPaths.isEmpty + ? null + : () { + final selectedFiles = _selectedPaths + .map((path) => _downloadableFiles[path]) + .whereType() + .toList(growable: false); + Navigator.of(context).pop(selectedFiles); + }, + icon: const Icon(Icons.download), + label: Text(l10n.workActionDownload), + ), + ], + ); + } + + List _buildTreeNodes( + List nodes, { + required String parentPath, + required double indentation, + }) { + return [ + for (var i = 0; i < nodes.length; i++) + _buildTreeNode( + nodes[i], + parentPath: parentPath, + indentation: indentation, + index: i, + ), + ]; + } + + Widget _buildTreeNode( + Child node, { + required String parentPath, + required double indentation, + required int index, + }) { + final currentPath = _buildNodePath( + parentPath: parentPath, + node: node, + index: index, + ); + + if (_isFolder(node)) { + if (!_hasDownloadableDescendant(node)) { + return const SizedBox.shrink(); + } + + final children = node.children ?? const []; + return Padding( + padding: EdgeInsets.only(left: indentation), + child: Theme( + data: Theme.of(context).copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + leading: const Icon(Icons.folder_outlined), + title: Text(_displayName(node)), + children: _buildTreeNodes( + children, + parentPath: currentPath, + indentation: indentation + 16, + ), + ), + ), + ); + } + + if (!_isDownloadable(node)) { + return const SizedBox.shrink(); + } + + final selected = _selectedPaths.contains(currentPath); + return Padding( + padding: EdgeInsets.only(left: indentation), + child: CheckboxListTile( + dense: true, + controlAffinity: ListTileControlAffinity.leading, + value: selected, + onChanged: (checked) { + setState(() { + if (checked ?? false) { + _selectedPaths.add(currentPath); + } else { + _selectedPaths.remove(currentPath); + } + }); + }, + secondary: Icon(_iconForFile(node)), + title: Text(_displayName(node)), + subtitle: Text(FileSizeFormatter.format(node.size)), + ), + ); + } + + Map _collectDownloadableFiles( + List nodes, { + String parentPath = '', + }) { + final result = {}; + for (var i = 0; i < nodes.length; i++) { + final node = nodes[i]; + final currentPath = _buildNodePath( + parentPath: parentPath, + node: node, + index: i, + ); + + if (_isFolder(node)) { + final children = node.children ?? const []; + result.addAll( + _collectDownloadableFiles( + children, + parentPath: currentPath, + ), + ); + continue; + } + + if (_isDownloadable(node)) { + result[currentPath] = node; + } + } + return result; + } + + bool _isFolder(Child node) => node.type?.toLowerCase() == 'folder'; + + bool _isDownloadable(Child node) { + final url = node.mediaDownloadUrl; + return !_isFolder(node) && url != null && url.isNotEmpty; + } + + bool _hasDownloadableDescendant(Child folder) { + final children = folder.children ?? const []; + for (final child in children) { + if (_isDownloadable(child)) { + return true; + } + if (_isFolder(child) && _hasDownloadableDescendant(child)) { + return true; + } + } + return false; + } + + String _buildNodePath({ + required String parentPath, + required Child node, + required int index, + }) { + final title = node.title?.trim(); + final segment = (title != null && title.isNotEmpty) + ? title + : (node.hash?.isNotEmpty ?? false) + ? node.hash! + : 'item_$index'; + return parentPath.isEmpty ? segment : '$parentPath/$segment'; + } + + String _displayName(Child node) { + final title = node.title?.trim(); + if (title != null && title.isNotEmpty) { + return title; + } + return context.l10n.unknownWorkTitle; + } + + IconData _iconForFile(Child file) { + if (FilePreviewUtils.isAudio(file)) { + return Icons.audio_file_outlined; + } + if (FilePreviewUtils.isImage(file)) { + return Icons.image_outlined; + } + if (FilePreviewUtils.isText(file)) { + return Icons.text_snippet_outlined; + } + return Icons.insert_drive_file_outlined; + } +} diff --git a/lib/widgets/detail/related_works_section.dart b/lib/widgets/detail/related_works_section.dart new file mode 100644 index 0000000..7dae23f --- /dev/null +++ b/lib/widgets/detail/related_works_section.dart @@ -0,0 +1,119 @@ +import 'package:asmrapp/data/models/works/work.dart'; +import 'package:asmrapp/l10n/l10n.dart'; +import 'package:asmrapp/widgets/work_card/work_card.dart'; +import 'package:flutter/material.dart'; + +class RelatedWorksSection extends StatelessWidget { + static const double _cardWidth = 188; + static const double _listHeight = 330; + + final List works; + final bool isLoading; + final String? error; + final bool hasRecommendations; + final VoidCallback? onSeeAll; + final VoidCallback? onRetry; + final ValueChanged onWorkTap; + + const RelatedWorksSection({ + super.key, + required this.works, + required this.isLoading, + required this.error, + required this.hasRecommendations, + required this.onSeeAll, + required this.onWorkTap, + this.onRetry, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Card( + margin: const EdgeInsets.fromLTRB(8, 8, 8, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 8, 8), + child: Row( + children: [ + Text( + context.l10n.similarWorksTitle, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + TextButton.icon( + onPressed: hasRecommendations ? onSeeAll : null, + icon: const Icon(Icons.chevron_right), + label: Text(context.l10n.similarWorksSeeAll), + ), + ], + ), + ), + Divider( + height: 1, + color: theme.colorScheme.surfaceContainerHighest, + ), + _buildBody(context), + ], + ), + ); + } + + Widget _buildBody(BuildContext context) { + if (isLoading) { + return const SizedBox( + height: _listHeight, + child: Center( + child: CircularProgressIndicator(), + ), + ); + } + + if (works.isEmpty) { + if (error != null && onRetry != null) { + return SizedBox( + height: _listHeight, + child: Center( + child: ElevatedButton( + onPressed: onRetry, + child: Text(context.l10n.retry), + ), + ), + ); + } + + return Padding( + padding: const EdgeInsets.all(16), + child: Text(context.l10n.workActionNoRecommendation), + ); + } + + return SizedBox( + height: _listHeight, + child: ListView.separated( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 16), + scrollDirection: Axis.horizontal, + itemCount: works.length, + separatorBuilder: (context, _) => const SizedBox(width: 12), + itemBuilder: (context, index) { + final work = works[index]; + return SizedBox( + width: _cardWidth, + child: HeroMode( + enabled: false, + child: WorkCard( + work: work, + onTap: () => onWorkTap(work), + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/widgets/detail/work_action_buttons.dart b/lib/widgets/detail/work_action_buttons.dart index 0802334..c300fc5 100644 --- a/lib/widgets/detail/work_action_buttons.dart +++ b/lib/widgets/detail/work_action_buttons.dart @@ -4,33 +4,33 @@ import 'package:asmrapp/l10n/l10n.dart'; import 'package:asmrapp/common/extensions/mark_status_localizations.dart'; class WorkActionButtons extends StatelessWidget { - final VoidCallback onRecommendationsTap; - final bool hasRecommendations; - final bool checkingRecommendations; final VoidCallback onFavoriteTap; final bool loadingFavorite; final VoidCallback onMarkTap; final MarkStatus? currentMarkStatus; final bool loadingMark; + final VoidCallback? onDownloadTap; + final bool loadingDownload; const WorkActionButtons({ super.key, - required this.onRecommendationsTap, - required this.hasRecommendations, - required this.checkingRecommendations, required this.onFavoriteTap, this.loadingFavorite = false, required this.onMarkTap, this.currentMarkStatus, this.loadingMark = false, + this.onDownloadTap, + this.loadingDownload = false, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + child: Wrap( + alignment: WrapAlignment.spaceEvenly, + spacing: 12, + runSpacing: 8, children: [ _ActionButton( icon: Icons.favorite_border, @@ -53,14 +53,10 @@ class WorkActionButtons extends StatelessWidget { }, ), _ActionButton( - icon: Icons.recommend, - label: checkingRecommendations - ? context.l10n.workActionChecking - : (hasRecommendations - ? context.l10n.workActionRecommend - : context.l10n.workActionNoRecommendation), - onTap: hasRecommendations ? onRecommendationsTap : null, - loading: checkingRecommendations, + icon: Icons.download_outlined, + label: context.l10n.workActionDownload, + onTap: onDownloadTap, + loading: loadingDownload, ), ], ), From 63fa0293f9ccd438cd8b4178c808e55e67dec039 Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Thu, 19 Feb 2026 00:20:08 +0900 Subject: [PATCH 11/30] feat: implement download management features and settings screen - Added DownloadDirectoryController to manage custom download paths and permissions. - Introduced DownloadProgressManager to track download tasks and their statuses. - Created DownloadTask model to represent individual download tasks. - Developed DownloadProgressPanel widget to display active and finished downloads. - Implemented SettingsScreen for users to configure download directory and access cache management. - Updated home_content.dart to include a download tab with DownloadProgressPanel. - Enhanced main_screen.dart to reflect active tab changes and filter button visibility. - Integrated file_selector package for directory selection in settings. - Added necessary localization strings for new features. --- android/app/src/main/AndroidManifest.xml | 3 + lib/core/di/service_locator.dart | 10 + .../download_directory_controller.dart | 193 +++++++++++++++ .../download/download_progress_manager.dart | 115 +++++++++ lib/core/download/download_task.dart | 83 +++++++ lib/data/services/api_service.dart | 2 + lib/l10n/app_ja.arb | 26 +++ lib/l10n/app_localizations.dart | 114 +++++++++ lib/l10n/app_localizations_ja.dart | 60 +++++ lib/l10n/app_localizations_zh.dart | 59 +++++ lib/l10n/app_zh.arb | 26 +++ lib/main.dart | 8 + .../viewmodels/detail_viewmodel.dart | 80 ++++--- .../viewmodels/home_viewmodel.dart | 13 ++ lib/screens/contents/home_content.dart | 122 ++++++---- lib/screens/main_screen.dart | 37 +-- lib/screens/settings/settings_screen.dart | 184 +++++++++++++++ .../download_file_selection_dialog.dart | 127 +++++++++- .../download/download_progress_panel.dart | 220 ++++++++++++++++++ lib/widgets/drawer_menu.dart | 11 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 72 ++++++ pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 27 files changed, 1482 insertions(+), 95 deletions(-) create mode 100644 lib/core/download/download_directory_controller.dart create mode 100644 lib/core/download/download_progress_manager.dart create mode 100644 lib/core/download/download_task.dart create mode 100644 lib/screens/settings/settings_screen.dart create mode 100644 lib/widgets/download/download_progress_panel.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 17e651e..c0b6bf3 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,9 @@ + + + setupServiceLocator() async { // 注册 WakeLockController getIt.registerLazySingleton(() => WakeLockController(prefs)); + + // 注册下载设置与进度 + getIt.registerLazySingleton( + () => DownloadDirectoryController(prefs), + ); + getIt.registerLazySingleton( + () => DownloadProgressManager(), + ); } Future setupSubtitleServices() async { diff --git a/lib/core/download/download_directory_controller.dart b/lib/core/download/download_directory_controller.dart new file mode 100644 index 0000000..f583df1 --- /dev/null +++ b/lib/core/download/download_directory_controller.dart @@ -0,0 +1,193 @@ +import 'dart:io'; + +import 'package:asmrapp/utils/logger.dart'; +import 'package:flutter/foundation.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class DownloadDirectoryController extends ChangeNotifier { + static const String _customDirectoryKey = 'download_custom_directory'; + + final SharedPreferences _prefs; + + DownloadDirectoryController(this._prefs) { + _customDirectoryPath = _prefs.getString(_customDirectoryKey); + } + + String? _customDirectoryPath; + String? _lastError; + + String? get customDirectoryPath => _customDirectoryPath; + bool get hasCustomDirectory => + _customDirectoryPath != null && _customDirectoryPath!.isNotEmpty; + String? get lastError => _lastError; + + Future ensureWritePermissionIfNeeded() async { + if (!Platform.isAndroid) { + return true; + } + + try { + final storageStatus = await Permission.storage.status; + if (storageStatus.isGranted) { + return true; + } + + final storageResult = await Permission.storage.request(); + if (storageResult.isGranted) { + return true; + } + + final manageStatus = await Permission.manageExternalStorage.status; + if (manageStatus.isGranted) { + return true; + } + final manageResult = await Permission.manageExternalStorage.request(); + return manageResult.isGranted; + } catch (e) { + AppLogger.error('请求存储权限失败', e); + return false; + } + } + + Future setCustomDirectoryPath(String path) async { + final trimmed = path.trim(); + if (trimmed.isEmpty) { + throw Exception('目录路径为空'); + } + + final directory = Directory(trimmed); + await _ensureDirectoryExists(directory); + + var writable = await _checkWritable(directory); + if (!writable) { + final granted = await ensureWritePermissionIfNeeded(); + if (granted) { + if (!await directory.exists()) { + await directory.create(recursive: true); + } + writable = await _checkWritable(directory); + } + } + if (!writable) { + throw Exception('目录不可写或权限不足'); + } + + _customDirectoryPath = directory.path; + _lastError = null; + await _prefs.setString(_customDirectoryKey, directory.path); + notifyListeners(); + } + + Future clearCustomDirectoryPath() async { + _customDirectoryPath = null; + _lastError = null; + await _prefs.remove(_customDirectoryKey); + notifyListeners(); + } + + Future resolveDownloadRootDirectory() async { + if (hasCustomDirectory) { + final custom = Directory(_customDirectoryPath!); + await _ensureDirectoryExists(custom); + + var writable = await _checkWritable(custom); + if (!writable) { + final granted = await ensureWritePermissionIfNeeded(); + if (granted) { + if (!await custom.exists()) { + await custom.create(recursive: true); + } + writable = await _checkWritable(custom); + } + } + + if (!writable) { + _lastError = '自定义下载目录不可写'; + notifyListeners(); + throw Exception(_lastError); + } + + _lastError = null; + return custom; + } + + final candidateBaseDirectories = []; + try { + candidateBaseDirectories.add(await getDownloadsDirectory()); + } catch (_) { + candidateBaseDirectories.add(null); + } + candidateBaseDirectories.add(await getApplicationDocumentsDirectory()); + + for (final baseDirectory in candidateBaseDirectories) { + if (baseDirectory == null) continue; + + try { + final rootDirectory = Directory( + '${baseDirectory.path}${Platform.pathSeparator}asmr_downloads', + ); + await _ensureDirectoryExists(rootDirectory); + var writable = await _checkWritable(rootDirectory); + if (!writable) { + final granted = await ensureWritePermissionIfNeeded(); + if (granted) { + if (!await rootDirectory.exists()) { + await rootDirectory.create(recursive: true); + } + writable = await _checkWritable(rootDirectory); + } + } + if (writable) { + _lastError = null; + return rootDirectory; + } + } catch (e) { + AppLogger.error('创建默认下载目录失败: ${baseDirectory.path}', e); + } + } + + _lastError = '无法创建下载目录'; + notifyListeners(); + throw Exception(_lastError); + } + + Future _checkWritable(Directory directory) async { + final probeFile = File( + '${directory.path}${Platform.pathSeparator}.asmr_write_probe', + ); + try { + await probeFile.writeAsString( + DateTime.now().microsecondsSinceEpoch.toString(), + flush: true, + ); + if (await probeFile.exists()) { + await probeFile.delete(); + } + return true; + } catch (e) { + AppLogger.error('目录写入检查失败: ${directory.path}', e); + return false; + } + } + + Future _ensureDirectoryExists(Directory directory) async { + if (await directory.exists()) { + return; + } + + try { + await directory.create(recursive: true); + return; + } catch (_) { + final granted = await ensureWritePermissionIfNeeded(); + if (!granted) { + rethrow; + } + if (!await directory.exists()) { + await directory.create(recursive: true); + } + } + } +} diff --git a/lib/core/download/download_progress_manager.dart b/lib/core/download/download_progress_manager.dart new file mode 100644 index 0000000..ef72c4f --- /dev/null +++ b/lib/core/download/download_progress_manager.dart @@ -0,0 +1,115 @@ +import 'dart:collection'; + +import 'package:asmrapp/core/download/download_task.dart'; +import 'package:flutter/foundation.dart'; + +class DownloadProgressManager extends ChangeNotifier { + static const int _maxHistory = 200; + + final List _tasks = []; + int _sequence = 0; + + UnmodifiableListView get tasks => UnmodifiableListView(_tasks); + + List get activeTasks => + _tasks.where((task) => task.isActive).toList(growable: false); + + List get finishedTasks => + _tasks.where((task) => task.isFinished).toList(growable: false); + + bool get hasActiveTasks => _tasks.any((task) => task.isActive); + + String createTask({ + required int? workId, + required String workTitle, + required String fileName, + required String savePath, + }) { + _sequence++; + final id = '${DateTime.now().microsecondsSinceEpoch}_$_sequence'; + _tasks.insert( + 0, + DownloadTask( + id: id, + workId: workId, + workTitle: workTitle, + fileName: fileName, + savePath: savePath, + receivedBytes: 0, + totalBytes: 0, + status: DownloadTaskStatus.queued, + createdAt: DateTime.now(), + ), + ); + _trimHistory(); + notifyListeners(); + return id; + } + + void markStarted(String taskId) { + _updateTask( + taskId, + (task) => task.copyWith( + status: DownloadTaskStatus.running, + startedAt: DateTime.now(), + clearError: true, + ), + ); + } + + void updateProgress(String taskId, int receivedBytes, int totalBytes) { + _updateTask( + taskId, + (task) => task.copyWith( + status: DownloadTaskStatus.running, + receivedBytes: receivedBytes < 0 ? 0 : receivedBytes, + totalBytes: totalBytes < 0 ? 0 : totalBytes, + ), + ); + } + + void markCompleted(String taskId) { + _updateTask( + taskId, + (task) => task.copyWith( + status: DownloadTaskStatus.completed, + receivedBytes: + task.totalBytes > 0 ? task.totalBytes : task.receivedBytes, + finishedAt: DateTime.now(), + ), + ); + } + + void markFailed(String taskId, Object error) { + _updateTask( + taskId, + (task) => task.copyWith( + status: DownloadTaskStatus.failed, + finishedAt: DateTime.now(), + errorMessage: error.toString(), + ), + ); + } + + void clearFinished() { + _tasks.removeWhere((task) => task.isFinished); + notifyListeners(); + } + + void _updateTask( + String taskId, DownloadTask Function(DownloadTask task) map) { + final index = _tasks.indexWhere((task) => task.id == taskId); + if (index < 0) { + return; + } + _tasks[index] = map(_tasks[index]); + notifyListeners(); + } + + void _trimHistory() { + if (_tasks.length <= _maxHistory) { + return; + } + _tasks.removeRange(_maxHistory, _tasks.length); + } +} diff --git a/lib/core/download/download_task.dart b/lib/core/download/download_task.dart new file mode 100644 index 0000000..50b83dc --- /dev/null +++ b/lib/core/download/download_task.dart @@ -0,0 +1,83 @@ +enum DownloadTaskStatus { + queued, + running, + completed, + failed, +} + +class DownloadTask { + final String id; + final int? workId; + final String workTitle; + final String fileName; + final String savePath; + final int receivedBytes; + final int totalBytes; + final DownloadTaskStatus status; + final DateTime createdAt; + final DateTime? startedAt; + final DateTime? finishedAt; + final String? errorMessage; + + const DownloadTask({ + required this.id, + required this.workId, + required this.workTitle, + required this.fileName, + required this.savePath, + required this.receivedBytes, + required this.totalBytes, + required this.status, + required this.createdAt, + this.startedAt, + this.finishedAt, + this.errorMessage, + }); + + double get progress { + if (status == DownloadTaskStatus.completed) { + return 1; + } + if (totalBytes <= 0) { + return 0; + } + final value = receivedBytes / totalBytes; + if (value.isNaN || value.isInfinite) { + return 0; + } + return value.clamp(0, 1); + } + + bool get isActive => + status == DownloadTaskStatus.queued || + status == DownloadTaskStatus.running; + + bool get isFinished => + status == DownloadTaskStatus.completed || + status == DownloadTaskStatus.failed; + + DownloadTask copyWith({ + int? receivedBytes, + int? totalBytes, + DownloadTaskStatus? status, + DateTime? startedAt, + DateTime? finishedAt, + String? errorMessage, + bool clearError = false, + }) { + return DownloadTask( + id: id, + workId: workId, + workTitle: workTitle, + fileName: fileName, + savePath: savePath, + receivedBytes: receivedBytes ?? this.receivedBytes, + totalBytes: totalBytes ?? this.totalBytes, + status: status ?? this.status, + createdAt: createdAt, + startedAt: startedAt ?? this.startedAt, + finishedAt: finishedAt ?? this.finishedAt, + errorMessage: clearError ? null : (errorMessage ?? this.errorMessage), + ); + } +} diff --git a/lib/data/services/api_service.dart b/lib/data/services/api_service.dart index 818a2aa..508dd67 100644 --- a/lib/data/services/api_service.dart +++ b/lib/data/services/api_service.dart @@ -99,12 +99,14 @@ class ApiService { String url, String savePath, { CancelToken? cancelToken, + ProgressCallback? onReceiveProgress, }) async { try { final response = await _dio.download( url, savePath, cancelToken: cancelToken, + onReceiveProgress: onReceiveProgress, ); final statusCode = response.statusCode ?? 0; diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index b8538f8..34ea340 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -17,9 +17,12 @@ "themeDark": "ダークモード", "navigationFavorites": "お気に入り", "navigationHome": "ホーム", + "navigationDownloadProgress": "ダウンロード進捗", "navigationForYou": "あなた向け", "navigationPopularWorks": "人気作品", "navigationRecommend": "おすすめ", + "homeTabWorks": "作品", + "homeTabDownloads": "進捗", "titleWithCount": "{title} ({count})", "@titleWithCount": { "placeholders": { @@ -147,6 +150,29 @@ } } }, + "downloadDirectoryTitle": "ダウンロードフォルダ", + "downloadDirectoryDescription": "保存先フォルダを指定できます。未指定時は既定のダウンロード先を使います。", + "downloadDirectoryDefaultValue": "未設定(既定の保存先)", + "downloadDirectoryPermissionHint": "必要な場合はストレージ権限を要求します。", + "downloadDirectoryPick": "フォルダを選択", + "downloadDirectoryReset": "既定に戻す", + "downloadDirectoryUpdated": "保存先を更新しました: {path}", + "@downloadDirectoryUpdated": { + "placeholders": { + "path": { + "type": "String" + } + } + }, + "downloadDirectoryResetSuccess": "保存先を既定に戻しました", + "downloadProgressEmpty": "進行中のダウンロードはありません", + "downloadProgressClearFinished": "完了履歴をクリア", + "downloadProgressActiveSection": "進行中", + "downloadProgressHistorySection": "履歴", + "downloadStatusQueued": "待機中", + "downloadStatusRunning": "ダウンロード中", + "downloadStatusCompleted": "完了", + "downloadStatusFailed": "失敗", "openDlsiteInBrowser": "DLsiteをブラウザで開く", "markStatusTitle": "マーク状態", "markStatusWantToListen": "聴きたい", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 5a2630a..7e8bbe6 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -200,6 +200,12 @@ abstract class AppLocalizations { /// **'主页'** String get navigationHome; + /// No description provided for @navigationDownloadProgress. + /// + /// In zh, this message translates to: + /// **'下载进度'** + String get navigationDownloadProgress; + /// No description provided for @navigationForYou. /// /// In zh, this message translates to: @@ -218,6 +224,18 @@ abstract class AppLocalizations { /// **'推荐'** String get navigationRecommend; + /// No description provided for @homeTabWorks. + /// + /// In zh, this message translates to: + /// **'作品'** + String get homeTabWorks; + + /// No description provided for @homeTabDownloads. + /// + /// In zh, this message translates to: + /// **'进度'** + String get homeTabDownloads; + /// No description provided for @titleWithCount. /// /// In zh, this message translates to: @@ -590,6 +608,102 @@ abstract class AppLocalizations { /// **'下载失败({failedCount} 个)'** String downloadAllFailed(int failedCount); + /// No description provided for @downloadDirectoryTitle. + /// + /// In zh, this message translates to: + /// **'下载文件夹'** + String get downloadDirectoryTitle; + + /// No description provided for @downloadDirectoryDescription. + /// + /// In zh, this message translates to: + /// **'可指定下载保存位置。未设置时使用默认下载目录。'** + String get downloadDirectoryDescription; + + /// No description provided for @downloadDirectoryDefaultValue. + /// + /// In zh, this message translates to: + /// **'未设置(使用默认目录)'** + String get downloadDirectoryDefaultValue; + + /// No description provided for @downloadDirectoryPermissionHint. + /// + /// In zh, this message translates to: + /// **'必要时会请求存储权限。'** + String get downloadDirectoryPermissionHint; + + /// No description provided for @downloadDirectoryPick. + /// + /// In zh, this message translates to: + /// **'选择文件夹'** + String get downloadDirectoryPick; + + /// No description provided for @downloadDirectoryReset. + /// + /// In zh, this message translates to: + /// **'恢复默认'** + String get downloadDirectoryReset; + + /// No description provided for @downloadDirectoryUpdated. + /// + /// In zh, this message translates to: + /// **'下载目录已更新:{path}'** + String downloadDirectoryUpdated(String path); + + /// No description provided for @downloadDirectoryResetSuccess. + /// + /// In zh, this message translates to: + /// **'已恢复默认下载目录'** + String get downloadDirectoryResetSuccess; + + /// No description provided for @downloadProgressEmpty. + /// + /// In zh, this message translates to: + /// **'当前没有下载任务'** + String get downloadProgressEmpty; + + /// No description provided for @downloadProgressClearFinished. + /// + /// In zh, this message translates to: + /// **'清空已完成'** + String get downloadProgressClearFinished; + + /// No description provided for @downloadProgressActiveSection. + /// + /// In zh, this message translates to: + /// **'进行中'** + String get downloadProgressActiveSection; + + /// No description provided for @downloadProgressHistorySection. + /// + /// In zh, this message translates to: + /// **'历史记录'** + String get downloadProgressHistorySection; + + /// No description provided for @downloadStatusQueued. + /// + /// In zh, this message translates to: + /// **'等待中'** + String get downloadStatusQueued; + + /// No description provided for @downloadStatusRunning. + /// + /// In zh, this message translates to: + /// **'下载中'** + String get downloadStatusRunning; + + /// No description provided for @downloadStatusCompleted. + /// + /// In zh, this message translates to: + /// **'已完成'** + String get downloadStatusCompleted; + + /// No description provided for @downloadStatusFailed. + /// + /// In zh, this message translates to: + /// **'失败'** + String get downloadStatusFailed; + /// No description provided for @openDlsiteInBrowser. /// /// In zh, this message translates to: diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 0da82d7..a393709 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -59,6 +59,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get navigationHome => 'ホーム'; + @override + String get navigationDownloadProgress => 'ダウンロード進捗'; + @override String get navigationForYou => 'あなた向け'; @@ -68,6 +71,12 @@ class AppLocalizationsJa extends AppLocalizations { @override String get navigationRecommend => 'おすすめ'; + @override + String get homeTabWorks => '作品'; + + @override + String get homeTabDownloads => '進捗'; + @override String titleWithCount(String title, int count) { return '$title ($count)'; @@ -270,6 +279,57 @@ class AppLocalizationsJa extends AppLocalizations { return 'ダウンロードに失敗しました($failedCount件)'; } + @override + String get downloadDirectoryTitle => 'ダウンロードフォルダ'; + + @override + String get downloadDirectoryDescription => + '保存先フォルダを指定できます。未指定時は既定のダウンロード先を使います。'; + + @override + String get downloadDirectoryDefaultValue => '未設定(既定の保存先)'; + + @override + String get downloadDirectoryPermissionHint => '必要な場合はストレージ権限を要求します。'; + + @override + String get downloadDirectoryPick => 'フォルダを選択'; + + @override + String get downloadDirectoryReset => '既定に戻す'; + + @override + String downloadDirectoryUpdated(String path) { + return '保存先を更新しました: $path'; + } + + @override + String get downloadDirectoryResetSuccess => '保存先を既定に戻しました'; + + @override + String get downloadProgressEmpty => '進行中のダウンロードはありません'; + + @override + String get downloadProgressClearFinished => '完了履歴をクリア'; + + @override + String get downloadProgressActiveSection => '進行中'; + + @override + String get downloadProgressHistorySection => '履歴'; + + @override + String get downloadStatusQueued => '待機中'; + + @override + String get downloadStatusRunning => 'ダウンロード中'; + + @override + String get downloadStatusCompleted => '完了'; + + @override + String get downloadStatusFailed => '失敗'; + @override String get openDlsiteInBrowser => 'DLsiteをブラウザで開く'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 1cad9a0..35755db 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -59,6 +59,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get navigationHome => '主页'; + @override + String get navigationDownloadProgress => '下载进度'; + @override String get navigationForYou => '为你推荐'; @@ -68,6 +71,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get navigationRecommend => '推荐'; + @override + String get homeTabWorks => '作品'; + + @override + String get homeTabDownloads => '进度'; + @override String titleWithCount(String title, int count) { return '$title ($count)'; @@ -270,6 +279,56 @@ class AppLocalizationsZh extends AppLocalizations { return '下载失败($failedCount 个)'; } + @override + String get downloadDirectoryTitle => '下载文件夹'; + + @override + String get downloadDirectoryDescription => '可指定下载保存位置。未设置时使用默认下载目录。'; + + @override + String get downloadDirectoryDefaultValue => '未设置(使用默认目录)'; + + @override + String get downloadDirectoryPermissionHint => '必要时会请求存储权限。'; + + @override + String get downloadDirectoryPick => '选择文件夹'; + + @override + String get downloadDirectoryReset => '恢复默认'; + + @override + String downloadDirectoryUpdated(String path) { + return '下载目录已更新:$path'; + } + + @override + String get downloadDirectoryResetSuccess => '已恢复默认下载目录'; + + @override + String get downloadProgressEmpty => '当前没有下载任务'; + + @override + String get downloadProgressClearFinished => '清空已完成'; + + @override + String get downloadProgressActiveSection => '进行中'; + + @override + String get downloadProgressHistorySection => '历史记录'; + + @override + String get downloadStatusQueued => '等待中'; + + @override + String get downloadStatusRunning => '下载中'; + + @override + String get downloadStatusCompleted => '已完成'; + + @override + String get downloadStatusFailed => '失败'; + @override String get openDlsiteInBrowser => '在浏览器中打开DLsite'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 6fd9b56..3adc9a3 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -17,9 +17,12 @@ "themeDark": "深色模式", "navigationFavorites": "收藏", "navigationHome": "主页", + "navigationDownloadProgress": "下载进度", "navigationForYou": "为你推荐", "navigationPopularWorks": "热门作品", "navigationRecommend": "推荐", + "homeTabWorks": "作品", + "homeTabDownloads": "进度", "titleWithCount": "{title} ({count})", "@titleWithCount": { "placeholders": { @@ -147,6 +150,29 @@ } } }, + "downloadDirectoryTitle": "下载文件夹", + "downloadDirectoryDescription": "可指定下载保存位置。未设置时使用默认下载目录。", + "downloadDirectoryDefaultValue": "未设置(使用默认目录)", + "downloadDirectoryPermissionHint": "必要时会请求存储权限。", + "downloadDirectoryPick": "选择文件夹", + "downloadDirectoryReset": "恢复默认", + "downloadDirectoryUpdated": "下载目录已更新:{path}", + "@downloadDirectoryUpdated": { + "placeholders": { + "path": { + "type": "String" + } + } + }, + "downloadDirectoryResetSuccess": "已恢复默认下载目录", + "downloadProgressEmpty": "当前没有下载任务", + "downloadProgressClearFinished": "清空已完成", + "downloadProgressActiveSection": "进行中", + "downloadProgressHistorySection": "历史记录", + "downloadStatusQueued": "等待中", + "downloadStatusRunning": "下载中", + "downloadStatusCompleted": "已完成", + "downloadStatusFailed": "失败", "openDlsiteInBrowser": "在浏览器中打开DLsite", "markStatusTitle": "标记状态", "markStatusWantToListen": "想听", diff --git a/lib/main.dart b/lib/main.dart index 27e71e8..b185bad 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,6 @@ import 'package:asmrapp/core/theme/app_theme.dart'; +import 'package:asmrapp/core/download/download_directory_controller.dart'; +import 'package:asmrapp/core/download/download_progress_manager.dart'; import 'package:asmrapp/core/theme/theme_controller.dart'; import 'package:asmrapp/l10n/app_localizations.dart'; import 'package:asmrapp/l10n/l10n.dart'; @@ -39,6 +41,12 @@ class MyApp extends StatelessWidget { ChangeNotifierProvider( create: (_) => getIt(), ), + ChangeNotifierProvider( + create: (_) => getIt(), + ), + ChangeNotifierProvider( + create: (_) => getIt(), + ), ], child: Consumer( builder: (context, themeController, child) { diff --git a/lib/presentation/viewmodels/detail_viewmodel.dart b/lib/presentation/viewmodels/detail_viewmodel.dart index d5ce881..f39fd86 100644 --- a/lib/presentation/viewmodels/detail_viewmodel.dart +++ b/lib/presentation/viewmodels/detail_viewmodel.dart @@ -15,10 +15,11 @@ import 'package:asmrapp/core/audio/models/playback_context.dart'; 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:asmrapp/core/download/download_directory_controller.dart'; +import 'package:asmrapp/core/download/download_progress_manager.dart'; import 'package:dio/dio.dart'; import 'package:asmrapp/l10n/l10n.dart'; import 'package:asmrapp/common/extensions/mark_status_localizations.dart'; -import 'package:path_provider/path_provider.dart'; enum PlaybackError { unsupportedType, @@ -72,6 +73,8 @@ class DownloadBatchResult { class DetailViewModel extends ChangeNotifier { late final ApiService _apiService; late final IAudioPlayerService _audioService; + late final DownloadDirectoryController _downloadDirectoryController; + late final DownloadProgressManager _downloadProgressManager; final Work work; Files? _files; @@ -109,6 +112,8 @@ class DetailViewModel extends ChangeNotifier { }) { _audioService = GetIt.I(); _apiService = GetIt.I(); + _downloadDirectoryController = GetIt.I(); + _downloadProgressManager = GetIt.I(); loadRecommendationsPreview(); } @@ -381,7 +386,9 @@ class DetailViewModel extends ChangeNotifier { var saveDirectoryPath = ''; try { - final saveDirectory = await _resolveDownloadDirectory(); + final rootDirectory = + await _downloadDirectoryController.resolveDownloadRootDirectory(); + final saveDirectory = await _resolveWorkDirectory(rootDirectory); saveDirectoryPath = saveDirectory.path; for (final file in files) { @@ -394,15 +401,31 @@ class DetailViewModel extends ChangeNotifier { final safeFileName = _sanitizeFileName(file.title); final savePath = await _createUniqueSavePath(saveDirectory, safeFileName); + final taskId = _downloadProgressManager.createTask( + workId: work.id, + workTitle: _resolveWorkTitle(), + fileName: safeFileName, + savePath: savePath, + ); try { + _downloadProgressManager.markStarted(taskId); await _apiService.downloadFileToPath( downloadUrl, savePath, cancelToken: _cancelToken, + onReceiveProgress: (receivedBytes, totalBytes) { + _downloadProgressManager.updateProgress( + taskId, + receivedBytes, + totalBytes, + ); + }, ); + _downloadProgressManager.markCompleted(taskId); successCount++; } catch (e) { + _downloadProgressManager.markFailed(taskId, e); failedCount++; AppLogger.error('下载文件失败: ${file.title}', e); } @@ -421,47 +444,20 @@ class DetailViewModel extends ChangeNotifier { ); } - Future _resolveDownloadDirectory() async { - final candidateBaseDirectories = []; - - try { - candidateBaseDirectories.add(await getDownloadsDirectory()); - } catch (_) { - candidateBaseDirectories.add(null); - } - candidateBaseDirectories.add(await getApplicationDocumentsDirectory()); - + Future _resolveWorkDirectory(Directory rootDirectory) async { final workFolderName = _sanitizeFileName( (work.sourceId?.trim().isNotEmpty ?? false) ? work.sourceId! : 'work_${work.id ?? 'unknown'}', ); - for (final baseDirectory in candidateBaseDirectories) { - if (baseDirectory == null) continue; - - try { - final rootDirectory = Directory( - '${baseDirectory.path}${Platform.pathSeparator}asmr_downloads', - ); - if (!await rootDirectory.exists()) { - await rootDirectory.create(recursive: true); - } - - final workDirectory = Directory( - '${rootDirectory.path}${Platform.pathSeparator}$workFolderName', - ); - if (!await workDirectory.exists()) { - await workDirectory.create(recursive: true); - } - - return workDirectory; - } catch (e) { - AppLogger.error('创建下载目录失败: ${baseDirectory.path}', e); - } + final workDirectory = Directory( + '${rootDirectory.path}${Platform.pathSeparator}$workFolderName', + ); + if (!await workDirectory.exists()) { + await workDirectory.create(recursive: true); } - - throw Exception('无法创建下载目录'); + return workDirectory; } Future _createUniqueSavePath( @@ -492,6 +488,18 @@ class DetailViewModel extends ChangeNotifier { return normalized.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_'); } + String _resolveWorkTitle() { + final sourceId = work.sourceId?.trim(); + if (sourceId != null && sourceId.isNotEmpty) { + return sourceId; + } + final title = work.title?.trim(); + if (title != null && title.isNotEmpty) { + return title; + } + return 'work_${work.id ?? 'unknown'}'; + } + void showMarkDialog(BuildContext context) { showDialog( context: context, diff --git a/lib/presentation/viewmodels/home_viewmodel.dart b/lib/presentation/viewmodels/home_viewmodel.dart index a1b1bee..226e9af 100644 --- a/lib/presentation/viewmodels/home_viewmodel.dart +++ b/lib/presentation/viewmodels/home_viewmodel.dart @@ -13,10 +13,12 @@ class HomeViewModel extends PaginatedWorksViewModel { bool _filterPanelExpanded = false; bool _hasSubtitle = false; FilterState _filterState = const FilterState(); + int _activeTabIndex = 0; bool get filterPanelExpanded => _filterPanelExpanded; bool get hasSubtitle => _hasSubtitle; FilterState get filterState => _filterState; + int get activeTabIndex => _activeTabIndex; HomeViewModel() : super(GetIt.I()); @@ -106,6 +108,17 @@ class HomeViewModel extends PaginatedWorksViewModel { } } + void setActiveTabIndex(int index) { + if (index == _activeTabIndex) { + return; + } + _activeTabIndex = index; + if (_activeTabIndex != 0 && _filterPanelExpanded) { + _filterPanelExpanded = false; + } + notifyListeners(); + } + @override String get pageName => '主页'; diff --git a/lib/screens/contents/home_content.dart b/lib/screens/contents/home_content.dart index d3c2127..5190f65 100644 --- a/lib/screens/contents/home_content.dart +++ b/lib/screens/contents/home_content.dart @@ -1,5 +1,7 @@ import 'package:asmrapp/presentation/layouts/work_layout_strategy.dart'; import 'package:asmrapp/presentation/viewmodels/home_viewmodel.dart'; +import 'package:asmrapp/l10n/l10n.dart'; +import 'package:asmrapp/widgets/download/download_progress_panel.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'; @@ -50,51 +52,91 @@ class _HomeContentState extends State super.build(context); return Consumer( builder: (context, viewModel, child) { - return Stack( + final isWorksTab = viewModel.activeTabIndex == 0; + return Column( children: [ - // 作品列表 - EnhancedWorkGridView( - works: viewModel.works, - isLoading: viewModel.isLoading, - error: viewModel.error, - currentPage: viewModel.currentPage, - totalPages: viewModel.totalPages, - onPageChanged: (page) => viewModel.loadPage(page), - onRefresh: () => viewModel.refresh(), - onRetry: () => viewModel.refresh(), - layoutStrategy: _layoutStrategy, - scrollController: _scrollController, + Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 12, 0), + child: Row( + children: [ + Expanded( + child: ChoiceChip( + label: Text(context.l10n.homeTabWorks), + selected: isWorksTab, + onSelected: (_) => viewModel.setActiveTabIndex(0), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ChoiceChip( + label: Text(context.l10n.homeTabDownloads), + selected: !isWorksTab, + onSelected: (_) => viewModel.setActiveTabIndex(1), + ), + ), + ], + ), ), - // 筛选面板 - Positioned( - top: 0, - left: 0, - right: 0, - child: AnimatedSlide( - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, - offset: Offset(0, viewModel.filterPanelExpanded ? 0 : -1), - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 8, - spreadRadius: 1, - offset: const Offset(0, 1), + const SizedBox(height: 8), + Expanded( + child: IndexedStack( + index: viewModel.activeTabIndex, + children: [ + Stack( + children: [ + // 作品列表 + EnhancedWorkGridView( + works: viewModel.works, + isLoading: viewModel.isLoading, + error: viewModel.error, + currentPage: viewModel.currentPage, + totalPages: viewModel.totalPages, + onPageChanged: (page) => viewModel.loadPage(page), + onRefresh: () => viewModel.refresh(), + onRetry: () => viewModel.refresh(), + layoutStrategy: _layoutStrategy, + scrollController: _scrollController, + ), + // 筛选面板 + Positioned( + top: 0, + left: 0, + right: 0, + child: AnimatedSlide( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + offset: Offset( + 0, + viewModel.filterPanelExpanded ? 0 : -1, + ), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 8, + spreadRadius: 1, + offset: const Offset(0, 1), + ), + ], + ), + child: FilterPanel( + hasSubtitle: viewModel.hasSubtitle, + onSubtitleChanged: viewModel.updateSubtitle, + orderField: viewModel.filterState.orderField, + isDescending: viewModel.filterState.isDescending, + onOrderFieldChanged: viewModel.updateOrderField, + onSortDirectionChanged: + viewModel.updateSortDirection, + ), + ), + ), ), ], ), - child: FilterPanel( - hasSubtitle: viewModel.hasSubtitle, - onSubtitleChanged: viewModel.updateSubtitle, - orderField: viewModel.filterState.orderField, - isDescending: viewModel.filterState.isDescending, - onOrderFieldChanged: viewModel.updateOrderField, - onSortDirectionChanged: viewModel.updateSortDirection, - ), - ), + const DownloadProgressPanel(), + ], ), ), ], diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 17fcfc1..5f00218 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -100,9 +100,12 @@ class _MainScreenState extends State { ], child: Builder( builder: (context) { + final homeViewModel = context.watch(); // 根据当前页面获取对应的总数 final totalCount = _currentIndex == 1 - ? context.watch().pagination?.totalCount + ? homeViewModel.activeTabIndex == 0 + ? homeViewModel.pagination?.totalCount + : null : _currentIndex == 2 ? context.watch().pagination?.totalCount : _currentIndex == 3 @@ -111,11 +114,16 @@ class _MainScreenState extends State { final titles = [ context.l10n.navigationFavorites, - context.l10n.navigationHome, + homeViewModel.activeTabIndex == 0 + ? context.l10n.navigationHome + : context.l10n.navigationDownloadProgress, context.l10n.navigationForYou, context.l10n.navigationPopularWorks, ]; final baseTitle = titles[_currentIndex]; + final showFilterButton = _currentIndex == 1 + ? homeViewModel.activeTabIndex == 0 + : _currentIndex == 2 || _currentIndex == 3; // 构建标题文本 final title = totalCount != null @@ -126,18 +134,19 @@ class _MainScreenState extends State { appBar: AppBar( title: Text(title), actions: [ - IconButton( - icon: const Icon(Icons.filter_list), - onPressed: () { - if (_currentIndex == 1) { - context.read().toggleFilterPanel(); - } else if (_currentIndex == 2) { - context.read().toggleFilterPanel(); - } else if (_currentIndex == 3) { - context.read().toggleFilterPanel(); - } - }, - ), + if (showFilterButton) + IconButton( + icon: const Icon(Icons.filter_list), + onPressed: () { + if (_currentIndex == 1) { + context.read().toggleFilterPanel(); + } else if (_currentIndex == 2) { + context.read().toggleFilterPanel(); + } else if (_currentIndex == 3) { + context.read().toggleFilterPanel(); + } + }, + ), IconButton( icon: const Icon(Icons.search), onPressed: () { diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart new file mode 100644 index 0000000..43d86b9 --- /dev/null +++ b/lib/screens/settings/settings_screen.dart @@ -0,0 +1,184 @@ +import 'package:asmrapp/core/download/download_directory_controller.dart'; +import 'package:asmrapp/l10n/l10n.dart'; +import 'package:asmrapp/screens/settings/cache_manager_screen.dart'; +import 'package:file_selector/file_selector.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class SettingsScreen extends StatefulWidget { + const SettingsScreen({super.key}); + + @override + State createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { + bool _updatingDirectory = false; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + return Scaffold( + appBar: AppBar( + title: Text(l10n.settings), + ), + body: Consumer( + builder: (context, controller, _) { + final path = controller.customDirectoryPath; + return ListView( + padding: const EdgeInsets.all(16), + children: [ + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.downloadDirectoryTitle, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Text( + l10n.downloadDirectoryDescription, + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 12), + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .surfaceContainerHighest + .withValues(alpha: 0.45), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + path?.isNotEmpty == true + ? path! + : l10n.downloadDirectoryDefaultValue, + style: Theme.of(context).textTheme.bodySmall, + ), + ), + const SizedBox(height: 8), + Text( + l10n.downloadDirectoryPermissionHint, + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 16), + Wrap( + spacing: 12, + runSpacing: 12, + children: [ + FilledButton.icon( + onPressed: _updatingDirectory + ? null + : () => _pickDownloadDirectory( + context, + controller, + ), + icon: const Icon(Icons.folder_open), + label: Text(l10n.downloadDirectoryPick), + ), + OutlinedButton.icon( + onPressed: _updatingDirectory || + !controller.hasCustomDirectory + ? null + : () => _resetDownloadDirectory( + context, + controller, + ), + icon: const Icon(Icons.undo), + label: Text(l10n.downloadDirectoryReset), + ), + ], + ), + if (controller.lastError != null) ...[ + const SizedBox(height: 12), + Text( + controller.lastError!, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ), + ], + ], + ), + ), + ), + const SizedBox(height: 12), + Card( + child: ListTile( + leading: const Icon(Icons.storage), + title: Text(l10n.cacheManager), + subtitle: Text(l10n.cacheDescription), + trailing: const Icon(Icons.chevron_right), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const CacheManagerScreen(), + ), + ); + }, + ), + ), + ], + ); + }, + ), + ); + } + + Future _pickDownloadDirectory( + BuildContext context, + DownloadDirectoryController controller, + ) async { + final l10n = context.l10n; + setState(() => _updatingDirectory = true); + try { + final selectedPath = await getDirectoryPath( + confirmButtonText: l10n.confirm, + ); + if (selectedPath == null || selectedPath.trim().isEmpty) { + return; + } + + await controller.setCustomDirectoryPath(selectedPath); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.downloadDirectoryUpdated(selectedPath))), + ); + } catch (e) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.operationFailed(e.toString()))), + ); + } finally { + if (mounted) { + setState(() => _updatingDirectory = false); + } + } + } + + Future _resetDownloadDirectory( + BuildContext context, + DownloadDirectoryController controller, + ) async { + final l10n = context.l10n; + setState(() => _updatingDirectory = true); + try { + await controller.clearCustomDirectoryPath(); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.downloadDirectoryResetSuccess)), + ); + } finally { + if (mounted) { + setState(() => _updatingDirectory = false); + } + } + } +} diff --git a/lib/widgets/detail/download_file_selection_dialog.dart b/lib/widgets/detail/download_file_selection_dialog.dart index 4064eaa..5b6ef28 100644 --- a/lib/widgets/detail/download_file_selection_dialog.dart +++ b/lib/widgets/detail/download_file_selection_dialog.dart @@ -21,11 +21,13 @@ class _DownloadFileSelectionDialogState extends State { final Set _selectedPaths = {}; late final Map _downloadableFiles; + late final Map> _folderDescendantFiles; @override void initState() { super.initState(); _downloadableFiles = _collectDownloadableFiles(widget.rootFiles); + _folderDescendantFiles = _collectFolderDescendantFiles(widget.rootFiles); } @override @@ -148,13 +150,41 @@ class _DownloadFileSelectionDialogState } final children = node.children ?? const []; + final descendantFiles = + _folderDescendantFiles[currentPath] ?? const {}; + if (descendantFiles.isEmpty) { + return const SizedBox.shrink(); + } + final folderSelectionValue = _folderSelectionValue(currentPath); + return Padding( padding: EdgeInsets.only(left: indentation), child: Theme( data: Theme.of(context).copyWith(dividerColor: Colors.transparent), child: ExpansionTile( + tilePadding: const EdgeInsets.only(left: 8, right: 8), + childrenPadding: const EdgeInsets.only(bottom: 4), leading: const Icon(Icons.folder_outlined), - title: Text(_displayName(node)), + title: Row( + children: [ + Expanded( + child: Text( + _displayName(node), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + Checkbox( + value: folderSelectionValue, + tristate: true, + onChanged: (_) => _toggleFolderSelection( + currentPath, + folderSelectionValue != true, + ), + ), + ], + ), children: _buildTreeNodes( children, parentPath: currentPath, @@ -186,7 +216,11 @@ class _DownloadFileSelectionDialogState }); }, secondary: Icon(_iconForFile(node)), - title: Text(_displayName(node)), + title: Text( + _displayName(node), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), subtitle: Text(FileSizeFormatter.format(node.size)), ), ); @@ -223,6 +257,55 @@ class _DownloadFileSelectionDialogState return result; } + Map> _collectFolderDescendantFiles( + List nodes, { + String parentPath = '', + }) { + final result = >{}; + + void visitNode(Child node, String currentParentPath, int index) { + final currentPath = _buildNodePath( + parentPath: currentParentPath, + node: node, + index: index, + ); + + if (!_isFolder(node)) { + return; + } + + final descendants = {}; + final children = node.children ?? const []; + + for (var i = 0; i < children.length; i++) { + final child = children[i]; + final childPath = _buildNodePath( + parentPath: currentPath, + node: child, + index: i, + ); + + if (_isFolder(child)) { + visitNode(child, currentPath, i); + descendants.addAll(result[childPath] ?? const {}); + continue; + } + + if (_isDownloadable(child)) { + descendants.add(childPath); + } + } + + result[currentPath] = descendants; + } + + for (var i = 0; i < nodes.length; i++) { + visitNode(nodes[i], parentPath, i); + } + + return result; + } + bool _isFolder(Child node) => node.type?.toLowerCase() == 'folder'; bool _isDownloadable(Child node) { @@ -254,7 +337,45 @@ class _DownloadFileSelectionDialogState : (node.hash?.isNotEmpty ?? false) ? node.hash! : 'item_$index'; - return parentPath.isEmpty ? segment : '$parentPath/$segment'; + final indexedSegment = '${index}_$segment'; + return parentPath.isEmpty ? indexedSegment : '$parentPath/$indexedSegment'; + } + + bool? _folderSelectionValue(String folderPath) { + final descendants = _folderDescendantFiles[folderPath]; + if (descendants == null || descendants.isEmpty) { + return false; + } + + var selectedCount = 0; + for (final path in descendants) { + if (_selectedPaths.contains(path)) { + selectedCount++; + } + } + + if (selectedCount == 0) { + return false; + } + if (selectedCount == descendants.length) { + return true; + } + return null; + } + + void _toggleFolderSelection(String folderPath, bool shouldSelectAll) { + final descendants = _folderDescendantFiles[folderPath]; + if (descendants == null || descendants.isEmpty) { + return; + } + + setState(() { + if (shouldSelectAll) { + _selectedPaths.addAll(descendants); + } else { + _selectedPaths.removeAll(descendants); + } + }); } String _displayName(Child node) { diff --git a/lib/widgets/download/download_progress_panel.dart b/lib/widgets/download/download_progress_panel.dart new file mode 100644 index 0000000..da9b357 --- /dev/null +++ b/lib/widgets/download/download_progress_panel.dart @@ -0,0 +1,220 @@ +import 'package:asmrapp/core/download/download_progress_manager.dart'; +import 'package:asmrapp/core/download/download_task.dart'; +import 'package:asmrapp/l10n/l10n.dart'; +import 'package:asmrapp/utils/file_size_formatter.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class DownloadProgressPanel extends StatelessWidget { + const DownloadProgressPanel({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, manager, _) { + final activeTasks = manager.activeTasks; + final finishedTasks = manager.finishedTasks; + + if (activeTasks.isEmpty && finishedTasks.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text( + context.l10n.downloadProgressEmpty, + textAlign: TextAlign.center, + ), + ), + ); + } + + return Column( + children: [ + if (finishedTasks.isNotEmpty) + Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), + child: TextButton.icon( + onPressed: manager.clearFinished, + icon: const Icon(Icons.cleaning_services_outlined), + label: Text(context.l10n.downloadProgressClearFinished), + ), + ), + ), + Expanded( + child: ListView( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 20), + children: [ + if (activeTasks.isNotEmpty) ...[ + _ProgressSectionTitle( + title: context.l10n.downloadProgressActiveSection, + ), + ...activeTasks.map((task) => _TaskCard(task: task)), + const SizedBox(height: 12), + ], + if (finishedTasks.isNotEmpty) ...[ + _ProgressSectionTitle( + title: context.l10n.downloadProgressHistorySection, + ), + ...finishedTasks.map((task) => _TaskCard(task: task)), + ], + ], + ), + ), + ], + ); + }, + ); + } +} + +class _ProgressSectionTitle extends StatelessWidget { + final String title; + + const _ProgressSectionTitle({required this.title}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(4, 4, 4, 8), + child: Text( + title, + style: Theme.of(context).textTheme.titleSmall, + ), + ); + } +} + +class _TaskCard extends StatelessWidget { + final DownloadTask task; + + const _TaskCard({required this.task}); + + @override + Widget build(BuildContext context) { + final statusColor = _statusColor(context, task.status); + final statusIcon = _statusIcon(task.status); + final canShowKnownProgress = + task.totalBytes > 0 && task.status == DownloadTaskStatus.running; + final showIndeterminate = + task.totalBytes <= 0 && task.status == DownloadTaskStatus.running; + + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 28, + height: 28, + margin: const EdgeInsets.only(top: 2), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.14), + borderRadius: BorderRadius.circular(7), + ), + child: Icon( + statusIcon, + size: 16, + color: statusColor, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + task.fileName, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(height: 2), + Text( + task.workTitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 6), + LinearProgressIndicator( + value: showIndeterminate ? null : task.progress, + minHeight: 4, + ), + const SizedBox(height: 6), + Text( + _buildProgressText(context, task, canShowKnownProgress), + style: Theme.of(context).textTheme.bodySmall, + ), + if (task.status == DownloadTaskStatus.failed && + task.errorMessage != null) ...[ + const SizedBox(height: 4), + Text( + task.errorMessage!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 12, + ), + ), + ], + ], + ), + ), + ], + ), + ), + ); + } + + String _buildProgressText( + BuildContext context, + DownloadTask task, + bool canShowKnownProgress, + ) { + switch (task.status) { + case DownloadTaskStatus.queued: + return context.l10n.downloadStatusQueued; + case DownloadTaskStatus.running: + if (canShowKnownProgress) { + final percent = (task.progress * 100).toStringAsFixed(0); + return '$percent% · ${FileSizeFormatter.format(task.receivedBytes)} / ' + '${FileSizeFormatter.format(task.totalBytes)}'; + } + return context.l10n.downloadStatusRunning; + case DownloadTaskStatus.completed: + return context.l10n.downloadStatusCompleted; + case DownloadTaskStatus.failed: + return context.l10n.downloadStatusFailed; + } + } + + IconData _statusIcon(DownloadTaskStatus status) { + switch (status) { + case DownloadTaskStatus.queued: + return Icons.schedule; + case DownloadTaskStatus.running: + return Icons.downloading_rounded; + case DownloadTaskStatus.completed: + return Icons.check_circle_rounded; + case DownloadTaskStatus.failed: + return Icons.error_rounded; + } + } + + Color _statusColor(BuildContext context, DownloadTaskStatus status) { + switch (status) { + case DownloadTaskStatus.queued: + return Theme.of(context).colorScheme.primary; + case DownloadTaskStatus.running: + return Theme.of(context).colorScheme.primary; + case DownloadTaskStatus.completed: + return Colors.green.shade600; + case DownloadTaskStatus.failed: + return Theme.of(context).colorScheme.error; + } + } +} diff --git a/lib/widgets/drawer_menu.dart b/lib/widgets/drawer_menu.dart index aa0b5bd..cfd93f7 100644 --- a/lib/widgets/drawer_menu.dart +++ b/lib/widgets/drawer_menu.dart @@ -5,6 +5,7 @@ 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/screens/settings/settings_screen.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:provider/provider.dart'; @@ -79,7 +80,8 @@ class DrawerMenu extends StatelessWidget { onTap: () async { Navigator.pop(context); if (authVM.isLoggedIn) { - final shouldLogout = await _showLogoutConfirmDialog(context); + final shouldLogout = + await _showLogoutConfirmDialog(context); if (shouldLogout) { await authVM.logout(); } @@ -116,7 +118,12 @@ class DrawerMenu extends StatelessWidget { title: Text(context.l10n.settings), onTap: () { Navigator.pop(context); - // TODO: 导航到设置页面 + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SettingsScreen(), + ), + ); }, ), ListTile( diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 4132f88..b637f77 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 4332e43..f3570f9 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux media_kit_libs_linux url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 54f3c3f..9bb8652 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import audio_service import audio_session +import file_selector_macos import just_audio import package_info_plus import shared_preferences_foundation @@ -17,6 +18,7 @@ import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 559ebb5..f76b69d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -217,6 +217,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" crypto: dependency: "direct main" description: @@ -297,6 +305,70 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_selector: + dependency: "direct main" + description: + name: file_selector + sha256: bd15e43e9268db636b53eeaca9f56324d1622af30e5c34d6e267649758c84d9a + url: "https://pub.dev" + source: hosted + version: "1.1.0" + file_selector_android: + dependency: transitive + description: + name: file_selector_android + sha256: "51e8fd0446de75e4b62c065b76db2210c704562d072339d333bd89c57a7f8a7c" + url: "https://pub.dev" + source: hosted + version: "0.5.2+4" + file_selector_ios: + dependency: transitive + description: + name: file_selector_ios + sha256: e2ecf2885c121691ce13b60db3508f53c01f869fb6e8dc5c1cfa771e4c46aeca + url: "https://pub.dev" + source: hosted + version: "0.5.3+5" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://pub.dev" + source: hosted + version: "0.9.5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + file_selector_web: + dependency: transitive + description: + name: file_selector_web + sha256: c4c0ea4224d97a60a7067eca0c8fd419e708ff830e0c83b11a48faf566cec3e7 + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" fixnum: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8ac64b9..c0dba34 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,6 +57,7 @@ dependencies: shared_preferences: ^2.2.2 flutter_cache_manager: ^3.4.1 permission_handler: ^12.0.1 + file_selector: ^1.0.3 scrollable_positioned_list: ^0.3.8 marquee: ^2.3.0 wakelock_plus: ^1.2.8 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index c515fc3..1f7a72f 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,11 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MediaKitLibsWindowsAudioPluginCApi")); PermissionHandlerWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 8e73ea7..c5f2556 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows media_kit_libs_windows_audio permission_handler_windows url_launcher_windows From e9d1a1886de042e166b9707743e27646370f80af Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Thu, 19 Feb 2026 00:30:15 +0900 Subject: [PATCH 12/30] feat: remove unused activeTabIndex and related logic from HomeViewModel; update HomeContent and MainScreen to reflect changes --- .../viewmodels/home_viewmodel.dart | 13 -- lib/screens/contents/home_content.dart | 125 ++++++------------ lib/screens/main_screen.dart | 21 +-- pubspec.lock | 8 +- 4 files changed, 59 insertions(+), 108 deletions(-) diff --git a/lib/presentation/viewmodels/home_viewmodel.dart b/lib/presentation/viewmodels/home_viewmodel.dart index 226e9af..a1b1bee 100644 --- a/lib/presentation/viewmodels/home_viewmodel.dart +++ b/lib/presentation/viewmodels/home_viewmodel.dart @@ -13,12 +13,10 @@ class HomeViewModel extends PaginatedWorksViewModel { bool _filterPanelExpanded = false; bool _hasSubtitle = false; FilterState _filterState = const FilterState(); - int _activeTabIndex = 0; bool get filterPanelExpanded => _filterPanelExpanded; bool get hasSubtitle => _hasSubtitle; FilterState get filterState => _filterState; - int get activeTabIndex => _activeTabIndex; HomeViewModel() : super(GetIt.I()); @@ -108,17 +106,6 @@ class HomeViewModel extends PaginatedWorksViewModel { } } - void setActiveTabIndex(int index) { - if (index == _activeTabIndex) { - return; - } - _activeTabIndex = index; - if (_activeTabIndex != 0 && _filterPanelExpanded) { - _filterPanelExpanded = false; - } - notifyListeners(); - } - @override String get pageName => '主页'; diff --git a/lib/screens/contents/home_content.dart b/lib/screens/contents/home_content.dart index 5190f65..97ac65f 100644 --- a/lib/screens/contents/home_content.dart +++ b/lib/screens/contents/home_content.dart @@ -1,7 +1,5 @@ import 'package:asmrapp/presentation/layouts/work_layout_strategy.dart'; import 'package:asmrapp/presentation/viewmodels/home_viewmodel.dart'; -import 'package:asmrapp/l10n/l10n.dart'; -import 'package:asmrapp/widgets/download/download_progress_panel.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'; @@ -52,91 +50,54 @@ class _HomeContentState extends State super.build(context); return Consumer( builder: (context, viewModel, child) { - final isWorksTab = viewModel.activeTabIndex == 0; - return Column( + return Stack( children: [ - Padding( - padding: const EdgeInsets.fromLTRB(12, 8, 12, 0), - child: Row( - children: [ - Expanded( - child: ChoiceChip( - label: Text(context.l10n.homeTabWorks), - selected: isWorksTab, - onSelected: (_) => viewModel.setActiveTabIndex(0), - ), - ), - const SizedBox(width: 8), - Expanded( - child: ChoiceChip( - label: Text(context.l10n.homeTabDownloads), - selected: !isWorksTab, - onSelected: (_) => viewModel.setActiveTabIndex(1), - ), - ), - ], - ), + // 作品列表 + EnhancedWorkGridView( + works: viewModel.works, + isLoading: viewModel.isLoading, + error: viewModel.error, + currentPage: viewModel.currentPage, + totalPages: viewModel.totalPages, + onPageChanged: (page) => viewModel.loadPage(page), + onRefresh: () => viewModel.refresh(), + onRetry: () => viewModel.refresh(), + layoutStrategy: _layoutStrategy, + scrollController: _scrollController, ), - const SizedBox(height: 8), - Expanded( - child: IndexedStack( - index: viewModel.activeTabIndex, - children: [ - Stack( - children: [ - // 作品列表 - EnhancedWorkGridView( - works: viewModel.works, - isLoading: viewModel.isLoading, - error: viewModel.error, - currentPage: viewModel.currentPage, - totalPages: viewModel.totalPages, - onPageChanged: (page) => viewModel.loadPage(page), - onRefresh: () => viewModel.refresh(), - onRetry: () => viewModel.refresh(), - layoutStrategy: _layoutStrategy, - scrollController: _scrollController, - ), - // 筛选面板 - Positioned( - top: 0, - left: 0, - right: 0, - child: AnimatedSlide( - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, - offset: Offset( - 0, - viewModel.filterPanelExpanded ? 0 : -1, - ), - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 8, - spreadRadius: 1, - offset: const Offset(0, 1), - ), - ], - ), - child: FilterPanel( - hasSubtitle: viewModel.hasSubtitle, - onSubtitleChanged: viewModel.updateSubtitle, - orderField: viewModel.filterState.orderField, - isDescending: viewModel.filterState.isDescending, - onOrderFieldChanged: viewModel.updateOrderField, - onSortDirectionChanged: - viewModel.updateSortDirection, - ), - ), - ), + // 筛选面板 + Positioned( + top: 0, + left: 0, + right: 0, + child: AnimatedSlide( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + offset: Offset( + 0, + viewModel.filterPanelExpanded ? 0 : -1, + ), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 8, + spreadRadius: 1, + offset: const Offset(0, 1), ), ], ), - const DownloadProgressPanel(), - ], + child: FilterPanel( + hasSubtitle: viewModel.hasSubtitle, + onSubtitleChanged: viewModel.updateSubtitle, + orderField: viewModel.filterState.orderField, + isDescending: viewModel.filterState.isDescending, + onOrderFieldChanged: viewModel.updateOrderField, + onSortDirectionChanged: viewModel.updateSortDirection, + ), + ), ), ), ], diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 5f00218..be79477 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -10,6 +10,7 @@ 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/download/download_progress_panel.dart'; import 'package:asmrapp/widgets/mini_player/mini_player.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -47,6 +48,7 @@ class _MainScreenState extends State { HomeContent(), RecommendContent(), PopularContent(), + DownloadProgressPanel(), ]; @override @@ -103,9 +105,7 @@ class _MainScreenState extends State { final homeViewModel = context.watch(); // 根据当前页面获取对应的总数 final totalCount = _currentIndex == 1 - ? homeViewModel.activeTabIndex == 0 - ? homeViewModel.pagination?.totalCount - : null + ? homeViewModel.pagination?.totalCount : _currentIndex == 2 ? context.watch().pagination?.totalCount : _currentIndex == 3 @@ -114,16 +114,14 @@ class _MainScreenState extends State { final titles = [ context.l10n.navigationFavorites, - homeViewModel.activeTabIndex == 0 - ? context.l10n.navigationHome - : context.l10n.navigationDownloadProgress, + context.l10n.navigationHome, context.l10n.navigationForYou, context.l10n.navigationPopularWorks, + context.l10n.navigationDownloadProgress, ]; final baseTitle = titles[_currentIndex]; - final showFilterButton = _currentIndex == 1 - ? homeViewModel.activeTabIndex == 0 - : _currentIndex == 2 || _currentIndex == 3; + final showFilterButton = + _currentIndex == 1 || _currentIndex == 2 || _currentIndex == 3; // 构建标题文本 final title = totalCount != null @@ -199,6 +197,11 @@ class _MainScreenState extends State { selectedIcon: const Icon(Icons.trending_up), label: context.l10n.navigationPopularWorks, ), + NavigationDestination( + icon: const Icon(Icons.download_outlined), + selectedIcon: const Icon(Icons.download), + label: context.l10n.navigationDownloadProgress, + ), ], ), ], diff --git a/pubspec.lock b/pubspec.lock index f76b69d..a787c44 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -1158,10 +1158,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.8" typed_data: dependency: transitive description: From df9a9218f832da68ff701eb492d83b3b54bf7d98 Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Thu, 19 Feb 2026 00:42:28 +0900 Subject: [PATCH 13/30] feat: refactor download functionality to use DownloadRequestItem; integrate background downloader and update related components --- lib/core/download/download_request_item.dart | 11 ++ lib/data/services/api_service.dart | 28 ---- lib/main.dart | 2 +- .../viewmodels/detail_viewmodel.dart | 128 +++++++++++++++--- lib/screens/detail_screen.dart | 3 +- .../download_file_selection_dialog.dart | 29 ++-- pubspec.lock | 16 ++- pubspec.yaml | 1 + 8 files changed, 159 insertions(+), 59 deletions(-) create mode 100644 lib/core/download/download_request_item.dart diff --git a/lib/core/download/download_request_item.dart b/lib/core/download/download_request_item.dart new file mode 100644 index 0000000..8b2891a --- /dev/null +++ b/lib/core/download/download_request_item.dart @@ -0,0 +1,11 @@ +import 'package:asmrapp/data/models/files/child.dart'; + +class DownloadRequestItem { + final Child file; + final List relativeDirectories; + + const DownloadRequestItem({ + required this.file, + required this.relativeDirectories, + }); +} diff --git a/lib/data/services/api_service.dart b/lib/data/services/api_service.dart index 508dd67..28f7636 100644 --- a/lib/data/services/api_service.dart +++ b/lib/data/services/api_service.dart @@ -94,34 +94,6 @@ class ApiService { } } - /// 下载文件到指定路径 - Future downloadFileToPath( - String url, - String savePath, { - CancelToken? cancelToken, - ProgressCallback? onReceiveProgress, - }) async { - try { - final response = await _dio.download( - url, - savePath, - cancelToken: cancelToken, - onReceiveProgress: onReceiveProgress, - ); - - final statusCode = response.statusCode ?? 0; - if (statusCode < 200 || statusCode >= 300) { - throw Exception('下载失败: $statusCode'); - } - } on DioException catch (e) { - AppLogger.error('文件下载失败', e, e.stackTrace); - throw Exception('网络请求失败: ${e.message}'); - } catch (e, stackTrace) { - AppLogger.error('文件下载异常', e, stackTrace); - throw Exception('文件下载失败: $e'); - } - } - /// 获取作品列表 Future getWorks({ int page = 1, diff --git a/lib/main.dart b/lib/main.dart index b185bad..b856b0d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,6 @@ -import 'package:asmrapp/core/theme/app_theme.dart'; import 'package:asmrapp/core/download/download_directory_controller.dart'; import 'package:asmrapp/core/download/download_progress_manager.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'; diff --git a/lib/presentation/viewmodels/detail_viewmodel.dart b/lib/presentation/viewmodels/detail_viewmodel.dart index f39fd86..76323ed 100644 --- a/lib/presentation/viewmodels/detail_viewmodel.dart +++ b/lib/presentation/viewmodels/detail_viewmodel.dart @@ -1,7 +1,9 @@ import 'dart:io'; +import 'package:asmrapp/core/download/download_request_item.dart'; import 'package:asmrapp/data/models/playlists_with_exist_statu/pagination.dart'; import 'package:asmrapp/data/models/playlists_with_exist_statu/playlist.dart'; +import 'package:asmrapp/data/repositories/auth_repository.dart'; import 'package:get_it/get_it.dart'; import 'package:flutter/material.dart'; import 'package:asmrapp/common/utils/file_preview_utils.dart'; @@ -17,6 +19,7 @@ import 'package:asmrapp/data/models/mark_status.dart'; import 'package:asmrapp/widgets/detail/mark_selection_dialog.dart'; import 'package:asmrapp/core/download/download_directory_controller.dart'; import 'package:asmrapp/core/download/download_progress_manager.dart'; +import 'package:background_downloader/background_downloader.dart'; import 'package:dio/dio.dart'; import 'package:asmrapp/l10n/l10n.dart'; import 'package:asmrapp/common/extensions/mark_status_localizations.dart'; @@ -75,6 +78,8 @@ class DetailViewModel extends ChangeNotifier { late final IAudioPlayerService _audioService; late final DownloadDirectoryController _downloadDirectoryController; late final DownloadProgressManager _downloadProgressManager; + late final AuthRepository _authRepository; + late final FileDownloader _fileDownloader; final Work work; Files? _files; @@ -114,6 +119,8 @@ class DetailViewModel extends ChangeNotifier { _apiService = GetIt.I(); _downloadDirectoryController = GetIt.I(); _downloadProgressManager = GetIt.I(); + _authRepository = GetIt.I(); + _fileDownloader = FileDownloader(); loadRecommendationsPreview(); } @@ -377,7 +384,9 @@ class DetailViewModel extends ChangeNotifier { } } - Future downloadFiles(List files) async { + Future downloadFiles( + List files, + ) async { _downloadingFiles = true; notifyListeners(); @@ -386,21 +395,28 @@ class DetailViewModel extends ChangeNotifier { var saveDirectoryPath = ''; try { + await _fileDownloader.ready; final rootDirectory = await _downloadDirectoryController.resolveDownloadRootDirectory(); final saveDirectory = await _resolveWorkDirectory(rootDirectory); saveDirectoryPath = saveDirectory.path; + final downloadHeaders = await _buildDownloadHeaders(); - for (final file in files) { + for (final requestItem in files) { + final file = requestItem.file; final downloadUrl = file.mediaDownloadUrl; if (downloadUrl == null || downloadUrl.isEmpty) { failedCount++; continue; } + final targetDirectory = await _resolveTargetDirectory( + saveDirectory, + requestItem.relativeDirectories, + ); final safeFileName = _sanitizeFileName(file.title); final savePath = - await _createUniqueSavePath(saveDirectory, safeFileName); + await _createUniqueSavePath(targetDirectory, safeFileName); final taskId = _downloadProgressManager.createTask( workId: work.id, workTitle: _resolveWorkTitle(), @@ -410,20 +426,43 @@ class DetailViewModel extends ChangeNotifier { try { _downloadProgressManager.markStarted(taskId); - await _apiService.downloadFileToPath( - downloadUrl, - savePath, - cancelToken: _cancelToken, - onReceiveProgress: (receivedBytes, totalBytes) { - _downloadProgressManager.updateProgress( - taskId, - receivedBytes, - totalBytes, - ); + final expectedBytes = (file.size ?? 0) > 0 ? file.size! : 0; + final statusUpdate = await _fileDownloader.download( + DownloadTask( + taskId: taskId, + url: downloadUrl, + filename: _fileNameFromPath(savePath), + directory: targetDirectory.path, + baseDirectory: BaseDirectory.root, + headers: downloadHeaders, + updates: Updates.statusAndProgress, + ), + onProgress: (progress) { + if (expectedBytes > 0) { + final receivedBytes = + (expectedBytes * progress.clamp(0.0, 1.0)).round(); + _downloadProgressManager.updateProgress( + taskId, + receivedBytes, + expectedBytes, + ); + return; + } + _downloadProgressManager.updateProgress(taskId, 0, 0); }, ); - _downloadProgressManager.markCompleted(taskId); - successCount++; + + if (statusUpdate.status == TaskStatus.complete) { + _downloadProgressManager.markCompleted(taskId); + successCount++; + continue; + } + + final errorMessage = statusUpdate.exception?.description ?? + '下载状态异常: ${statusUpdate.status.name}'; + _downloadProgressManager.markFailed(taskId, errorMessage); + failedCount++; + AppLogger.error('下载文件失败: ${file.title}', errorMessage); } catch (e) { _downloadProgressManager.markFailed(taskId, e); failedCount++; @@ -444,6 +483,15 @@ class DetailViewModel extends ChangeNotifier { ); } + Future> _buildDownloadHeaders() async { + final authData = await _authRepository.getAuthData(); + final token = authData?.token?.trim(); + if (token == null || token.isEmpty) { + return const {}; + } + return {'Authorization': 'Bearer $token'}; + } + Future _resolveWorkDirectory(Directory rootDirectory) async { final workFolderName = _sanitizeFileName( (work.sourceId?.trim().isNotEmpty ?? false) @@ -460,6 +508,33 @@ class DetailViewModel extends ChangeNotifier { return workDirectory; } + Future _resolveTargetDirectory( + Directory workDirectory, + List relativeDirectories, + ) async { + if (relativeDirectories.isEmpty) { + return workDirectory; + } + + final safeSegments = relativeDirectories + .map((segment) => _sanitizePathSegment(segment, fallback: 'folder')) + .where((segment) => segment.isNotEmpty) + .toList(growable: false); + if (safeSegments.isEmpty) { + return workDirectory; + } + + var targetPath = workDirectory.path; + for (final segment in safeSegments) { + targetPath = '$targetPath${Platform.pathSeparator}$segment'; + } + final targetDirectory = Directory(targetPath); + if (!await targetDirectory.exists()) { + await targetDirectory.create(recursive: true); + } + return targetDirectory; + } + Future _createUniqueSavePath( Directory directory, String fileName, @@ -481,11 +556,30 @@ class DetailViewModel extends ChangeNotifier { } String _sanitizeFileName(String? original) { + return _sanitizePathSegment(original, fallback: 'file'); + } + + String _fileNameFromPath(String fullPath) { + final segments = fullPath.split(RegExp(r'[\\/]')); + if (segments.isEmpty || segments.last.isEmpty) { + return 'file'; + } + return segments.last; + } + + String _sanitizePathSegment( + String? original, { + required String fallback, + }) { final normalized = (original ?? '').trim(); if (normalized.isEmpty) { - return 'file'; + return fallback; + } + final sanitized = normalized.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_'); + if (sanitized == '.' || sanitized == '..') { + return fallback; } - return normalized.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_'); + return sanitized; } String _resolveWorkTitle() { diff --git a/lib/screens/detail_screen.dart b/lib/screens/detail_screen.dart index e38bae4..54f8747 100644 --- a/lib/screens/detail_screen.dart +++ b/lib/screens/detail_screen.dart @@ -1,4 +1,5 @@ import 'package:asmrapp/common/utils/file_preview_utils.dart'; +import 'package:asmrapp/core/download/download_request_item.dart'; import 'package:asmrapp/data/models/files/child.dart'; import 'package:asmrapp/data/models/works/work.dart'; import 'package:asmrapp/l10n/l10n.dart'; @@ -176,7 +177,7 @@ class DetailScreen extends StatelessWidget { return; } - final selectedFiles = await showDialog>( + final selectedFiles = await showDialog>( context: context, builder: (dialogContext) => DownloadFileSelectionDialog( rootFiles: files, diff --git a/lib/widgets/detail/download_file_selection_dialog.dart b/lib/widgets/detail/download_file_selection_dialog.dart index 5b6ef28..696241b 100644 --- a/lib/widgets/detail/download_file_selection_dialog.dart +++ b/lib/widgets/detail/download_file_selection_dialog.dart @@ -1,4 +1,5 @@ import 'package:asmrapp/common/utils/file_preview_utils.dart'; +import 'package:asmrapp/core/download/download_request_item.dart'; import 'package:asmrapp/data/models/files/child.dart'; import 'package:asmrapp/l10n/l10n.dart'; import 'package:asmrapp/utils/file_size_formatter.dart'; @@ -20,7 +21,7 @@ class DownloadFileSelectionDialog extends StatefulWidget { class _DownloadFileSelectionDialogState extends State { final Set _selectedPaths = {}; - late final Map _downloadableFiles; + late final Map _downloadableFiles; late final Map> _folderDescendantFiles; @override @@ -105,7 +106,7 @@ class _DownloadFileSelectionDialogState : () { final selectedFiles = _selectedPaths .map((path) => _downloadableFiles[path]) - .whereType() + .whereType() .toList(growable: false); Navigator.of(context).pop(selectedFiles); }, @@ -226,11 +227,12 @@ class _DownloadFileSelectionDialogState ); } - Map _collectDownloadableFiles( + Map _collectDownloadableFiles( List nodes, { String parentPath = '', + List parentDirectories = const [], }) { - final result = {}; + final result = {}; for (var i = 0; i < nodes.length; i++) { final node = nodes[i]; final currentPath = _buildNodePath( @@ -245,13 +247,20 @@ class _DownloadFileSelectionDialogState _collectDownloadableFiles( children, parentPath: currentPath, + parentDirectories: [ + ...parentDirectories, + _buildStorageSegment(node, index: i), + ], ), ); continue; } if (_isDownloadable(node)) { - result[currentPath] = node; + result[currentPath] = DownloadRequestItem( + file: node, + relativeDirectories: List.from(parentDirectories), + ); } } return result; @@ -331,14 +340,18 @@ class _DownloadFileSelectionDialogState required Child node, required int index, }) { + final segment = _buildStorageSegment(node, index: index); + final indexedSegment = '${index}_$segment'; + return parentPath.isEmpty ? indexedSegment : '$parentPath/$indexedSegment'; + } + + String _buildStorageSegment(Child node, {required int index}) { final title = node.title?.trim(); - final segment = (title != null && title.isNotEmpty) + return (title != null && title.isNotEmpty) ? title : (node.hash?.isNotEmpty ?? false) ? node.hash! : 'item_$index'; - final indexedSegment = '${index}_$segment'; - return parentPath.isEmpty ? indexedSegment : '$parentPath/$indexedSegment'; } bool? _folderSelectionValue(String folderPath) { diff --git a/pubspec.lock b/pubspec.lock index a787c44..5c1ac0e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -73,6 +73,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.2" + background_downloader: + dependency: "direct main" + description: + name: background_downloader + sha256: "2ea5322fe836c0aaf96aefd29ef1936771c71927f687cf18168dcc119666a45f" + url: "https://pub.dev" + source: hosted + version: "9.5.2" boolean_selector: dependency: transitive description: @@ -157,10 +165,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -1158,10 +1166,10 @@ packages: dependency: transitive description: name: test_api - sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.9" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c0dba34..431d9bc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,6 +62,7 @@ dependencies: marquee: ^2.3.0 wakelock_plus: ^1.2.8 url_launcher: ^6.3.2 + background_downloader: ^9.5.2 dev_dependencies: flutter_test: From 3ed909a2a95f9d43ea9a5788ab7157ef77c97c1d Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Thu, 19 Feb 2026 00:45:14 +0900 Subject: [PATCH 14/30] feat: add onSelected handler to PopupMenuButton in FilterPanel for order field changes --- lib/widgets/filter/filter_panel.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/widgets/filter/filter_panel.dart b/lib/widgets/filter/filter_panel.dart index fba731a..f9d654d 100644 --- a/lib/widgets/filter/filter_panel.dart +++ b/lib/widgets/filter/filter_panel.dart @@ -114,6 +114,7 @@ class FilterPanel extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), child: PopupMenuButton( + onSelected: onOrderFieldChanged, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), From 4a624ef04d058922155b5bb9b4c8b1b13da5beb3 Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Thu, 19 Feb 2026 00:50:01 +0900 Subject: [PATCH 15/30] feat: refactor audio player service and related components; remove unused playlist references and improve state listener initialization --- lib/core/audio/audio_player_service.dart | 3 - lib/core/audio/cache/audio_cache_manager.dart | 1 + .../controllers/playback_controller.dart | 6 +- lib/core/audio/models/playback_context.dart | 6 -- .../audio/state/playback_state_manager.dart | 60 +++++++----- lib/core/audio/utils/playlist_builder.dart | 15 +-- lib/widgets/detail/mark_selection_dialog.dart | 94 ++++++++++--------- 7 files changed, 90 insertions(+), 95 deletions(-) diff --git a/lib/core/audio/audio_player_service.dart b/lib/core/audio/audio_player_service.dart index b4f61ad..6867643 100644 --- a/lib/core/audio/audio_player_service.dart +++ b/lib/core/audio/audio_player_service.dart @@ -16,7 +16,6 @@ import './events/playback_event_hub.dart'; class AudioPlayerService implements IAudioPlayerService { late final AudioPlayer _player; AudioNotificationService? _notificationService; - late final ConcatenatingAudioSource _playlist; late final PlaybackStateManager _stateManager; late final PlaybackController _playbackController; late final Future _initialization; @@ -61,7 +60,6 @@ class AudioPlayerService implements IAudioPlayerService { _eventHub, ); } - _playlist = ConcatenatingAudioSource(children: []); _stateManager = PlaybackStateManager( player: _player, @@ -72,7 +70,6 @@ class AudioPlayerService implements IAudioPlayerService { _playbackController = PlaybackController( player: _player, stateManager: _stateManager, - playlist: _playlist, ); if (_supportsAudioSession) { diff --git a/lib/core/audio/cache/audio_cache_manager.dart b/lib/core/audio/cache/audio_cache_manager.dart index 6467f18..79617e7 100644 --- a/lib/core/audio/cache/audio_cache_manager.dart +++ b/lib/core/audio/cache/audio_cache_manager.dart @@ -94,6 +94,7 @@ class AudioCacheManager { /// 创建缓存音频源 static AudioSource _createCachingSource(String url, File cacheFile) { + // ignore: experimental_member_use return LockCachingAudioSource(Uri.parse(url), cacheFile: cacheFile); } diff --git a/lib/core/audio/controllers/playback_controller.dart b/lib/core/audio/controllers/playback_controller.dart index b9451a7..d75986e 100644 --- a/lib/core/audio/controllers/playback_controller.dart +++ b/lib/core/audio/controllers/playback_controller.dart @@ -10,15 +10,12 @@ import 'package:asmrapp/data/models/works/work.dart'; class PlaybackController { final AudioPlayer _player; final PlaybackStateManager _stateManager; - final ConcatenatingAudioSource _playlist; PlaybackController({ required AudioPlayer player, required PlaybackStateManager stateManager, - required ConcatenatingAudioSource playlist, }) : _player = player, - _stateManager = stateManager, - _playlist = playlist; + _stateManager = stateManager; // 基础播放控制 Future play() => _player.play(); @@ -118,7 +115,6 @@ class PlaybackController { try { await PlaylistBuilder.setPlaylistSource( player: _player, - playlist: _playlist, files: context.playlist, initialIndex: context.currentIndex, initialPosition: initialPosition ?? Duration.zero, diff --git a/lib/core/audio/models/playback_context.dart b/lib/core/audio/models/playback_context.dart index 9c7f1ec..0e643c8 100644 --- a/lib/core/audio/models/playback_context.dart +++ b/lib/core/audio/models/playback_context.dart @@ -176,10 +176,4 @@ class PlaybackContext { file.mediaDownloadUrl != null && file.type?.toLowerCase() != 'vtt') .toList(); } - - // 工具方法:获取文件名(不含扩展名) - String? _getBaseName(String? filename) { - if (filename == null) return null; - return filename.replaceAll(RegExp(r'\.[^.]+$'), ''); - } } diff --git a/lib/core/audio/state/playback_state_manager.dart b/lib/core/audio/state/playback_state_manager.dart index cf542ef..6765815 100644 --- a/lib/core/audio/state/playback_state_manager.dart +++ b/lib/core/audio/state/playback_state_manager.dart @@ -20,6 +20,7 @@ class PlaybackStateManager { AudioTrackInfo? _currentTrack; PlaybackContext? _currentContext; + bool _listenersInitialized = false; final List _subscriptions = []; @@ -33,31 +34,45 @@ class PlaybackStateManager { // 初始化状态监听 void initStateListeners() { + if (_listenersInitialized) { + return; + } + _listenersInitialized = true; + // 监听播放器索引变化 - _player.currentIndexStream.listen((index) { - if (index != null && _currentContext != null) { - final newFile = _currentContext!.playlist[index]; - updateTrackAndContext(newFile, _currentContext!.work); - } - }); + _subscriptions.add( + _player.currentIndexStream.listen((index) { + if (index != null && _currentContext != null) { + final newFile = _currentContext!.playlist[index]; + updateTrackAndContext(newFile, _currentContext!.work); + } + }), + ); // 直接监听 AudioPlayer 的原始流 - _player.playerStateStream.listen((state) async { - final position = _player.position; - final duration = _player.duration; - - // 转换并发送到 EventHub - _eventHub.emit(PlaybackStateEvent(state, position, duration)); - - if (state.processingState == ProcessingState.completed) { - _onPlaybackCompleted(); - } - saveState(); - }); - - _player.positionStream.listen((position) { - _eventHub.emit(PlaybackProgressEvent(position, _player.bufferedPosition)); - }); + _subscriptions.add( + _player.playerStateStream.listen((state) async { + final position = _player.position; + final duration = _player.duration; + + // 转换并发送到 EventHub + _eventHub.emit(PlaybackStateEvent(state, position, duration)); + + if (state.processingState == ProcessingState.completed) { + _onPlaybackCompleted(); + } + saveState(); + }), + ); + + _subscriptions.add( + _player.positionStream.listen((position) { + _eventHub + .emit(PlaybackProgressEvent(position, _player.bufferedPosition)); + }), + ); + + _setupEventListeners(); } // 状态更新方法 @@ -154,5 +169,6 @@ class PlaybackStateManager { subscription.cancel(); } _subscriptions.clear(); + _listenersInitialized = false; } } diff --git a/lib/core/audio/utils/playlist_builder.dart b/lib/core/audio/utils/playlist_builder.dart index 4b9c6cd..36890f8 100644 --- a/lib/core/audio/utils/playlist_builder.dart +++ b/lib/core/audio/utils/playlist_builder.dart @@ -9,26 +9,15 @@ class PlaylistBuilder { })); } - static Future updatePlaylist( - ConcatenatingAudioSource playlist, - List sources, - ) async { - await playlist.clear(); - await playlist.addAll(sources); - } - static Future setPlaylistSource({ required AudioPlayer player, - required ConcatenatingAudioSource playlist, required List files, required int initialIndex, required Duration initialPosition, }) async { final sources = await buildAudioSources(files); - await updatePlaylist(playlist, sources); - - await player.setAudioSource( - playlist, + await player.setAudioSources( + sources, initialIndex: initialIndex, initialPosition: initialPosition, ); diff --git a/lib/widgets/detail/mark_selection_dialog.dart b/lib/widgets/detail/mark_selection_dialog.dart index d03246a..efae424 100644 --- a/lib/widgets/detail/mark_selection_dialog.dart +++ b/lib/widgets/detail/mark_selection_dialog.dart @@ -27,54 +27,56 @@ class MarkSelectionDialog extends StatelessWidget { color: isDark ? Colors.white70 : Colors.black87, ), ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: MarkStatus.values.map((status) { - final isSelected = status == currentStatus; - return ListTile( - enabled: !loading, - leading: Radio( - value: status, - groupValue: currentStatus, - onChanged: loading + content: RadioGroup( + groupValue: currentStatus, + onChanged: (MarkStatus? value) { + if (loading || value == null) { + return; + } + onMarkSelected(value); + Navigator.of(context).pop(); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: MarkStatus.values.map((status) { + final isSelected = status == currentStatus; + return ListTile( + enabled: !loading, + leading: Radio( + value: status, + enabled: !loading, + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.disabled)) { + return isDark ? Colors.white24 : Colors.black26; + } + if (states.contains(WidgetState.selected)) { + return isDark ? Colors.white70 : Colors.black87; + } + return isDark ? Colors.white38 : Colors.black45; + }), + ), + title: Text( + status.localizedLabel(context.l10n), + style: TextStyle( + color: loading + ? (isDark ? Colors.white38 : Colors.black38) + : (isSelected + ? (isDark ? Colors.white : Colors.black87) + : (isDark ? Colors.white70 : Colors.black54)), + ), + ), + onTap: loading ? null - : (MarkStatus? value) { - if (value != null) { - onMarkSelected(value); - Navigator.of(context).pop(); - } + : () { + onMarkSelected(status); + Navigator.of(context).pop(); }, - fillColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.disabled)) { - return isDark ? Colors.white24 : Colors.black26; - } - if (states.contains(WidgetState.selected)) { - return isDark ? Colors.white70 : Colors.black87; - } - return isDark ? Colors.white38 : Colors.black45; - }), - ), - title: Text( - status.localizedLabel(context.l10n), - style: TextStyle( - color: loading - ? (isDark ? Colors.white38 : Colors.black38) - : (isSelected - ? (isDark ? Colors.white : Colors.black87) - : (isDark ? Colors.white70 : Colors.black54)), - ), - ), - onTap: loading - ? null - : () { - onMarkSelected(status); - Navigator.of(context).pop(); - }, - hoverColor: isDark - ? Colors.white.withValues(alpha: 0.05) - : Colors.black.withValues(alpha: 0.05), - ); - }).toList(), + hoverColor: isDark + ? Colors.white.withValues(alpha: 0.05) + : Colors.black.withValues(alpha: 0.05), + ); + }).toList(), + ), ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), From 2226c93bf5c66b0e15a1212af6201a859faa8dc3 Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Thu, 19 Feb 2026 00:51:42 +0900 Subject: [PATCH 16/30] feat: update Flutter version to 3.41.1 and improve formatting in build workflow --- .github/workflows/build.yml | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 22f14eb..a0b93b8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,11 +6,11 @@ permissions: on: push: tags: - - 'v*' # 当推送 v 开头的tag时触发,如 v1.0.0 + - "v*" # 当推送 v 开头的tag时触发,如 v1.0.0 workflow_dispatch: env: - FLUTTER_VERSION: '3.27.0' + FLUTTER_VERSION: "3.41.1" jobs: build-android: @@ -19,19 +19,19 @@ jobs: steps: - uses: actions/checkout@v3 with: - fetch-depth: 0 # 获取完整的 git 历史 + fetch-depth: 0 # 获取完整的 git 历史 - name: Setup Java uses: actions/setup-java@v3 with: - distribution: 'zulu' - java-version: '17' + distribution: "zulu" + java-version: "17" - name: Setup Flutter uses: subosito/flutter-action@v2 with: flutter-version: ${{ env.FLUTTER_VERSION }} - channel: 'stable' + channel: "stable" - name: Get dependencies run: flutter pub get @@ -66,13 +66,13 @@ jobs: steps: - uses: actions/checkout@v3 with: - fetch-depth: 0 # 获取完整的 git 历史 + fetch-depth: 0 # 获取完整的 git 历史 - name: Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.27.0' - channel: 'stable' + flutter-version: ${{ env.FLUTTER_VERSION }} + channel: "stable" - name: Get dependencies run: flutter pub get @@ -95,12 +95,12 @@ jobs: upload: runs-on: ubuntu-latest - needs: [ build-android, build-ios ] + needs: [build-android, build-ios] steps: - uses: actions/checkout@v3 with: - fetch-depth: 0 # 获取完整的 git 历史 + fetch-depth: 0 # 获取完整的 git 历史 - name: Download artifacts uses: actions/download-artifact@v4 @@ -141,30 +141,30 @@ jobs: draft: false body: | ## 🚧 Pre-release Version - + ### 📋 Release Information **Version:** ${{ github.ref_name }} **Previous Version:** ${{ steps.previoustag.outputs.tag }} **Build Environment:** Flutter ${{ env.FLUTTER_VERSION }} - + ### 📝 Changelog ${{ steps.commits.outputs.commits }} - + ### 📦 Distribution | File | Description | Purpose | |------|-------------|----------| | `.apk` | Android Package | Direct installation for testing | | `.aab` | Android App Bundle | Google Play Store deployment | - + ### 🔍 Additional Notes - This is a pre-release build intended for testing purposes - Features and functionality may not be fully stable - Not recommended for production use - + ### 📱 Compatibility - Minimum Android SDK: 21 (Android 5.0) - Target Android SDK: 33 (Android 13) - + > **Note:** Please report any issues or bugs through the GitHub issue tracker. files: | dist/flutter-apk/app-release.apk From 4163cf4d0f38e1ef843ee8066a00fc1a6ec8626c Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Thu, 19 Feb 2026 01:02:31 +0900 Subject: [PATCH 17/30] feat: implement work rating feature; add updateWorkRating method and integrate with DetailViewModel and UI --- lib/data/services/api_service.dart | 20 +++++ .../viewmodels/detail_viewmodel.dart | 89 +++++++++++++++++++ lib/screens/detail_screen.dart | 2 + lib/widgets/detail/work_action_buttons.dart | 9 +- pubspec.lock | 8 +- 5 files changed, 121 insertions(+), 7 deletions(-) diff --git a/lib/data/services/api_service.dart b/lib/data/services/api_service.dart index 28f7636..0b7d590 100644 --- a/lib/data/services/api_service.dart +++ b/lib/data/services/api_service.dart @@ -432,6 +432,26 @@ class ApiService { } } + /// 更新作品评分 + Future updateWorkRating(String workId, int rating) async { + try { + final response = await _dio.put( + '/review', + data: { + 'work_id': int.parse(workId), + 'rating': rating, + }, + ); + + if (response.statusCode != 200) { + throw Exception('评分失败: ${response.statusCode}'); + } + } catch (e) { + AppLogger.error('更新评分失败', e); + rethrow; + } + } + /// 将 MarkStatus 枚举转换为 API 参数 String convertMarkStatusToApi(MarkStatus status) { switch (status) { diff --git a/lib/presentation/viewmodels/detail_viewmodel.dart b/lib/presentation/viewmodels/detail_viewmodel.dart index 76323ed..02fd86a 100644 --- a/lib/presentation/viewmodels/detail_viewmodel.dart +++ b/lib/presentation/viewmodels/detail_viewmodel.dart @@ -106,6 +106,10 @@ class DetailViewModel extends ChangeNotifier { bool _loadingMark = false; bool get loadingMark => _loadingMark; + int? _currentRating; + int? get currentRating => _currentRating; + bool _loadingRating = false; + bool get loadingRating => _loadingRating; bool _downloadingFiles = false; bool get downloadingFiles => _downloadingFiles; @@ -121,6 +125,7 @@ class DetailViewModel extends ChangeNotifier { _downloadProgressManager = GetIt.I(); _authRepository = GetIt.I(); _fileDownloader = FileDownloader(); + _currentRating = _normalizeRating(work.userRating); loadRecommendationsPreview(); } @@ -384,6 +389,23 @@ class DetailViewModel extends ChangeNotifier { } } + Future updateRating(int rating) async { + _loadingRating = true; + notifyListeners(); + + try { + await _apiService.updateWorkRating(work.id.toString(), rating); + _currentRating = rating; + AppLogger.info('更新评分成功: $rating'); + } catch (e) { + AppLogger.error('更新评分失败', e); + rethrow; + } finally { + _loadingRating = false; + notifyListeners(); + } + } + Future downloadFiles( List files, ) async { @@ -629,6 +651,73 @@ class DetailViewModel extends ChangeNotifier { ); } + Future showRatingDialog(BuildContext context) async { + var selectedRating = _currentRating ?? 0; + final rating = await showDialog( + context: context, + builder: (dialogContext) => StatefulBuilder( + builder: (builderContext, setState) => AlertDialog( + title: Text(builderContext.l10n.workActionRate), + content: Row( + mainAxisSize: MainAxisSize.min, + children: List.generate(5, (index) { + final value = index + 1; + return IconButton( + onPressed: _loadingRating + ? null + : () => setState(() { + selectedRating = value; + }), + icon: Icon( + value <= selectedRating ? Icons.star : Icons.star_border, + color: Colors.amber, + ), + ); + }), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: Text(builderContext.l10n.cancel), + ), + TextButton( + onPressed: selectedRating == 0 || _loadingRating + ? null + : () => Navigator.of(dialogContext).pop(selectedRating), + child: Text(builderContext.l10n.confirm), + ), + ], + ), + ), + ); + + if (rating == null) return; + + try { + await updateRating(rating); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('${context.l10n.workActionRate}: $rating/5')), + ); + } catch (e) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.operationFailed(e.toString()))), + ); + } + } + + int? _normalizeRating(dynamic rating) { + final parsed = switch (rating) { + int value => value, + double value => value.round(), + String value => int.tryParse(value) ?? double.tryParse(value)?.round(), + _ => null, + }; + if (parsed == null) return null; + return parsed.clamp(1, 5); + } + @override void dispose() { // 取消所有正在进行的请求 diff --git a/lib/screens/detail_screen.dart b/lib/screens/detail_screen.dart index 54f8747..76cbfc1 100644 --- a/lib/screens/detail_screen.dart +++ b/lib/screens/detail_screen.dart @@ -68,6 +68,8 @@ class DetailScreen extends StatelessWidget { onMarkTap: () => viewModel.showMarkDialog(context), currentMarkStatus: viewModel.currentMarkStatus, loadingMark: viewModel.loadingMark, + onRateTap: () => viewModel.showRatingDialog(context), + loadingRate: viewModel.loadingRating, onDownloadTap: viewModel.files == null ? null : () => _showDownloadDialog(context, viewModel), diff --git a/lib/widgets/detail/work_action_buttons.dart b/lib/widgets/detail/work_action_buttons.dart index c300fc5..015cc8f 100644 --- a/lib/widgets/detail/work_action_buttons.dart +++ b/lib/widgets/detail/work_action_buttons.dart @@ -9,6 +9,8 @@ class WorkActionButtons extends StatelessWidget { final VoidCallback onMarkTap; final MarkStatus? currentMarkStatus; final bool loadingMark; + final VoidCallback onRateTap; + final bool loadingRate; final VoidCallback? onDownloadTap; final bool loadingDownload; @@ -19,6 +21,8 @@ class WorkActionButtons extends StatelessWidget { required this.onMarkTap, this.currentMarkStatus, this.loadingMark = false, + required this.onRateTap, + this.loadingRate = false, this.onDownloadTap, this.loadingDownload = false, }); @@ -48,9 +52,8 @@ class WorkActionButtons extends StatelessWidget { _ActionButton( icon: Icons.star_border, label: context.l10n.workActionRate, - onTap: () { - // TODO: 实现评分功能 - }, + onTap: onRateTap, + loading: loadingRate, ), _ActionButton( icon: Icons.download_outlined, diff --git a/pubspec.lock b/pubspec.lock index 5c1ac0e..20bf583 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -165,10 +165,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -1166,10 +1166,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.8" typed_data: dependency: transitive description: From 17de61294cb9e6fe78e8e633eaf17a40e2761031 Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Thu, 19 Feb 2026 01:10:55 +0900 Subject: [PATCH 18/30] feat: add English localization support; create app_localizations_en.dart and update localization delegates --- lib/l10n/app_en.arb | 261 +++++++++++++++++ lib/l10n/app_localizations.dart | 6 +- lib/l10n/app_localizations_en.dart | 456 +++++++++++++++++++++++++++++ 3 files changed, 722 insertions(+), 1 deletion(-) create mode 100644 lib/l10n/app_en.arb create mode 100644 lib/l10n/app_localizations_en.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..eadd40f --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,261 @@ +{ + "@@locale": "en", + "appName": "asmr.one", + "retry": "Retry", + "cancel": "Cancel", + "confirm": "Confirm", + "logoutAction": "Log out", + "logoutConfirmTitle": "Log out", + "logoutConfirmMessage": "Are you sure you want to log out?", + "login": "Log in", + "favorites": "Favorites", + "settings": "Settings", + "cacheManager": "Cache Manager", + "screenAlwaysOn": "Keep Screen On", + "themeSystem": "Follow System", + "themeLight": "Light Mode", + "themeDark": "Dark Mode", + "navigationFavorites": "Favorites", + "navigationHome": "Home", + "navigationDownloadProgress": "Downloads", + "navigationForYou": "For You", + "navigationPopularWorks": "Popular Works", + "navigationRecommend": "Recommendations", + "homeTabWorks": "Works", + "homeTabDownloads": "Progress", + "titleWithCount": "{title} ({count})", + "@titleWithCount": { + "placeholders": { + "title": { + "type": "String" + }, + "count": { + "type": "int" + } + } + }, + "search": "Search", + "searchHint": "Search...", + "searchPromptInitial": "Enter keywords to search", + "searchNoResults": "No results found", + "subtitle": "Subtitles", + "subtitleAvailable": "Subtitles available", + "orderFieldCollectionTime": "Collection Time", + "orderFieldReleaseDate": "Release Date", + "orderFieldSales": "Sales", + "orderFieldPrice": "Price", + "orderFieldRating": "Rating", + "orderFieldReviewCount": "Review Count", + "orderFieldId": "RJ Number", + "orderFieldMyRating": "My Rating", + "orderFieldAllAges": "All Ages", + "orderFieldRandom": "Random", + "orderLabel": "Sort By", + "orderDirectionDesc": "Descending", + "orderDirectionAsc": "Ascending", + "searchOrderNewest": "Newest Collected", + "searchOrderOldest": "Oldest Collected", + "searchOrderReleaseDesc": "Release Date (Newest)", + "searchOrderReleaseAsc": "Release Date (Oldest)", + "searchOrderSalesDesc": "Sales (High to Low)", + "searchOrderSalesAsc": "Sales (Low to High)", + "searchOrderPriceDesc": "Price (High to Low)", + "searchOrderPriceAsc": "Price (Low to High)", + "searchOrderRatingDesc": "Rating (High to Low)", + "searchOrderReviewCountDesc": "Review Count (High to Low)", + "searchOrderIdDesc": "RJ Number (High to Low)", + "searchOrderIdAsc": "RJ Number (Low to High)", + "searchOrderRandom": "Random Order", + "favoritesTitle": "Favorites", + "pleaseLogin": "Please log in first", + "emptyContent": "No content", + "emptyWorks": "No works", + "similarWorksTitle": "Similar Works", + "similarWorksSeeAll": "See All", + "playlistAddToFavorites": "Add to Favorites", + "playlistEmpty": "No playlists", + "playlistAddSuccess": "Added: {name}", + "@playlistAddSuccess": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "playlistRemoveSuccess": "Removed: {name}", + "@playlistRemoveSuccess": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "playlistSystemMarked": "My Marks", + "playlistSystemLiked": "Liked", + "playlistWorksCount": "{count} works", + "@playlistWorksCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "workActionFavorite": "Favorite", + "workActionMark": "Mark", + "workActionRate": "Rate", + "workActionDownload": "Download", + "workActionChecking": "Checking", + "workActionRecommend": "Recommendations", + "workActionNoRecommendation": "No recommendations", + "downloadDialogTitle": "Select files to download", + "downloadDialogNoFiles": "No downloadable files", + "downloadSelectedCount": "Selected: {count}", + "@downloadSelectedCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "downloadSelectAll": "Select All", + "downloadClearSelection": "Clear Selection", + "downloadNoFilesSelected": "Please select files to download", + "downloadSuccess": "Downloaded {count} files: {path}", + "@downloadSuccess": { + "placeholders": { + "count": { + "type": "int" + }, + "path": { + "type": "String" + } + } + }, + "downloadPartial": "Downloaded {successCount} / Failed {failedCount}", + "@downloadPartial": { + "placeholders": { + "successCount": { + "type": "int" + }, + "failedCount": { + "type": "int" + } + } + }, + "downloadAllFailed": "Download failed ({failedCount})", + "@downloadAllFailed": { + "placeholders": { + "failedCount": { + "type": "int" + } + } + }, + "downloadDirectoryTitle": "Download Folder", + "downloadDirectoryDescription": "You can choose where downloads are saved. If not set, the default folder is used.", + "downloadDirectoryDefaultValue": "Not set (use default location)", + "downloadDirectoryPermissionHint": "Storage permission will be requested if needed.", + "downloadDirectoryPick": "Choose Folder", + "downloadDirectoryReset": "Reset to Default", + "downloadDirectoryUpdated": "Save location updated: {path}", + "@downloadDirectoryUpdated": { + "placeholders": { + "path": { + "type": "String" + } + } + }, + "downloadDirectoryResetSuccess": "Save location reset to default", + "downloadProgressEmpty": "No active downloads", + "downloadProgressClearFinished": "Clear Completed", + "downloadProgressActiveSection": "Active", + "downloadProgressHistorySection": "History", + "downloadStatusQueued": "Queued", + "downloadStatusRunning": "Downloading", + "downloadStatusCompleted": "Completed", + "downloadStatusFailed": "Failed", + "openDlsiteInBrowser": "Open DLsite in Browser", + "markStatusTitle": "Mark Status", + "markStatusWantToListen": "Want to Listen", + "markStatusListening": "Listening", + "markStatusListened": "Listened", + "markStatusRelistening": "Relistening", + "markStatusOnHold": "On Hold", + "markUpdated": "Status changed to {status}", + "@markUpdated": { + "placeholders": { + "status": { + "type": "String" + } + } + }, + "markFailed": "Failed to update mark: {error}", + "@markFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "workFilesTitle": "Files", + "playUnsupportedFileType": "Unsupported format: {type}", + "@playUnsupportedFileType": { + "placeholders": { + "type": { + "type": "String" + } + } + }, + "playUrlMissing": "Cannot play: URL is missing", + "playFilesNotLoaded": "File list not loaded", + "playFailed": "Playback failed: {error}", + "@playFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "operationFailed": "Operation failed: {error}", + "@operationFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "cacheManagerTitle": "Cache Manager", + "cacheAudio": "Audio Cache", + "cacheSubtitle": "Subtitle Cache", + "cacheTotal": "Total Cache Size", + "cacheClear": "Clear", + "cacheClearAll": "Clear All", + "cacheInfoTitle": "About Cache", + "cacheDescription": "Cache keeps recent audio and subtitles to speed up playback. Expired and oversized data is cleaned up automatically.", + "cacheLoadFailed": "Failed to load: {error}", + "@cacheLoadFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "cacheClearFailed": "Failed to clear: {error}", + "@cacheClearFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "subtitleTag": "Subtitles", + "noPlaying": "Nothing playing", + "screenOnDisable": "Disable Keep Screen On", + "screenOnEnable": "Enable Keep Screen On", + "unknownWorkTitle": "Unknown Work", + "unknownArtist": "Unknown Artist", + "lyricsEmpty": "No lyrics", + "loginTitle": "Log in", + "loginUsernameLabel": "Username", + "loginPasswordLabel": "Password", + "loginAction": "Log in" +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 7e8bbe6..c66e83a 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -5,6 +5,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:intl/intl.dart' as intl; +import 'app_localizations_en.dart'; import 'app_localizations_ja.dart'; import 'app_localizations_zh.dart'; @@ -94,6 +95,7 @@ abstract class AppLocalizations { /// A list of this localizations delegate's supported locales. static const List supportedLocales = [ + Locale('en'), Locale('ja'), Locale('zh') ]; @@ -932,7 +934,7 @@ class _AppLocalizationsDelegate @override bool isSupported(Locale locale) => - ['ja', 'zh'].contains(locale.languageCode); + ['en', 'ja', 'zh'].contains(locale.languageCode); @override bool shouldReload(_AppLocalizationsDelegate old) => false; @@ -941,6 +943,8 @@ class _AppLocalizationsDelegate AppLocalizations lookupAppLocalizations(Locale locale) { // Lookup logic when only language code is specified. switch (locale.languageCode) { + case 'en': + return AppLocalizationsEn(); case 'ja': return AppLocalizationsJa(); case 'zh': diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart new file mode 100644 index 0000000..b05556b --- /dev/null +++ b/lib/l10n/app_localizations_en.dart @@ -0,0 +1,456 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get appName => 'asmr.one'; + + @override + String get retry => 'Retry'; + + @override + String get cancel => 'Cancel'; + + @override + String get confirm => 'Confirm'; + + @override + String get logoutAction => 'Log out'; + + @override + String get logoutConfirmTitle => 'Log out'; + + @override + String get logoutConfirmMessage => 'Are you sure you want to log out?'; + + @override + String get login => 'Log in'; + + @override + String get favorites => 'Favorites'; + + @override + String get settings => 'Settings'; + + @override + String get cacheManager => 'Cache Manager'; + + @override + String get screenAlwaysOn => 'Keep Screen On'; + + @override + String get themeSystem => 'Follow System'; + + @override + String get themeLight => 'Light Mode'; + + @override + String get themeDark => 'Dark Mode'; + + @override + String get navigationFavorites => 'Favorites'; + + @override + String get navigationHome => 'Home'; + + @override + String get navigationDownloadProgress => 'Downloads'; + + @override + String get navigationForYou => 'For You'; + + @override + String get navigationPopularWorks => 'Popular Works'; + + @override + String get navigationRecommend => 'Recommendations'; + + @override + String get homeTabWorks => 'Works'; + + @override + String get homeTabDownloads => 'Progress'; + + @override + String titleWithCount(String title, int count) { + return '$title ($count)'; + } + + @override + String get search => 'Search'; + + @override + String get searchHint => 'Search...'; + + @override + String get searchPromptInitial => 'Enter keywords to search'; + + @override + String get searchNoResults => 'No results found'; + + @override + String get subtitle => 'Subtitles'; + + @override + String get subtitleAvailable => 'Subtitles available'; + + @override + String get orderFieldCollectionTime => 'Collection Time'; + + @override + String get orderFieldReleaseDate => 'Release Date'; + + @override + String get orderFieldSales => 'Sales'; + + @override + String get orderFieldPrice => 'Price'; + + @override + String get orderFieldRating => 'Rating'; + + @override + String get orderFieldReviewCount => 'Review Count'; + + @override + String get orderFieldId => 'RJ Number'; + + @override + String get orderFieldMyRating => 'My Rating'; + + @override + String get orderFieldAllAges => 'All Ages'; + + @override + String get orderFieldRandom => 'Random'; + + @override + String get orderLabel => 'Sort By'; + + @override + String get orderDirectionDesc => 'Descending'; + + @override + String get orderDirectionAsc => 'Ascending'; + + @override + String get searchOrderNewest => 'Newest Collected'; + + @override + String get searchOrderOldest => 'Oldest Collected'; + + @override + String get searchOrderReleaseDesc => 'Release Date (Newest)'; + + @override + String get searchOrderReleaseAsc => 'Release Date (Oldest)'; + + @override + String get searchOrderSalesDesc => 'Sales (High to Low)'; + + @override + String get searchOrderSalesAsc => 'Sales (Low to High)'; + + @override + String get searchOrderPriceDesc => 'Price (High to Low)'; + + @override + String get searchOrderPriceAsc => 'Price (Low to High)'; + + @override + String get searchOrderRatingDesc => 'Rating (High to Low)'; + + @override + String get searchOrderReviewCountDesc => 'Review Count (High to Low)'; + + @override + String get searchOrderIdDesc => 'RJ Number (High to Low)'; + + @override + String get searchOrderIdAsc => 'RJ Number (Low to High)'; + + @override + String get searchOrderRandom => 'Random Order'; + + @override + String get favoritesTitle => 'Favorites'; + + @override + String get pleaseLogin => 'Please log in first'; + + @override + String get emptyContent => 'No content'; + + @override + String get emptyWorks => 'No works'; + + @override + String get similarWorksTitle => 'Similar Works'; + + @override + String get similarWorksSeeAll => 'See All'; + + @override + String get playlistAddToFavorites => 'Add to Favorites'; + + @override + String get playlistEmpty => 'No playlists'; + + @override + String playlistAddSuccess(String name) { + return 'Added: $name'; + } + + @override + String playlistRemoveSuccess(String name) { + return 'Removed: $name'; + } + + @override + String get playlistSystemMarked => 'My Marks'; + + @override + String get playlistSystemLiked => 'Liked'; + + @override + String playlistWorksCount(int count) { + return '$count works'; + } + + @override + String get workActionFavorite => 'Favorite'; + + @override + String get workActionMark => 'Mark'; + + @override + String get workActionRate => 'Rate'; + + @override + String get workActionDownload => 'Download'; + + @override + String get workActionChecking => 'Checking'; + + @override + String get workActionRecommend => 'Recommendations'; + + @override + String get workActionNoRecommendation => 'No recommendations'; + + @override + String get downloadDialogTitle => 'Select files to download'; + + @override + String get downloadDialogNoFiles => 'No downloadable files'; + + @override + String downloadSelectedCount(int count) { + return 'Selected: $count'; + } + + @override + String get downloadSelectAll => 'Select All'; + + @override + String get downloadClearSelection => 'Clear Selection'; + + @override + String get downloadNoFilesSelected => 'Please select files to download'; + + @override + String downloadSuccess(int count, String path) { + return 'Downloaded $count files: $path'; + } + + @override + String downloadPartial(int successCount, int failedCount) { + return 'Downloaded $successCount / Failed $failedCount'; + } + + @override + String downloadAllFailed(int failedCount) { + return 'Download failed ($failedCount)'; + } + + @override + String get downloadDirectoryTitle => 'Download Folder'; + + @override + String get downloadDirectoryDescription => + 'You can choose where downloads are saved. If not set, the default folder is used.'; + + @override + String get downloadDirectoryDefaultValue => 'Not set (use default location)'; + + @override + String get downloadDirectoryPermissionHint => + 'Storage permission will be requested if needed.'; + + @override + String get downloadDirectoryPick => 'Choose Folder'; + + @override + String get downloadDirectoryReset => 'Reset to Default'; + + @override + String downloadDirectoryUpdated(String path) { + return 'Save location updated: $path'; + } + + @override + String get downloadDirectoryResetSuccess => 'Save location reset to default'; + + @override + String get downloadProgressEmpty => 'No active downloads'; + + @override + String get downloadProgressClearFinished => 'Clear Completed'; + + @override + String get downloadProgressActiveSection => 'Active'; + + @override + String get downloadProgressHistorySection => 'History'; + + @override + String get downloadStatusQueued => 'Queued'; + + @override + String get downloadStatusRunning => 'Downloading'; + + @override + String get downloadStatusCompleted => 'Completed'; + + @override + String get downloadStatusFailed => 'Failed'; + + @override + String get openDlsiteInBrowser => 'Open DLsite in Browser'; + + @override + String get markStatusTitle => 'Mark Status'; + + @override + String get markStatusWantToListen => 'Want to Listen'; + + @override + String get markStatusListening => 'Listening'; + + @override + String get markStatusListened => 'Listened'; + + @override + String get markStatusRelistening => 'Relistening'; + + @override + String get markStatusOnHold => 'On Hold'; + + @override + String markUpdated(String status) { + return 'Status changed to $status'; + } + + @override + String markFailed(String error) { + return 'Failed to update mark: $error'; + } + + @override + String get workFilesTitle => 'Files'; + + @override + String playUnsupportedFileType(String type) { + return 'Unsupported format: $type'; + } + + @override + String get playUrlMissing => 'Cannot play: URL is missing'; + + @override + String get playFilesNotLoaded => 'File list not loaded'; + + @override + String playFailed(String error) { + return 'Playback failed: $error'; + } + + @override + String operationFailed(String error) { + return 'Operation failed: $error'; + } + + @override + String get cacheManagerTitle => 'Cache Manager'; + + @override + String get cacheAudio => 'Audio Cache'; + + @override + String get cacheSubtitle => 'Subtitle Cache'; + + @override + String get cacheTotal => 'Total Cache Size'; + + @override + String get cacheClear => 'Clear'; + + @override + String get cacheClearAll => 'Clear All'; + + @override + String get cacheInfoTitle => 'About Cache'; + + @override + String get cacheDescription => + 'Cache keeps recent audio and subtitles to speed up playback. Expired and oversized data is cleaned up automatically.'; + + @override + String cacheLoadFailed(String error) { + return 'Failed to load: $error'; + } + + @override + String cacheClearFailed(String error) { + return 'Failed to clear: $error'; + } + + @override + String get subtitleTag => 'Subtitles'; + + @override + String get noPlaying => 'Nothing playing'; + + @override + String get screenOnDisable => 'Disable Keep Screen On'; + + @override + String get screenOnEnable => 'Enable Keep Screen On'; + + @override + String get unknownWorkTitle => 'Unknown Work'; + + @override + String get unknownArtist => 'Unknown Artist'; + + @override + String get lyricsEmpty => 'No lyrics'; + + @override + String get loginTitle => 'Log in'; + + @override + String get loginUsernameLabel => 'Username'; + + @override + String get loginPasswordLabel => 'Password'; + + @override + String get loginAction => 'Log in'; +} From 9873f537cf795cf21460be938d0dadc9bb7e6fdc Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Thu, 19 Feb 2026 01:18:50 +0900 Subject: [PATCH 19/30] feat: add language selection feature; implement LocaleController and update settings screen for language preferences --- lib/core/di/service_locator.dart | 4 ++ lib/core/locale/locale_controller.dart | 45 ++++++++++++++ lib/l10n/app_en.arb | 6 ++ lib/l10n/app_ja.arb | 6 ++ lib/l10n/app_localizations.dart | 36 ++++++++++++ lib/l10n/app_localizations_en.dart | 18 ++++++ lib/l10n/app_localizations_ja.dart | 18 ++++++ lib/l10n/app_localizations_zh.dart | 18 ++++++ lib/l10n/app_zh.arb | 6 ++ lib/main.dart | 9 ++- lib/screens/settings/settings_screen.dart | 71 ++++++++++++++++++++--- 11 files changed, 228 insertions(+), 9 deletions(-) create mode 100644 lib/core/locale/locale_controller.dart diff --git a/lib/core/di/service_locator.dart b/lib/core/di/service_locator.dart index 55dd73b..8942b8e 100644 --- a/lib/core/di/service_locator.dart +++ b/lib/core/di/service_locator.dart @@ -16,6 +16,7 @@ import '../../core/audio/storage/i_playback_state_repository.dart'; import '../../core/audio/storage/playback_state_repository.dart'; import '../audio/events/playback_event_hub.dart'; import '../../core/theme/theme_controller.dart'; +import '../locale/locale_controller.dart'; import '../../core/platform/i_lyric_overlay_controller.dart'; import '../../core/platform/lyric_overlay_controller.dart'; import '../../core/platform/lyric_overlay_manager.dart'; @@ -93,6 +94,9 @@ Future setupServiceLocator() async { getIt.registerLazySingleton( () => ThemeController(prefs), ); + getIt.registerLazySingleton( + () => LocaleController(prefs), + ); // 注册 WakeLockController getIt.registerLazySingleton(() => WakeLockController(prefs)); diff --git a/lib/core/locale/locale_controller.dart b/lib/core/locale/locale_controller.dart new file mode 100644 index 0000000..5a9ca4b --- /dev/null +++ b/lib/core/locale/locale_controller.dart @@ -0,0 +1,45 @@ +import 'package:asmrapp/l10n/app_localizations.dart'; +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class LocaleController extends ChangeNotifier { + static const String _localeKey = 'app_locale_code'; + + final SharedPreferences _prefs; + + LocaleController(this._prefs) { + final savedLanguageCode = _prefs.getString(_localeKey); + if (savedLanguageCode == null || savedLanguageCode.trim().isEmpty) { + return; + } + + final isSupported = AppLocalizations.supportedLocales.any( + (locale) => locale.languageCode == savedLanguageCode, + ); + if (isSupported) { + _locale = Locale(savedLanguageCode); + } + } + + Locale? _locale; + + Locale? get locale => _locale; + + Future setLocale(Locale? locale) async { + final currentLanguageCode = _locale?.languageCode; + final nextLanguageCode = locale?.languageCode; + if (currentLanguageCode == nextLanguageCode) { + return; + } + + _locale = locale; + notifyListeners(); + + if (locale == null) { + await _prefs.remove(_localeKey); + return; + } + + await _prefs.setString(_localeKey, locale.languageCode); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index eadd40f..10a5613 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -10,6 +10,12 @@ "login": "Log in", "favorites": "Favorites", "settings": "Settings", + "languageTitle": "Language", + "languageDescription": "Choose the app display language.", + "languageSystem": "Follow System", + "languageEnglish": "English", + "languageJapanese": "Japanese", + "languageChinese": "Chinese", "cacheManager": "Cache Manager", "screenAlwaysOn": "Keep Screen On", "themeSystem": "Follow System", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 34ea340..baef9ae 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -10,6 +10,12 @@ "login": "ログイン", "favorites": "お気に入り", "settings": "設定", + "languageTitle": "言語", + "languageDescription": "アプリの表示言語を選択できます。", + "languageSystem": "システムと同じ", + "languageEnglish": "English", + "languageJapanese": "日本語", + "languageChinese": "中文", "cacheManager": "キャッシュ管理", "screenAlwaysOn": "画面常時オン", "themeSystem": "システムと同じ", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index c66e83a..d874225 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -160,6 +160,42 @@ abstract class AppLocalizations { /// **'设置'** String get settings; + /// No description provided for @languageTitle. + /// + /// In zh, this message translates to: + /// **'语言'** + String get languageTitle; + + /// No description provided for @languageDescription. + /// + /// In zh, this message translates to: + /// **'可以选择应用显示语言。'** + String get languageDescription; + + /// No description provided for @languageSystem. + /// + /// In zh, this message translates to: + /// **'跟随系统'** + String get languageSystem; + + /// No description provided for @languageEnglish. + /// + /// In zh, this message translates to: + /// **'English'** + String get languageEnglish; + + /// No description provided for @languageJapanese. + /// + /// In zh, this message translates to: + /// **'日本語'** + String get languageJapanese; + + /// No description provided for @languageChinese. + /// + /// In zh, this message translates to: + /// **'中文'** + String get languageChinese; + /// No description provided for @cacheManager. /// /// In zh, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index b05556b..6f84546 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -38,6 +38,24 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settings => 'Settings'; + @override + String get languageTitle => 'Language'; + + @override + String get languageDescription => 'Choose the app display language.'; + + @override + String get languageSystem => 'Follow System'; + + @override + String get languageEnglish => 'English'; + + @override + String get languageJapanese => 'Japanese'; + + @override + String get languageChinese => 'Chinese'; + @override String get cacheManager => 'Cache Manager'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index a393709..4f39146 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -38,6 +38,24 @@ class AppLocalizationsJa extends AppLocalizations { @override String get settings => '設定'; + @override + String get languageTitle => '言語'; + + @override + String get languageDescription => 'アプリの表示言語を選択できます。'; + + @override + String get languageSystem => 'システムと同じ'; + + @override + String get languageEnglish => 'English'; + + @override + String get languageJapanese => '日本語'; + + @override + String get languageChinese => '中文'; + @override String get cacheManager => 'キャッシュ管理'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 35755db..feb522a 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -38,6 +38,24 @@ class AppLocalizationsZh extends AppLocalizations { @override String get settings => '设置'; + @override + String get languageTitle => '语言'; + + @override + String get languageDescription => '可以选择应用显示语言。'; + + @override + String get languageSystem => '跟随系统'; + + @override + String get languageEnglish => 'English'; + + @override + String get languageJapanese => '日本語'; + + @override + String get languageChinese => '中文'; + @override String get cacheManager => '缓存管理'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 3adc9a3..5399732 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -10,6 +10,12 @@ "login": "登录", "favorites": "我的收藏", "settings": "设置", + "languageTitle": "语言", + "languageDescription": "可以选择应用显示语言。", + "languageSystem": "跟随系统", + "languageEnglish": "English", + "languageJapanese": "日本語", + "languageChinese": "中文", "cacheManager": "缓存管理", "screenAlwaysOn": "屏幕常亮", "themeSystem": "跟随系统主题", diff --git a/lib/main.dart b/lib/main.dart index b856b0d..c4ec458 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:asmrapp/core/download/download_directory_controller.dart'; import 'package:asmrapp/core/download/download_progress_manager.dart'; +import 'package:asmrapp/core/locale/locale_controller.dart'; import 'package:asmrapp/core/theme/app_theme.dart'; import 'package:asmrapp/core/theme/theme_controller.dart'; import 'package:asmrapp/l10n/app_localizations.dart'; @@ -41,6 +42,9 @@ class MyApp extends StatelessWidget { ChangeNotifierProvider( create: (_) => getIt(), ), + ChangeNotifierProvider( + create: (_) => getIt(), + ), ChangeNotifierProvider( create: (_) => getIt(), ), @@ -48,8 +52,8 @@ class MyApp extends StatelessWidget { create: (_) => getIt(), ), ], - child: Consumer( - builder: (context, themeController, child) { + child: Consumer2( + builder: (context, themeController, localeController, child) { return MaterialApp( onGenerateTitle: (context) => context.l10n.appName, localizationsDelegates: const [ @@ -59,6 +63,7 @@ class MyApp extends StatelessWidget { GlobalCupertinoLocalizations.delegate, ], supportedLocales: AppLocalizations.supportedLocales, + locale: localeController.locale, theme: AppTheme.light, darkTheme: AppTheme.dark, themeMode: themeController.themeMode, diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index 43d86b9..f212ffb 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -1,4 +1,5 @@ import 'package:asmrapp/core/download/download_directory_controller.dart'; +import 'package:asmrapp/core/locale/locale_controller.dart'; import 'package:asmrapp/l10n/l10n.dart'; import 'package:asmrapp/screens/settings/cache_manager_screen.dart'; import 'package:file_selector/file_selector.dart'; @@ -24,8 +25,11 @@ class _SettingsScreenState extends State { title: Text(l10n.settings), ), body: Consumer( - builder: (context, controller, _) { - final path = controller.customDirectoryPath; + builder: (context, downloadController, _) { + final path = downloadController.customDirectoryPath; + final localeController = context.watch(); + final selectedLanguageCode = + localeController.locale?.languageCode ?? 'system'; return ListView( padding: const EdgeInsets.all(16), children: [ @@ -77,28 +81,28 @@ class _SettingsScreenState extends State { ? null : () => _pickDownloadDirectory( context, - controller, + downloadController, ), icon: const Icon(Icons.folder_open), label: Text(l10n.downloadDirectoryPick), ), OutlinedButton.icon( onPressed: _updatingDirectory || - !controller.hasCustomDirectory + !downloadController.hasCustomDirectory ? null : () => _resetDownloadDirectory( context, - controller, + downloadController, ), icon: const Icon(Icons.undo), label: Text(l10n.downloadDirectoryReset), ), ], ), - if (controller.lastError != null) ...[ + if (downloadController.lastError != null) ...[ const SizedBox(height: 12), Text( - controller.lastError!, + downloadController.lastError!, style: TextStyle( color: Theme.of(context).colorScheme.error, ), @@ -109,6 +113,59 @@ class _SettingsScreenState extends State { ), ), const SizedBox(height: 12), + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.languageTitle, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Text( + l10n.languageDescription, + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 12), + DropdownButtonFormField( + initialValue: selectedLanguageCode, + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + items: [ + DropdownMenuItem( + value: 'system', + child: Text(l10n.languageSystem), + ), + DropdownMenuItem( + value: 'en', + child: Text(l10n.languageEnglish), + ), + DropdownMenuItem( + value: 'ja', + child: Text(l10n.languageJapanese), + ), + DropdownMenuItem( + value: 'zh', + child: Text(l10n.languageChinese), + ), + ], + onChanged: (value) async { + if (value == null) { + return; + } + final nextLocale = + value == 'system' ? null : Locale(value); + await localeController.setLocale(nextLocale); + }, + ), + ], + ), + ), + ), + const SizedBox(height: 12), Card( child: ListTile( leading: const Icon(Icons.storage), From 3401693cece9d4d2dc430ccbbaa223b34054a60c Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Thu, 19 Feb 2026 01:42:31 +0900 Subject: [PATCH 20/30] feat: add Windows build support; update build workflow and add launcher icons --- .github/workflows/build.yml | 36 ++++- .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 8994 -> 9109 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 29763 -> 30429 bytes flutter_launcher_icons.yaml | 9 ++ .../AppIcon.appiconset/Contents.json | 123 +----------------- .../AppIcon.appiconset/Icon-App-72x72@1x.png | Bin 8994 -> 9109 bytes .../AppIcon.appiconset/Icon-App-72x72@2x.png | Bin 29763 -> 30429 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 9862 -> 9987 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 32751 -> 33659 bytes .../Icon-App-83.5x83.5@2x.png | Bin 38357 -> 38577 bytes pubspec.lock | 8 +- pubspec.yaml | 15 +-- windows/runner/resources/app_icon.ico | Bin 33772 -> 79152 bytes 13 files changed, 53 insertions(+), 138 deletions(-) create mode 100644 flutter_launcher_icons.yaml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a0b93b8..ef96a8e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -93,9 +93,40 @@ jobs: name: ios-build path: build/ios/iphoneos/app-release.ipa + build-windows: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # 获取完整的 git 历史 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + channel: "stable" + + - name: Get dependencies + run: flutter pub get + + - name: Build Windows + run: flutter build windows --release + + - name: Package Windows build + shell: pwsh + run: | + Compress-Archive -Path "build/windows/x64/runner/Release/*" -DestinationPath "build/windows/x64/runner/Release/windows-release.zip" -Force + + - name: Upload Windows artifacts + uses: actions/upload-artifact@v4 + with: + name: windows-build + path: build/windows/x64/runner/Release/windows-release.zip + upload: runs-on: ubuntu-latest - needs: [build-android, build-ios] + needs: [build-android, build-ios, build-windows] steps: - uses: actions/checkout@v3 @@ -155,6 +186,8 @@ jobs: |------|-------------|----------| | `.apk` | Android Package | Direct installation for testing | | `.aab` | Android App Bundle | Google Play Store deployment | + | `.ipa` | iOS Package | iOS sideload/testing distribution | + | `.zip` | Windows Package | Windows desktop distribution | ### 🔍 Additional Notes - This is a pre-release build intended for testing purposes @@ -170,6 +203,7 @@ jobs: dist/flutter-apk/app-release.apk dist/bundle/release/app-release.aab dist/app-release.ipa + dist/windows-release.zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index 318a3f4c2bb16b508f1b01642b50e8a2894059a9..558b4650a11d8524a9da3fed74de495c26e754ba 100644 GIT binary patch literal 9109 zcmV;GBWm17`-@9yfo`|bO@*@rX3AvxSdN;XMY79}TgVg-(27zwf%K@uczkUxR{1VLaR z3k<`50wjo$I6)NIresMLC5oGd!&%7T@V!~z-u;$-y?0kvSJk~IxV^o8k3-rHRD+qR z?pp3W`#Ik^&3w;8g99T3k2?RkTsm_6T-HLLb0#YwfN3MmD&6Sg=RL`vvRa z&wS$fI&uQ=1u`hn8I9;@!ID=c4G1v8fT84gcMDhu;Rje()P)ox_z3&qQM&f1bq~HB z{+{6@x8R{y44)ri7w&(8-pb-(88-t^37v|fF@^+y(?*T9*ej!UfVe=s^Bh3}W2{ic zRFGj{O9h16ALo!SP}0<9lReH3t@YU*K6*=sCB%-HCjN{q_So<_1`P=DyG2}e*|7vn zD0s7q@-<-+RCOdV&=phXq3+u{#g#;_gK~r&u38osgAlH zhz{c+$$X(-d9?EwmgyI$wOFk{$x%+F!8Js_j6W)3TM-i`YT-lX_DCy-KHmg~N)uHa zsoTHjD{L~j%#3PSK6ZV(90ssO)?`_ffI69FKTl-LQ@Nx}oO-x$@ zIS=-JXn)Y-x-_-UQqU6!fEVywNmrHeGc((tZmA(Xer#lTOMfle*wK6O=bR5K=S~yf zFX2lgqF9HGs{C@tMOgzf#^AO`Yi@_C{&`35p{u4n(&RsFpRW}%si=cd{=Iu6PcICN zZ1CUb+1T*Mqc--JMn=9~B{dhHNkg&O<2(X1AAv{e8$!J9qIN*zp~Hco`$9=33`E=G zi2MdBO@mSM)MWmhrO{_jAW8-Sp)cY8FB^Nr(f0y4uA|WqP>iA~+H0VOJM{JgEgYQC zu#Q8hy;&3jjLDwU8?d&os=WtsS3$ev`5ZR)$4*S|zP+4!dJ${vk&OGigZM-Q6YH zcpX*X+vUvh@#58c@ym;d(Z3#He<@YM^|2>Jt%YqJD-_yX+}X#uB22{w`@tCDvq`OB zYMXU&y@IlBbSjBdHXwkgEOLCTvAX9~6>~J!^&AK4gF9*o2Vtw5lVAkAP>auwmfl%P zeCfh2prYFTI|-7Yn1%Q^eRc$X+FkoT}CriH&<4UNqrMh^DT4_TsEkIjtK-o6ug z{y2QGdm9i|2vC$^br6`3Iu9Idml;t>)&zzS2UshLwLLl)2S#w;)L|%;%MX@aAkK#Dt+R_@uf`M=M-7mS1d%I45MWz}h~6`Z zL#_f2t*BFcK0`Mv?uQ%NrO6@IVWKIa*>v^Bs(pSMHGP_3Al*M;5lE%g7>*je(BS1d zuQl|^Y?n~N2;M5fViq5X-JKXBdYUaS2rpm}0=6nI?*`ZvNFQdE&vDg5=`b=G0uUt! z8m`=T+!`k4&`I~}j@HS)l+cBwsnXiJ53OhCaEGHZKXg25sG*5O?PvEwp*S?XE?1~6 zFOw$D2RA75?hQ!>$+l<|IG)g{t`D>rzJD@*#c1 z^8<*jGPX?;)d2(w1ngE1bQSu<4BX;pa&!3|7eEPF%!J##_Wr~0({pkUaRV_7HJ4?= z+E%T6H?L0UT~0F%p#b?iWo)*M*JcIh2n04+Z$q6hrl^Yha95klph2+i<}tYAM=?@T zHu~+C*dVJrNOn4h2n2VzHfwbaNEGTw9I->dqLV-LHE@`$p6wG1KIfGdd2HO;Z@IT~ z`sp#TRqZhuKtDg)S}9i^>_=e+38bFQxJx-as_OG2Ldbks_!!kNiXE7~G8(OY@Iar- z3>Z6bqr?07+yD)Jpv)x)_k!a|1p_QvZDML&zXDAcnd-n`CBu9ls|*oLq9!z42&BZY z&&7U&T^bMmY}rp&m2_lKTw;u)m#5Ly2c?Zdd?JgP(C4FYuOin45I(fOzoMTzcEmeb z*{FMaQ#(C@hCRYz4|zb?5zk2tb@(HRTO#K}xlU~BKx%{_*GGmTrPp!8uvtN~2`m68 z29L}A@SvA>wS)bPuD|&(_RVvI(q14&fJd#66@DAXU*??3QjMW$8zI5#ZuQ&u>=(`h z9QdP-T497S^&IHH5e5LJn7g2|ENVFAcy zkgJoEme_4zQ-O9M7~T4_tJ7b*ENTu7nbO9%@Je+`SYf9|Up(VeioUlZ9+Ub^Vj;0! zz*9-cnkWBs?MH@sZ_0e^l!_4PhC_4SZY|%Y=2H?l=*y+6#M(^4xxOw5`cw+85v*YJ~|0ib`6Ete!gax1EQPKoU zq4x{V|Kf!|S&odpwXvAuc5a_kJlG7GUssRvnqbu8O2TO0*wijgbhC~l*7Jy3z}stR z=@2S5!1X`~)bb&uBVXW(16wu3IcyYrc!r0N&P2J1M`F_HV+>JU4XCo12)3$zp+eh% zXt1*rVeixOTO-!~TJFgo+{06etZl+(19Jfh3x#!5c95;uy9n3osmXAC{Nz=HPygt> z#b}y*DbbI#LyCnF*rrl!R6o40p2`9x!V84!k%kMc0EIQ0iZD1_LiqWLmbT@lt4#=G zB*Bb|Y7>{5sM01bczYE_l|xxaL`TIsPKFR5YA6INL}SWy+}m>qM|^WnPiBG$IC0Bf z-aY=}ad6tA7C@nm4U9BKCZoZv91Dl6Xk_Q=L-U!(+S1jC+^Bin(vhw70!d#Mcu`aDqD=fAORQz;10fkV_*J{W(6Iv-+lf(tN1|eDpNu7B02~RKF*R~I z-rE(;#JAZU7TQ(jMeh%s`z_YRH$Dc$Jahz+Sv5-WaEfqn)T7Y!Yje0UY z#vg5xF`J8SCf(spAe9`NK7>Jwh1=Cuz_>~%t~4NR_c1|)DPfC~;Cm2oOo;@vNjIQ& zN>Kz@C3rfbkA?MOU6fnF`lfIZHZvIEYOdlwk+q*VNt(_=Zl6vY7$KsEYm8l*#X^zatkMnp`rpEVEdh~B2P}X zzrS8w-A$Z`kdTJBbbHJo1JBV`Viro6pfcR_29Q`MWC@wkstDKG;mebVb9yG>e_DoS z8?El3>1=-$kbY^`Bhz8nt^!8l(+cr|`?U4%e*gWII-^#E5f$CvinTue-4~}93=?@` zV{>QnAHUwI|HS&{-p4I%Ea0}zY@Kc_#3l+JDYj5tAE2sz9Xv>Ym9T-2C(N>_UthP* zWN9qY3D5(_4H#{g(PRqc_9Z~s3BmG$LQ&n7TI#fw6G=7}0T4GAZLEPHsr@YF9~`rbP$zw=T3cO&^TqZ85B z&oEs<09UR)eCxy7xv$Uarp6KS0cC&v*|+}lkG@iH(0r`l)BtR0lA#CAQF>51Z^N~6 zIGPUHu6MU;92;i?cZ2;vnuo?jml!YTiNRoKw~h>^q;))IBM@Cpw;rEy-+8FnDU{H} z-ZoefWOWpVA#~Elx60gY!>IoLP3Ku7Sc*@jFP=EJU|free6jp{zw?zJ{M$FeHBB^~ z=$B6+K%VD+=Z}`Y{A(vf#uk8+8c_&dYY6cxk8l3LPp8hM!DL;##}tEre$CJ?=2a>p z1eG~E9i`Erc599GJq+#@+JJ;gDWZ}qT}kQ;?YGcWhzN{B+Td*hf%fcz_+XuEyX1v2 zo-6wsTUaraOhV!^r)_1e-dd{v;_Gw&?G063Slp`N|9VqUQ@>tMU0$z$^_x$Uw^xOw zQeACUgM0T2$1YEVFU;=vILz<^SJ(`niP8_by_~Y9R;%b_ysPb^?Ew)o5AG?W=$YLp z_4m1gI{@bC@dG4Sx@JW|7=-K6h<%{V6pN-uWuykADuccU->M?Vqo=cCyDENspT0D& zPbPV}>8<3j50t4SLDXn$Vm?-QW7i5BwY8EORA*mV*ja1eTxmY`Vgm0qz%m7gX4PA| zRZTy;kZAP7*3Lq-Dy{$UpMFZ?;mIg{@%+fh$w@XF z02gxq#!w&O0l!mu5uU+bJ~v`R~2E4@}$I(lEU7qve0|Yo`}pxrBA? zAnSx)uG)pIPtL~IS~NDF1VB2*zNSpt2&zprX#`GN!6*_}bOmXK^m`oOUK1{d(aj2I z1O;5AF>phO-4>a&x{!@8IO%4vfYfng?M~51)s=Y2IyqMUFfZn3lp3%D1WO3ifhR~j z!tdrNqudKRkQT-2t3Mg7Z9VE-e}?)kY7r(A8fhPJ4Gd< z`#DGTIg%NT2oTYr7prO3H15^&oAs$!2veeKUR_f-Mr^3v1Sk-Fep4J~;?_DDPs(p> zZN%1pxKsI+o7?rZO1xEn;qh~S?u>u?y&~~37RhUOYmXm~Ab13{AepKwFZrdFE9>q= zros7XUelzFqM#MX9B#K}rouB3u~(?Dq@8P{Q(=*BlhF{pTL~<Mh_cld1iOt4hVg zQRxNRgjf*t@FbP06v~q`&6QPiCZkiYMrF!#5XXRE6o}rw0)Z_Z7acU-!_kuyNsFl; z+*nOO`(K2Rmar)*Clj=~5uCkbUdxNekK-X1e3(3e6qU_J0Xqd^>4-`n;N0B_Wz&^g z`TbmN?3<6Be|q%CfB1p(o6o5lyH)km8qGDK=>(_Wy{}}#KoAX6R0~jq;ckN-U{G+W zA%*B#*`+4JTH5CO`y`VYctuRowzsmXb#}rQGJLbhEYn*nu&IPrf`t1=e)6b-yB0L(fN!iz7xKaPB~<0KftHoY)2gAmm(xAe3+?K+!-3 z@k*nSD;p!xFa5)3-gs+eYjz}>GFok+ZWJ?bTsad-VJel1=vcHlmFcSlLb`o}t`W() zmk=iu6ON}OVuMn(=W53DMHdm#-oDlw1oIJ~vY6Cq;ikg}#s*j3H@^Jmk3C9zTb zN6*IJ6~t9oTw&ob0fAAmAzCe>PzliugW+6&|QanD=c<^at zIz_0&yP4>f7Lq_E9l;!+3*Ce?q$^s~uKZ{z{K{j9;_SJp*v&jjrLL}3CYsJ@ChG77%tWNm~kUVy#Wn%cb4> zyJ8}40N5O~a&7ndq|zK{%pOA&@Gw6_!APnropzfefe4XwG9vrQk}1;CC_$Ev z-2n7XVivVv5zxJTvDeH8ESLqnhw!hbLiTrzKSR5}yl>0aNN4 zG*R)ped_LF4J_TDvAbo@MzM!wqJR-=iC&@EWdNN`1#3k*osi)*VhP7Z7KsrOG~6x1 zG-p3Ko_bPoKgMGpTz&YZ*OD?N^?U&ViRze;?)1h1eA#_3!H5tnO@+#~5F?l$!5X>@6R=rn?^cyi2(6;W&***Y6^{0>bX0&0Elh8ooc0ohfkHc>n-I&EkO zsghxUCSQGO<^TTR)VH3L4^kraupgGmSi~BO9K25&*#KL|$m6hHMs2^bUyn~`fIGxy zUHZ35bq1jzrYPNXNl{2jLjZfZf*YMROio?uFciX=fgCsZ==$c_-+I9svjvEt(SnU8 zety170PDw^LER0)Zr(rOrK12t%x1Ss5?Q z(vPJ@u_2p4`GPo}(6PoT6_f}-6ZldbcTb{>7GIaeXOO+<<`5sJ-jvD({@_3Vkl73LLjI~@P8@QzEhQD#TKNotqqZw=##r0KvCI)p69G7rK zdHq@c{0-ubq7L6798lMRBv7JwA&_FVtc{ALeVLoBY-*Q(7z(CIlB6sZW-*T+86$v}e2rVC~)l0BKX=-hI>(5Z>I{Y1B%$HVca1u5Fk8{m~I+2=p{-bWuTm#T{=4{TTl5nRs~l!?*rJ`jKZY&d^qY5bW}vL36ywXEa|pfMm2 z{ySfRLsQ}a%{EII*tZE1qLZC5UjnVPANh86_x|t?{@1J`R_c{TtKM!o6C-1u*Yf-} z^1O@5`RAka`)}Qj?^niEhDLQvP~-Ya^3*(U`$Scw)ZJj|<4W#V&#+VkYq;Qew})oQT8pbOZNiy64l#Y%R(~t(&^Yp0+^+Zm!i``9fxX?GlNF-~d$}J}_1l&JZtDi9HuWEwB4~28_tP z3QHO;g)=8k{ri9R+eL%%u~WIG^flkV*UDSo(p+}q=KAb|Pk#NyGZ0cxgknQkd$@J~ z`LlR@Bxt%c8YZ;ke*}o02SJCM&4c;weoF^1@64GkH~3>=iF8h!o0&K_<5Zm;2Yvsi zoAY7XZU(w7Ggn4p*3kj4ev;KAZZXZTub{7Au)loT{qAyePWEYi8h9He1UR^#f9#pL z|FIu(t6UC+aa7Cg-#?amc=N$a`8(fu=`6B!uow_@^X}I6tIspj1O$o}Mg#dkugAza zczbRm?_f8eBJlixP*BpQk+zJud5{|cY}fcPjTrqQOT#v2Z1@Mix>(JXYx#XEF`^m@ z8y3@{bg}L&_k3AL2`nqfnP{Qh zezCOn`ZFg*RF`dH&%aY>Ij?_-h%N}y52D?6e$!Wrt&k5kBBrSri&3VsL9eHouZo86 z#z-DS&`Fd{ffl&fFZko53O?`$vRtyqqxN{TyL&dQ)PRR`^ggDqD#|~7#{a>pzx{x{ zHlKd|tp4H3J~52_a_U^BRO26S2DnquidtKVX_(;D`h(Xmj6=l0I`v!L^@8{8x1M>w z;?+3xN$L)np@fg3K2mg5;6RUCl#velBW3Bg{6?Y9RHiY;c4}i3uWFQpjg-U&QQ(Pc zO+;;S&~`aGBnF2hHl&4vQ;aN4`IY0nZz&01o>#YPctqQ8QqQ2Z7B@qf=`^3KC{s~r z*8fIU;8a9boNfQjmC94U@xr?mzu@(~L&uh^x;Kdx?D1f~L6er23U})()|m+|uNN_; z(TPO2YAvsB$7V-pp|s|H-S;!AMp) zXae^C&;9XF4^O{6qKiH3MtXv5e7F*RY7P*DqI$jT%h@*pJ|fK2akKesOg0QiI(qfW z-Tf~=t`j2q>eY4xD*6{+AofV@M&pc+l|hDZ!*OA=Yqt znTu{FU#$j0pc8lRgf3(df(JYG`^V<3gf05sqOjFUSAr*|!;!QWj?i{nzE@R;S4Pd6 z+i26-RFk|7-w6j-oi&Z0T%LKpZ8=9(Whx@o39WFFKqn16| z4v*$5w(MNCTH@xNeRots_JJ#^Ri0j1etq0R7*~p|_lagdb4>JV7t~rQo$<)*d^|C# zDUuk_v_rEf-6a)WIey0IG_!_KE(ECzYr9sZP>hbqUV@R1^=kylv)w&^Y)mEOV24ao zSii(PG?)IFxkeZsX~$IFR4I8|m6no?V1V4FmtDK}n^CUXA*bs7pB*pz&5J=tl=YH( z-q0hdxNfr4h(gE^F$WXlJrAa+6hCTxMZpo9?mcKANmy?XqR$cVeJJetne4%kT+r|i zexmW&Hy-&s;4RaYR?XX3+QTzZ0`~k<|<#G^6wl+pFr;UcK)kj101jd#P7p=5DOr+5iQ=Z=a29en?4Gsv>aDu=s#mYxa!+t;>+T`th=P^Z-PN_c z_wGIWIo~45;G^9Y?MaP;}l?BIWGcfv#aK1{%8KKa>`#|QTxEc(;0bwEIe zI3oXxqJyOtH53d4xE>&&%Biifw1wn=3;5SfBOg%mX)b?`mGIz-aH|eZfW82OQJu4x z4)d1$6gwvoAV{#}cy|g!2;m1rSJe3o(1%z1+@v18fd9CQf6|-wIUydDNi#r|vdK7% zauA448yan~SB7=~p@6V+TMB{@qAHKPNACK0j-xw%*v3Am zl;lHrr%0+UKbFFAg=|z|t0qheRYwxjl~U(S(mlUJ9tI)09#onDgixxcOp>ttdaL}- zT|Q(-=W>S^`RP>jr*$5kU5b?omK^0&7F|PZmC1)iVk=_YgcdpA`i~Pq|6>#(R+><8 zn936;#~jDazjr%#ah7SyPr0|w@lz3ohylX8n`|x#CsJZ_A4?8ds-dlkDQh6fj$J`l zRu7vl_9X}evFAR}3&MO!SCz3d)7zhHsu4YTZ212dVcq@iG|9aZxil<_bzHB?H+NhZ zGB9UA-@}s6v=anE2BIPzio2xuNRtn3KU^*3GI58%*1LCypPKJ|`m-erAY7}`noG`P zvDoZ$9$_wqeX<|&u3_DUcEFO61I;9ZlL+Zi18@*?ybRXMlIjiS#)ppNV2UF+E&+>vrt9<3Ay7ybwT~6P0|{`JHux?J}n-9Ws#vgovv} zvAV}*lgMc=9qdvy$m0>gQpl+k%-UpeKd7|W@yvmbK+qBvH`VImdhFylK@iI|&#Bv| zC*X9d`mIfCZsJfdfF9ofpw==Xdh_Ond3OA>w@Qw@6 zZlGi&ihLP^kk~rL^qxT+aTR#Pz3Ggc&#{e)`@y<)X=1Qd1VS|>GLx-dU$M_mLDOd` zj?(=j5di{Fjgz<$78+r>9@ZNAL@%0APRV8w7lz2u*o|G4?YUdICE*1;M$uN~$GZV> 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 IM6N<$g35hz+yDRo diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 0c4bddc7dc211e472dc1deae0a55eaabf25480d8..e5ee72b684a9f5b23f05b4730a97b1db30867234 100644 GIT binary patch literal 30429 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 Yf4EA=M8_-DyZ`_I07*qoM6N<$g6FCUPXGV_ literal 29763 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

^a9 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%0APRV8w7lz2u*o|G4?YUdICE*1;M$uN~$GZV> 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 IM6N<$g35hz+yDRo diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png index 0c4bddc7dc211e472dc1deae0a55eaabf25480d8..e5ee72b684a9f5b23f05b4730a97b1db30867234 100644 GIT binary patch literal 30429 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 Yf4EA=M8_-DyZ`_I07*qoM6N<$g6FCUPXGV_ literal 29763 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

^a9 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 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 525b4f1a712f9526ef21b0b0983eb8f55eab86b7..34150674d946ea9073c9477a0dad6aec41882f92 100644 GIT binary patch literal 33659 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

(tjL{ChX4M=r^G2ivHK0Gl%zLU28C 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%0D_vzxKTFt9xKL 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 N002ovPDHLkV1kiNl=c7s literal 38357 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.2.3 <4.0.0' + sdk: ">=3.2.3 <4.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -84,7 +84,6 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: - # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. @@ -122,11 +121,5 @@ flutter: # For details regarding fonts from package dependencies, # see https://flutter.dev/to/font-from-package -flutter_launcher_icons: - android: "ic_launcher" - ios: true - image_path: "assets/icon/icon.png" - min_sdk_android: 21 - dependency_overrides: - path: ^1.9.0 # 强制使用更高版本的 path 包 + path: ^1.9.0 # 强制使用更高版本的 path 包 diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico index c04e20caf6370ebb9253ad831cc31de4a9c965f6..04173b09e292cb9fce63087955c29f9373d7a4f9 100644 GIT binary patch literal 79152 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 mwz%lr+P3eFowsiI{{IIv^s1(?()~~X00008A literal 33772 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 From 3d0d45c849a10a674fcced51cb439dcbefa34a54 Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Thu, 19 Feb 2026 01:44:24 +0900 Subject: [PATCH 21/30] fix policy --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 6dd3525..f3d7cad 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.2.0 +version: 1.2.0+1 environment: sdk: ">=3.2.3 <4.0.0" From 8e8bf48b0206c6ca53e75fde443bcb6fe78aa236 Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Thu, 19 Feb 2026 01:56:40 +0900 Subject: [PATCH 22/30] feat: update Java version to 21 in build workflow; enhance iOS deployment target to 14.0; add Japanese localization support in README files --- .github/workflows/build.yml | 2 +- README.md | 2 +- README_en.md | 4 +- README_ja.md | 74 ++++++++++++++++++++++++++++ ios/Flutter/AppFrameworkInfo.plist | 2 +- ios/Podfile | 4 +- ios/Runner.xcodeproj/project.pbxproj | 6 +-- pubspec.yaml | 2 +- 8 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 README_ja.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ef96a8e..ede5f88 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: "zulu" - java-version: "17" + java-version: "21" - name: Setup Flutter uses: subosito/flutter-action@v2 diff --git a/README.md b/README.md index 27a899e..34511b9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Yuro -[English](README_en.md) +[English](README_en.md) | [日本語](README_ja.md) 一个使用 Flutter 构建的 ASMR.ONE 客户端。 diff --git a/README_en.md b/README_en.md index bb1a452..eaba786 100644 --- a/README_en.md +++ b/README_en.md @@ -1,6 +1,6 @@ # ASMR One App -[中文说明](README.md) +[中文说明](README.md) | [日本語](README_ja.md) A beautiful and modern ASMR player application built with Flutter. @@ -56,4 +56,4 @@ Please read our [Development Guidelines](docs/guidelines_en.md) before making a ## License -This project is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike License (CC BY-NC-SA) - see the [LICENSE](LICENSE) file for details. This license allows others to remix, tweak, and build upon your work non-commercially, as long as they credit you and license their new creations under the identical terms. \ No newline at end of file +This project is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike License (CC BY-NC-SA) - see the [LICENSE](LICENSE) file for details. This license allows others to remix, tweak, and build upon your work non-commercially, as long as they credit you and license their new creations under the identical terms. diff --git a/README_ja.md b/README_ja.md new file mode 100644 index 0000000..3606327 --- /dev/null +++ b/README_ja.md @@ -0,0 +1,74 @@ +# Yuro + +[中文说明](README.md) | [English](README_en.md) + +Flutter で構築された ASMR.ONE クライアントです。 + +## プロジェクト概要 + +Yuro は、美しいアニメーションとモダンなユーザーインターフェースを通じて、快適でスムーズな ASMR リスニング体験を提供することを目指しています。 + +## 特徴 + +- 安定したバックグラウンド再生 +- 美しいアニメーション表現 +- スムーズな再生体験 +- シンプルな UI デザイン +- 包括的なスマートキャッシュ機構 + - 画像キャッシュ最適化: カバー画像の読み込み速度を向上し、重複読み込みを削減 + - 字幕ローカルキャッシュ: 字幕のマッチングと読み込みを高速化 + - 音声ファイルキャッシュ: 再ダウンロードを減らし、通信量を節約 +- サーバー負荷の軽減 + - スマートなキャッシュ戦略でリソース利用を最適化 + - 遅延読み込みで不要なリクエストを回避 + - 適切なキャッシュクリアでローカルストレージと性能を両立 + +## 開発ガイドライン + +コード品質と一貫性を保つため、開発ガイドラインを整備しています: +- [Development Guidelines](docs/guidelines_en.md) + +## プロジェクト構成 + +

+lib/
+├── core/                 # コア機能
+├── data/                # データレイヤー
+├── domain/              # ドメインレイヤー
+├── presentation/        # プレゼンテーションレイヤー
+└── common/             # 共通機能
+
+ +## はじめに + +1. リポジトリをクローン +```bash +git clone [repository-url] +``` + +2. 依存関係をインストール +```bash +flutter pub get +``` + +3. アプリを実行 +```bash +flutter run +``` + +## 主な機能 + +- モダンな UI デザイン +- スムーズなアニメーション +- ASMR 再生コントロール +- プレイリスト管理 +- 検索機能 +- お気に入り機能 + +## コントリビュート + +コントリビュート前に [Development Guidelines](docs/guidelines_en.md) を確認してください。 + +## ライセンス + +本プロジェクトは Creative Commons Attribution-NonCommercial-ShareAlike License (CC BY-NC-SA) のもとで公開されています。詳細は [LICENSE](LICENSE) を参照してください。このライセンスでは、適切なクレジット表示と同一ライセンスでの公開を条件に、非商用での改変・再配布が可能です。 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 7c56964..163000d 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 14.0 diff --git a/ios/Podfile b/ios/Podfile index d97f17e..ef7020e 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +# Define a global platform for your project +platform :ios, '14.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 58ef42a..4f037a1 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -473,7 +473,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -602,7 +602,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -653,7 +653,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/pubspec.yaml b/pubspec.yaml index f3d7cad..49476b3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.2.0+1 +version: 1.2.1 environment: sdk: ">=3.2.3 <4.0.0" From c7ea89d78bd2cfe9bf6c5f4c55dd1e88451a0c83 Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Thu, 19 Feb 2026 02:29:23 +0900 Subject: [PATCH 23/30] feat: enhance WorkGrid and WorkRow to support dynamic column count and spacing --- lib/widgets/work_grid.dart | 1 + .../work_grid/components/grid_loading.dart | 50 +++++++++++-------- lib/widgets/work_row.dart | 45 ++++++++--------- pubspec.yaml | 2 +- 4 files changed, 54 insertions(+), 44 deletions(-) diff --git a/lib/widgets/work_grid.dart b/lib/widgets/work_grid.dart index 24ee888..df49d45 100644 --- a/lib/widgets/work_grid.dart +++ b/lib/widgets/work_grid.dart @@ -33,6 +33,7 @@ class WorkGrid extends StatelessWidget { works: rows[index], onWorkTap: onWorkTap, spacing: columnSpacing, + columnsCount: columnsCount, ), ); }, diff --git a/lib/widgets/work_grid/components/grid_loading.dart b/lib/widgets/work_grid/components/grid_loading.dart index 95d463f..1dffd9d 100644 --- a/lib/widgets/work_grid/components/grid_loading.dart +++ b/lib/widgets/work_grid/components/grid_loading.dart @@ -1,32 +1,42 @@ import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; +import 'package:asmrapp/presentation/layouts/work_layout_config.dart'; class GridLoading extends StatelessWidget { const GridLoading({super.key}); @override Widget build(BuildContext context) { - return Shimmer.fromColors( - baseColor: Theme.of(context).colorScheme.surfaceContainerHighest, - highlightColor: Theme.of(context).colorScheme.surface, - child: GridView.builder( - padding: const EdgeInsets.all(16), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - childAspectRatio: 0.75, - crossAxisSpacing: 16, - mainAxisSpacing: 16, - ), - itemCount: 6, - itemBuilder: (context, index) { - return Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), + return LayoutBuilder( + builder: (context, constraints) { + final deviceType = DeviceType.fromWidth(constraints.maxWidth); + final columnsCount = WorkLayoutConfig.getColumnsCount(deviceType); + final spacing = WorkLayoutConfig.getSpacing(deviceType); + final padding = WorkLayoutConfig.getPadding(deviceType); + + return Shimmer.fromColors( + baseColor: Theme.of(context).colorScheme.surfaceContainerHighest, + highlightColor: Theme.of(context).colorScheme.surface, + child: GridView.builder( + padding: padding, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: columnsCount, + childAspectRatio: 0.75, + crossAxisSpacing: spacing, + mainAxisSpacing: spacing, ), - ); - }, - ), + itemCount: columnsCount * 3, + itemBuilder: (context, index) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + ); + }, + ), + ); + }, ); } } diff --git a/lib/widgets/work_row.dart b/lib/widgets/work_row.dart index c2a5a79..94d5f8f 100644 --- a/lib/widgets/work_row.dart +++ b/lib/widgets/work_row.dart @@ -6,42 +6,41 @@ class WorkRow extends StatelessWidget { final List works; final void Function(Work work)? onWorkTap; final double spacing; + final int columnsCount; const WorkRow({ super.key, required this.works, this.onWorkTap, this.spacing = 8.0, + this.columnsCount = 2, }); @override Widget build(BuildContext context) { + final children = []; + + for (var i = 0; i < columnsCount; i++) { + if (i > 0) { + children.add(SizedBox(width: spacing)); + } + + children.add( + Expanded( + child: i < works.length + ? WorkCard( + work: works[i], + onTap: onWorkTap != null ? () => onWorkTap!(works[i]) : null, + ) + : const SizedBox.shrink(), + ), + ); + } + return IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // 第一个卡片 - Expanded( - child: works.isNotEmpty - ? WorkCard( - work: works[0], - onTap: - onWorkTap != null ? () => onWorkTap!(works[0]) : null, - ) - : const SizedBox.shrink(), - ), - SizedBox(width: spacing), - // 第二个卡片或占位符 - Expanded( - child: works.length > 1 - ? WorkCard( - work: works[1], - onTap: - onWorkTap != null ? () => onWorkTap!(works[1]) : null, - ) - : const SizedBox.shrink(), // 空占位符,保持两列布局 - ), - ], + children: children, ), ); } diff --git a/pubspec.yaml b/pubspec.yaml index 49476b3..6480144 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.2.1 +version: 1.2.2 environment: sdk: ">=3.2.3 <4.0.0" From 782fc47fdb7b5c8874a65b3888c5b11454f94617 Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Thu, 19 Feb 2026 10:57:03 +0900 Subject: [PATCH 24/30] feat: simplify file selection logic in DownloadFileSelectionDialog --- .../download_file_selection_dialog.dart | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/widgets/detail/download_file_selection_dialog.dart b/lib/widgets/detail/download_file_selection_dialog.dart index 696241b..2f79121 100644 --- a/lib/widgets/detail/download_file_selection_dialog.dart +++ b/lib/widgets/detail/download_file_selection_dialog.dart @@ -52,16 +52,12 @@ class _DownloadFileSelectionDialogState style: Theme.of(context).textTheme.bodyMedium, ), ), - if (hasDownloadableFiles && - _selectedPaths.length < _downloadableFiles.length) + if (hasDownloadableFiles) TextButton( - onPressed: () { - setState(() { - _selectedPaths - ..clear() - ..addAll(_downloadableFiles.keys); - }); - }, + onPressed: + _selectedPaths.length == _downloadableFiles.length + ? null + : _selectAllFiles, child: Text(l10n.downloadSelectAll), ), if (_selectedPaths.isNotEmpty) @@ -391,6 +387,14 @@ class _DownloadFileSelectionDialogState }); } + void _selectAllFiles() { + setState(() { + _selectedPaths + ..clear() + ..addAll(_downloadableFiles.keys); + }); + } + String _displayName(Child node) { final title = node.title?.trim(); if (title != null && title.isNotEmpty) { From 06f497335fb1085da3be6c6fd78ba054db553648 Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Thu, 19 Feb 2026 11:02:57 +0900 Subject: [PATCH 25/30] feat: refactor WorkCover widget layout for improved structure and readability --- lib/widgets/detail/work_cover.dart | 98 ++++++++++++++++-------------- 1 file changed, 54 insertions(+), 44 deletions(-) diff --git a/lib/widgets/detail/work_cover.dart b/lib/widgets/detail/work_cover.dart index de4ca14..e507b49 100644 --- a/lib/widgets/detail/work_cover.dart +++ b/lib/widgets/detail/work_cover.dart @@ -19,55 +19,65 @@ class WorkCover extends StatelessWidget { @override Widget build(BuildContext context) { - Widget content = Stack( - children: [ - AspectRatio( + final screenHeight = MediaQuery.sizeOf(context).height; + + Widget content = Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: screenHeight), + child: AspectRatio( aspectRatio: 195 / 146, - child: CachedNetworkImage( - imageUrl: imageUrl, - fit: BoxFit.cover, - width: double.infinity, - height: double.infinity, - ), - ), - Positioned( - left: 8, - top: 8, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: .7), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - sourceId, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Colors.white, - fontSize: 12, + child: Stack( + fit: StackFit.expand, + children: [ + CachedNetworkImage( + imageUrl: imageUrl, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + ), + Positioned( + left: 8, + top: 8, + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: .7), + borderRadius: BorderRadius.circular(4), ), - ), - ), - ), - if (releaseDate != null) - Positioned( - right: 8, - bottom: 8, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.7), - borderRadius: BorderRadius.circular(4), + child: Text( + sourceId, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.white, + fontSize: 12, + ), + ), + ), ), - child: Text( - releaseDate!, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Colors.white, - fontSize: 12, + if (releaseDate != null) + Positioned( + right: 8, + bottom: 8, + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.7), + borderRadius: BorderRadius.circular(4), ), - ), - ), + child: Text( + releaseDate!, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.white, + fontSize: 12, + ), + ), + ), + ), + ], ), - ], + ), + ), ); if (heroTag != null) { From ecb9c97f4f9386ae59971a1ffa463772d6e7f2ea Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Thu, 19 Feb 2026 11:09:44 +0900 Subject: [PATCH 26/30] feat: enhance FilePreviewDialog to support image navigation and previewing --- lib/screens/detail_screen.dart | 58 +++++ lib/widgets/detail/file_preview_dialog.dart | 244 ++++++++++++++++++-- 2 files changed, 277 insertions(+), 25 deletions(-) diff --git a/lib/screens/detail_screen.dart b/lib/screens/detail_screen.dart index 76cbfc1..423554a 100644 --- a/lib/screens/detail_screen.dart +++ b/lib/screens/detail_screen.dart @@ -146,12 +146,19 @@ class DetailScreen extends StatelessWidget { } if (viewModel.canPreviewFile(file)) { + final imageFiles = _resolveImageFilesForPreview(viewModel, file); + final initialImageIndex = imageFiles == null + ? null + : _findImageIndexForPreview(imageFiles, file); + if (!context.mounted) return; await showDialog( context: context, builder: (dialogContext) => FilePreviewDialog( file: file, loadTextPreview: viewModel.loadTextPreview, + imageFiles: imageFiles, + initialImageIndex: initialImageIndex, ), ); return; @@ -259,6 +266,57 @@ class DetailScreen extends StatelessWidget { return context.l10n.playFailed(error.toString()); } + List? _resolveImageFilesForPreview( + DetailViewModel viewModel, + Child file, + ) { + if (!FilePreviewUtils.isImage(file)) { + return null; + } + + final rootChildren = viewModel.files?.children; + if (rootChildren == null || rootChildren.isEmpty) { + return [file]; + } + + final imageFiles = _collectImageFilesFromTree(rootChildren); + if (imageFiles.isEmpty) { + return [file]; + } + + return imageFiles; + } + + List _collectImageFilesFromTree(List nodes) { + final imageFiles = []; + + for (final node in nodes) { + if (node.type == 'folder') { + final children = node.children; + if (children != null && children.isNotEmpty) { + imageFiles.addAll(_collectImageFilesFromTree(children)); + } + continue; + } + + if (FilePreviewUtils.isImage(node)) { + imageFiles.add(node); + } + } + + return imageFiles; + } + + int _findImageIndexForPreview(List imageFiles, Child targetFile) { + final identityIndex = + imageFiles.indexWhere((imageFile) => identical(imageFile, targetFile)); + if (identityIndex >= 0) { + return identityIndex; + } + + return imageFiles.indexOf(targetFile); + } + void _openWorkDetail(BuildContext context, Work targetWork) { Navigator.push( context, diff --git a/lib/widgets/detail/file_preview_dialog.dart b/lib/widgets/detail/file_preview_dialog.dart index e045226..ea177df 100644 --- a/lib/widgets/detail/file_preview_dialog.dart +++ b/lib/widgets/detail/file_preview_dialog.dart @@ -3,31 +3,149 @@ import 'package:asmrapp/data/models/files/child.dart'; import 'package:asmrapp/l10n/l10n.dart'; import 'package:flutter/material.dart'; -class FilePreviewDialog extends StatelessWidget { +class FilePreviewDialog extends StatefulWidget { final Child file; final Future Function(Child file) loadTextPreview; + final List? imageFiles; + final int? initialImageIndex; const FilePreviewDialog({ super.key, required this.file, required this.loadTextPreview, + this.imageFiles, + this.initialImageIndex, }); + @override + State createState() => _FilePreviewDialogState(); +} + +class _FilePreviewDialogState extends State { + late final bool _imageMode; + late final List _imageFiles; + late int _currentImageIndex; + + @override + void initState() { + super.initState(); + _imageMode = FilePreviewUtils.isImage(widget.file); + _imageFiles = _resolveImageFiles(); + _currentImageIndex = _resolveInitialImageIndex(); + } + + List _resolveImageFiles() { + if (!_imageMode) { + return const []; + } + + final candidates = widget.imageFiles; + if (candidates == null || candidates.isEmpty) { + return [widget.file]; + } + + final imageCandidates = + candidates.where(FilePreviewUtils.isImage).toList(growable: false); + if (imageCandidates.isEmpty) { + return [widget.file]; + } + + return imageCandidates; + } + + int _resolveInitialImageIndex() { + if (!_imageMode) return 0; + + final explicitIndex = widget.initialImageIndex; + if (explicitIndex != null && + explicitIndex >= 0 && + explicitIndex < _imageFiles.length) { + return explicitIndex; + } + + final fileIndex = _imageFiles.indexOf(widget.file); + if (fileIndex >= 0) { + return fileIndex; + } + + return 0; + } + + Child get _currentFile { + if (_imageMode) { + return _imageFiles[_currentImageIndex]; + } + return widget.file; + } + + bool get _hasPreviousImage => _currentImageIndex > 0; + + bool get _hasNextImage => _currentImageIndex < _imageFiles.length - 1; + + void _showPreviousImage() { + if (!_hasPreviousImage) return; + setState(() { + _currentImageIndex -= 1; + }); + } + + void _showNextImage() { + if (!_hasNextImage) return; + setState(() { + _currentImageIndex += 1; + }); + } + + String _dialogTitle() { + final title = _currentFile.title ?? ''; + if (!_imageMode || _imageFiles.length <= 1) { + return title; + } + + final indexText = '[$_currentImageIndex]'; + if (title.isEmpty) { + return indexText; + } + return '$title $indexText'; + } + @override Widget build(BuildContext context) { - final title = file.title ?? ''; + final canNavigateImages = _imageMode && _imageFiles.length > 1; + final materialLocalizations = MaterialLocalizations.of(context); return Dialog.fullscreen( child: Scaffold( appBar: AppBar( - title: Text(title), + title: Text(_dialogTitle()), + actions: canNavigateImages + ? [ + IconButton( + tooltip: materialLocalizations.previousPageTooltip, + onPressed: _hasPreviousImage ? _showPreviousImage : null, + icon: const Icon(Icons.navigate_before), + ), + IconButton( + tooltip: materialLocalizations.nextPageTooltip, + onPressed: _hasNextImage ? _showNextImage : null, + icon: const Icon(Icons.navigate_next), + ), + ] + : null, ), body: SafeArea( - child: FilePreviewUtils.isImage(file) - ? _ImagePreview(file: file) + child: _imageMode + ? _ImagePreview( + file: _currentFile, + onPrevious: _hasPreviousImage ? _showPreviousImage : null, + onNext: _hasNextImage ? _showNextImage : null, + currentImageIndex: _currentImageIndex, + maxImageIndex: _imageFiles.length - 1, + canNavigateImages: canNavigateImages, + ) : _TextPreview( - file: file, - loadTextPreview: loadTextPreview, + file: widget.file, + loadTextPreview: widget.loadTextPreview, ), ), ), @@ -37,8 +155,20 @@ class FilePreviewDialog extends StatelessWidget { class _ImagePreview extends StatelessWidget { final Child file; + final VoidCallback? onPrevious; + final VoidCallback? onNext; + final int currentImageIndex; + final int maxImageIndex; + final bool canNavigateImages; - const _ImagePreview({required this.file}); + const _ImagePreview({ + required this.file, + required this.onPrevious, + required this.onNext, + required this.currentImageIndex, + required this.maxImageIndex, + required this.canNavigateImages, + }); @override Widget build(BuildContext context) { @@ -47,24 +177,88 @@ class _ImagePreview extends StatelessWidget { return _PreviewMessage(message: context.l10n.playUrlMissing); } - return InteractiveViewer( - minScale: 1, - maxScale: 4, - child: Center( - child: Image.network( - url, - fit: BoxFit.contain, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return const Center(child: CircularProgressIndicator()); - }, - errorBuilder: (context, error, stackTrace) { - return _PreviewMessage( - message: context.l10n.operationFailed(error.toString()), - ); - }, + final colorScheme = Theme.of(context).colorScheme; + final materialLocalizations = MaterialLocalizations.of(context); + + return Stack( + children: [ + Positioned.fill( + child: InteractiveViewer( + key: ValueKey(url), + minScale: 1, + maxScale: 4, + child: Center( + child: Image.network( + url, + fit: BoxFit.contain, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return const Center(child: CircularProgressIndicator()); + }, + errorBuilder: (context, error, stackTrace) { + return _PreviewMessage( + message: context.l10n.operationFailed(error.toString()), + ); + }, + ), + ), + ), ), - ), + if (canNavigateImages) + Positioned( + left: 16, + right: 16, + bottom: 16, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DecoratedBox( + decoration: BoxDecoration( + color: colorScheme.surface.withValues(alpha: 0.9), + borderRadius: BorderRadius.circular(999), + ), + child: IconButton( + tooltip: materialLocalizations.previousPageTooltip, + onPressed: onPrevious, + icon: const Icon(Icons.navigate_before), + ), + ), + const SizedBox(width: 12), + DecoratedBox( + decoration: BoxDecoration( + color: colorScheme.surface.withValues(alpha: 0.9), + borderRadius: BorderRadius.circular(999), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 8, + ), + child: Text( + '$currentImageIndex / $maxImageIndex', + style: TextStyle( + color: colorScheme.onSurface, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + const SizedBox(width: 12), + DecoratedBox( + decoration: BoxDecoration( + color: colorScheme.surface.withValues(alpha: 0.9), + borderRadius: BorderRadius.circular(999), + ), + child: IconButton( + tooltip: materialLocalizations.nextPageTooltip, + onPressed: onNext, + icon: const Icon(Icons.navigate_next), + ), + ), + ], + ), + ), + ], ); } } From 60ecaed48fea20801d4728025cce1d77cc224f26 Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Thu, 19 Feb 2026 11:35:35 +0900 Subject: [PATCH 27/30] feat: add localization support for tags and work titles; refactor related components --- lib/common/utils/work_localizations.dart | 110 ++++++++++++++++++ lib/widgets/detail/work_info.dart | 22 ++-- lib/widgets/detail/work_info_header.dart | 13 ++- lib/widgets/player/player_work_info.dart | 8 +- .../work_card/components/work_tags_panel.dart | 56 ++++----- .../work_card/components/work_title.dart | 5 +- 6 files changed, 165 insertions(+), 49 deletions(-) create mode 100644 lib/common/utils/work_localizations.dart diff --git a/lib/common/utils/work_localizations.dart b/lib/common/utils/work_localizations.dart new file mode 100644 index 0000000..765c095 --- /dev/null +++ b/lib/common/utils/work_localizations.dart @@ -0,0 +1,110 @@ +import 'package:asmrapp/data/models/works/tag.dart'; +import 'package:asmrapp/data/models/works/work.dart'; +import 'package:flutter/widgets.dart'; + +enum _PreferredLanguage { + chinese, + japanese, + english, + other, +} + +_PreferredLanguage _resolvePreferredLanguage(Locale locale) { + switch (locale.languageCode.toLowerCase()) { + case 'zh': + return _PreferredLanguage.chinese; + case 'ja': + return _PreferredLanguage.japanese; + case 'en': + return _PreferredLanguage.english; + default: + return _PreferredLanguage.other; + } +} + +String _firstNonEmpty(Iterable values) { + for (final value in values) { + final normalized = value?.trim(); + if (normalized != null && normalized.isNotEmpty) { + return normalized; + } + } + return ''; +} + +List _preferredEditionLangCodes(Locale locale) { + switch (_resolvePreferredLanguage(locale)) { + case _PreferredLanguage.chinese: + return const ['CHI_HANS', 'CHI_HANT']; + case _PreferredLanguage.japanese: + return const ['JPN']; + case _PreferredLanguage.english: + return const ['ENG']; + case _PreferredLanguage.other: + return const []; + } +} + +extension TagLocalizationX on Tag { + String localizedName(Locale locale) { + switch (_resolvePreferredLanguage(locale)) { + case _PreferredLanguage.chinese: + return _firstNonEmpty([ + i18n?.zhCn?.name, + i18n?.jaJp?.name, + i18n?.enUs?.name, + name, + ]); + case _PreferredLanguage.japanese: + return _firstNonEmpty([ + i18n?.jaJp?.name, + i18n?.zhCn?.name, + i18n?.enUs?.name, + name, + ]); + case _PreferredLanguage.english: + return _firstNonEmpty([ + i18n?.enUs?.name, + i18n?.jaJp?.name, + i18n?.zhCn?.name, + name, + ]); + case _PreferredLanguage.other: + return _firstNonEmpty([ + i18n?.enUs?.name, + i18n?.jaJp?.name, + i18n?.zhCn?.name, + name, + ]); + } + } +} + +extension WorkLocalizationX on Work { + String localizedTitle(Locale locale) { + final preferredTitles = []; + final preferredLangCodes = _preferredEditionLangCodes(locale); + final editions = otherLanguageEditionsInDb ?? const []; + + for (final langCode in preferredLangCodes) { + for (final edition in editions) { + if (edition.lang == langCode) { + preferredTitles.add(edition.title); + } + } + } + + return _firstNonEmpty([ + ...preferredTitles, + title, + sourceId, + ]); + } + + String localizedCircleName() { + return _firstNonEmpty([ + circle?.name, + name, + ]); + } +} diff --git a/lib/widgets/detail/work_info.dart b/lib/widgets/detail/work_info.dart index fbf5dfe..b261bdb 100644 --- a/lib/widgets/detail/work_info.dart +++ b/lib/widgets/detail/work_info.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/common/utils/work_localizations.dart'; import 'package:asmrapp/widgets/common/tag_chip.dart'; import 'package:asmrapp/widgets/detail/work_info_header.dart'; import 'package:asmrapp/utils/logger.dart'; @@ -13,16 +14,10 @@ class WorkInfo extends StatelessWidget { required this.work, }); - String _getLocalizedTagName(Tag tag) { - final zhName = tag.i18n?.zhCn?.name; - if (zhName != null) return zhName; - final jaName = tag.i18n?.jaJp?.name; - if (jaName != null) return jaName; - return tag.name ?? ''; - } - - void _onTagTap(BuildContext context, Tag tag) { - final keyword = tag.name ?? ''; + void _onTagTap(BuildContext context, Tag tag, Locale locale) { + final keyword = (tag.name ?? '').trim().isNotEmpty + ? (tag.name ?? '').trim() + : tag.localizedName(locale); if (keyword.isEmpty) return; AppLogger.debug('点击标签: $keyword'); @@ -35,6 +30,8 @@ class WorkInfo extends StatelessWidget { @override Widget build(BuildContext context) { + final locale = Localizations.localeOf(context); + return Padding( padding: const EdgeInsets.all(16.0), child: Column( @@ -47,9 +44,10 @@ class WorkInfo extends StatelessWidget { spacing: 8, runSpacing: 8, children: work.tags! + .where((tag) => tag.localizedName(locale).isNotEmpty) .map((tag) => TagChip( - text: _getLocalizedTagName(tag), - onTap: () => _onTagTap(context, tag), + text: tag.localizedName(locale), + onTap: () => _onTagTap(context, tag, locale), )) .toList(), ), diff --git a/lib/widgets/detail/work_info_header.dart b/lib/widgets/detail/work_info_header.dart index 72a7153..f085121 100644 --- a/lib/widgets/detail/work_info_header.dart +++ b/lib/widgets/detail/work_info_header.dart @@ -1,4 +1,5 @@ import 'package:asmrapp/data/models/works/work.dart'; +import 'package:asmrapp/common/utils/work_localizations.dart'; import 'package:asmrapp/utils/logger.dart'; import 'package:asmrapp/widgets/common/tag_chip.dart'; import 'package:asmrapp/widgets/detail/work_stats_info.dart'; @@ -26,11 +27,15 @@ class WorkInfoHeader extends StatelessWidget { @override Widget build(BuildContext context) { + final locale = Localizations.localeOf(context); + final localizedTitle = work.localizedTitle(locale); + final circleName = work.localizedCircleName(); + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - work.title ?? '', + localizedTitle, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), @@ -40,12 +45,12 @@ class WorkInfoHeader extends StatelessWidget { spacing: 8, runSpacing: 8, children: [ - if (work.circle?.name != null) + if (circleName.isNotEmpty) TagChip( - text: work.circle?.name ?? '', + text: circleName, backgroundColor: Colors.orange.withValues(alpha: 0.2), textColor: Colors.orange[700], - onTap: () => _onTagTap(context, work.circle?.name ?? ''), + onTap: () => _onTagTap(context, circleName), ), ...?work.vas?.map( (va) => TagChip( diff --git a/lib/widgets/player/player_work_info.dart b/lib/widgets/player/player_work_info.dart index c1a1f15..79b218a 100644 --- a/lib/widgets/player/player_work_info.dart +++ b/lib/widgets/player/player_work_info.dart @@ -1,4 +1,5 @@ import 'package:asmrapp/core/audio/models/playback_context.dart'; +import 'package:asmrapp/common/utils/work_localizations.dart'; import 'package:flutter/material.dart'; import 'package:marquee/marquee.dart'; import 'package:asmrapp/l10n/l10n.dart'; @@ -13,6 +14,9 @@ class PlayerWorkInfo extends StatelessWidget { @override Widget build(BuildContext context) { + final locale = Localizations.localeOf(context); + final workTitle = this.context?.work.localizedTitle(locale) ?? ''; + return Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), @@ -22,7 +26,9 @@ class PlayerWorkInfo extends StatelessWidget { SizedBox( height: Theme.of(context).textTheme.titleMedium!.fontSize! * 1.5, child: Marquee( - text: this.context?.work.title ?? context.l10n.unknownWorkTitle, + text: workTitle.isNotEmpty + ? workTitle + : context.l10n.unknownWorkTitle, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), diff --git a/lib/widgets/work_card/components/work_tags_panel.dart b/lib/widgets/work_card/components/work_tags_panel.dart index 81e8613..528ec02 100644 --- a/lib/widgets/work_card/components/work_tags_panel.dart +++ b/lib/widgets/work_card/components/work_tags_panel.dart @@ -1,5 +1,5 @@ -import 'package:asmrapp/data/models/works/tag.dart'; import 'package:asmrapp/data/models/works/work.dart'; +import 'package:asmrapp/common/utils/work_localizations.dart'; import 'package:flutter/material.dart'; import 'package:asmrapp/l10n/l10n.dart'; @@ -11,21 +11,16 @@ class WorkTagsPanel extends StatelessWidget { required this.work, }); - String _getLocalizedTagName(Tag tag) { - final zhName = tag.i18n?.zhCn?.name; - if (zhName != null) return zhName; - final jaName = tag.i18n?.jaJp?.name; - if (jaName != null) return jaName; - return tag.name ?? ''; - } - @override Widget build(BuildContext context) { + final locale = Localizations.localeOf(context); + final circleName = work.localizedCircleName(); + return Wrap( spacing: 4, runSpacing: 2, children: [ - if (work.circle?.name != null) + if (circleName.isNotEmpty) Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( @@ -33,7 +28,7 @@ class WorkTagsPanel extends StatelessWidget { borderRadius: BorderRadius.circular(4), ), child: Text( - work.circle?.name ?? '', + circleName, style: TextStyle( fontSize: 10, color: Colors.orange[700], @@ -69,26 +64,25 @@ class WorkTagsPanel extends StatelessWidget { ), ), ), - ...work.tags - ?.map((tag) => Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .surfaceContainerHighest, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - _getLocalizedTagName(tag), - style: TextStyle( - fontSize: 10, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - )) - .toList() ?? - [], + ...(work.tags ?? const []) + .map((tag) => tag.localizedName(locale)) + .where((localizedName) => localizedName.isNotEmpty) + .map( + (localizedName) => Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + localizedName, + style: TextStyle( + fontSize: 10, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ), ], ); } diff --git a/lib/widgets/work_card/components/work_title.dart b/lib/widgets/work_card/components/work_title.dart index 5e7476b..a2ab959 100644 --- a/lib/widgets/work_card/components/work_title.dart +++ b/lib/widgets/work_card/components/work_title.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:asmrapp/data/models/works/work.dart'; +import 'package:asmrapp/common/utils/work_localizations.dart'; class WorkTitle extends StatelessWidget { final Work work; @@ -11,8 +12,10 @@ class WorkTitle extends StatelessWidget { @override Widget build(BuildContext context) { + final locale = Localizations.localeOf(context); + return Text( - work.title ?? '', + work.localizedTitle(locale), style: Theme.of(context).textTheme.titleMedium?.copyWith( fontSize: 14, ), From 464ce9dd39c90844195d756c2db85d7e0f48f328 Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Thu, 19 Feb 2026 11:41:10 +0900 Subject: [PATCH 28/30] feat: enhance DetailScreen to display localized title in app bar --- lib/screens/detail_screen.dart | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/screens/detail_screen.dart b/lib/screens/detail_screen.dart index 423554a..334b365 100644 --- a/lib/screens/detail_screen.dart +++ b/lib/screens/detail_screen.dart @@ -1,3 +1,4 @@ +import 'package:asmrapp/common/utils/work_localizations.dart'; import 'package:asmrapp/common/utils/file_preview_utils.dart'; import 'package:asmrapp/core/download/download_request_item.dart'; import 'package:asmrapp/data/models/files/child.dart'; @@ -32,6 +33,8 @@ class DetailScreen extends StatelessWidget { @override Widget build(BuildContext context) { final rjCode = _extractRjCode(); + final localizedTitle = work.localizedTitle(Localizations.localeOf(context)); + final appBarTitle = _buildAppBarTitle(rjCode, localizedTitle); return ChangeNotifierProvider( create: (_) => DetailViewModel( @@ -39,7 +42,7 @@ class DetailScreen extends StatelessWidget { )..loadFiles(), child: Scaffold( appBar: AppBar( - title: Text(work.sourceId ?? ''), + title: Text(appBarTitle), actions: [ if (rjCode != null) IconButton( @@ -123,6 +126,20 @@ class DetailScreen extends StatelessWidget { ); } + String _buildAppBarTitle(String? rjCode, String title) { + final normalizedTitle = title.trim(); + if (rjCode == null || rjCode.isEmpty) { + return normalizedTitle; + } + if (normalizedTitle.isEmpty) { + return rjCode; + } + if (normalizedTitle.toUpperCase() == rjCode.toUpperCase()) { + return rjCode; + } + return '$rjCode - $normalizedTitle'; + } + Future _handleFileTap( BuildContext context, DetailViewModel viewModel, From 3ea93676ae9fd1286562807b468c11666cf58694 Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Thu, 19 Feb 2026 11:44:20 +0900 Subject: [PATCH 29/30] feat: add login requirement handling in PlaylistsViewModel and update UI accordingly --- .../viewmodels/playlists_viewmodel.dart | 18 ++++++++++++++++- .../playlists/playlists_list_view.dart | 20 ++++++++++++------- lib/screens/main_screen.dart | 7 +++---- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/lib/presentation/viewmodels/playlists_viewmodel.dart b/lib/presentation/viewmodels/playlists_viewmodel.dart index 44f7d33..1d0d38a 100644 --- a/lib/presentation/viewmodels/playlists_viewmodel.dart +++ b/lib/presentation/viewmodels/playlists_viewmodel.dart @@ -2,16 +2,19 @@ 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/presentation/viewmodels/auth_viewmodel.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(); + final AuthViewModel _authViewModel; List? _playlists; bool _isLoading = false; String? _error; + bool _loginRequired = false; Pagination? _pagination; int _currentPage = 1; @@ -27,6 +30,7 @@ class PlaylistsViewModel extends ChangeNotifier { List get playlists => _playlists ?? []; bool get isLoading => _isLoading; String? get error => _error; + bool get loginRequired => _loginRequired; int get currentPage => _currentPage; int? get totalPages => _pagination?.totalCount != null && _pagination?.pageSize != null @@ -42,7 +46,7 @@ class PlaylistsViewModel extends ChangeNotifier { ? (_worksPagination!.totalCount! / _worksPagination!.pageSize!).ceil() : null; - PlaylistsViewModel() { + PlaylistsViewModel(this._authViewModel) { loadPlaylists(); } @@ -51,8 +55,19 @@ class PlaylistsViewModel extends ChangeNotifier { if (_isLoading) return; if (page < 1 || (totalPages != null && page > totalPages!)) return; + if (!_authViewModel.isLoggedIn) { + _playlists = []; + _pagination = null; + _currentPage = 1; + _loginRequired = true; + _error = null; + notifyListeners(); + return; + } + _isLoading = true; _error = null; + _loginRequired = false; notifyListeners(); try { @@ -63,6 +78,7 @@ class PlaylistsViewModel extends ChangeNotifier { AppLogger.info('第$page页播放列表加载成功: ${_playlists?.length ?? 0}个播放列表'); } catch (e) { AppLogger.error('加载播放列表失败', e); + _loginRequired = false; _error = e.toString(); } finally { _isLoading = false; diff --git a/lib/screens/contents/playlists/playlists_list_view.dart b/lib/screens/contents/playlists/playlists_list_view.dart index 7c03d78..f91d8f8 100644 --- a/lib/screens/contents/playlists/playlists_list_view.dart +++ b/lib/screens/contents/playlists/playlists_list_view.dart @@ -18,21 +18,27 @@ class PlaylistsListView extends StatelessWidget { Widget build(BuildContext context) { return Consumer( builder: (context, viewModel, child) { + final errorMessage = viewModel.loginRequired + ? context.l10n.pleaseLogin + : viewModel.error; + if (viewModel.isLoading && viewModel.playlists.isEmpty) { return const Center(child: CircularProgressIndicator()); } - if (viewModel.error != null && viewModel.playlists.isEmpty) { + if (errorMessage != null && viewModel.playlists.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text(viewModel.error!), - const SizedBox(height: 16), - ElevatedButton( - onPressed: viewModel.refresh, - child: Text(context.l10n.retry), - ), + Text(errorMessage), + if (!viewModel.loginRequired) ...[ + const SizedBox(height: 16), + ElevatedButton( + onPressed: viewModel.refresh, + child: Text(context.l10n.retry), + ), + ], ], ), ); diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index be79477..f351fc7 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -56,12 +56,11 @@ class _MainScreenState extends State { super.initState(); // 初始化所有 ViewModel // 注意初始化顺序,如果有依赖关系需要先初始化依赖项 + final authViewModel = Provider.of(context, listen: false); _homeViewModel = HomeViewModel(); _popularViewModel = PopularViewModel(); - _recommendViewModel = RecommendViewModel( - Provider.of(context, listen: false), - ); - _playlistsViewModel = PlaylistsViewModel(); + _recommendViewModel = RecommendViewModel(authViewModel); + _playlistsViewModel = PlaylistsViewModel(authViewModel); } void _onPageChanged(int index) { From eb94b0dd0f0aca7a1843dd60f5aaa0872b63e90f Mon Sep 17 00:00:00 2001 From: h-akatuki Date: Thu, 19 Feb 2026 11:49:47 +0900 Subject: [PATCH 30/30] feat: update README files for enhanced localization and project details; bump version to 1.2.3 --- README.md | 71 ++++++++++++++++++++++++++++++---------------------- README_en.md | 62 ++++++++++++++++++++++++++++++++------------- README_ja.md | 71 ++++++++++++++++++++++++++++++---------------------- pubspec.yaml | 2 +- 4 files changed, 127 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index 34511b9..fd3dbf2 100644 --- a/README.md +++ b/README.md @@ -2,26 +2,44 @@ [English](README_en.md) | [日本語](README_ja.md) -一个使用 Flutter 构建的 ASMR.ONE 客户端。 +Yuro 是一个使用 Flutter 构建的 ASMR.ONE 第三方客户端,聚焦于稳定播放、下载管理与多语言体验。 ## 项目概述 -Yuro 旨在通过精美的动画和现代化的用户界面,提供流畅愉悦的 ASMR 聆听体验。 +Yuro 致力于提供流畅、轻量且现代化的 ASMR 使用体验,在播放、缓存和文件处理上做了针对性优化。 -## 特性 +## 近期更新(主要为 `8882982` 之后) -- 稳定的后台播放,再也不用担心杀后台了 -- 精美的动画效果 -- 流畅的播放体验 -- 简洁的UI设计 -- 全方位的智能缓存机制 - - 图片智能缓存:优化封面加载速度,告别重复加载 - - 字幕本地缓存:实现快速字幕匹配与加载 - - 音频文件缓存:减少重复下载,节省流量开销 -- 为服务器减轻压力 - - 智能的缓存策略确保资源高效利用 - - 懒加载机制避免无效请求 - - 合理的缓存清理机制平衡本地存储 +- 新增多语言本地化(中文 / English / 日本語)与应用内语言切换 +- 增强作品详情本地化:标题、标签等内容可按语言显示 +- 实现文件下载能力:文件选择下载、下载目录设置、下载进度与历史记录 +- 新增文件预览(音频 / 图片 / 文本)及图片浏览导航 +- 支持作品评分、退出登录确认、在浏览器打开 DLsite +- 强化播放列表登录态处理,未登录时提供引导 +- 构建与发布流程增强:新增 Windows 构建支持,CI 使用 Flutter 3.41.1 + +## 核心特性 + +- 稳定后台播放与 Mini Player +- 智能缓存机制(封面、字幕、音频) +- 搜索、收藏夹与播放列表管理 +- 推荐内容与相关推荐浏览 +- 作品标记(想听 / 在听 / 听过等)与评分 +- 下载任务管理与进度跟踪 +- 多语言界面(中文、English、日本語) + +## 支持平台 + +- Android +- iOS(14.0+) +- Windows +- GitHub Actions 提供 Android / iOS / Windows 构建产物 + +## 开发要求 + +- Flutter 3.41.1(stable) +- Dart >= 3.2.3 < 4.0.0 +- CI Java 21 ## 开发准则 @@ -32,11 +50,13 @@ Yuro 旨在通过精美的动画和现代化的用户界面,提供流畅愉悦
 lib/
-├── core/                 # 核心功能
-├── data/                # 数据层
-├── domain/              # 领域层
-├── presentation/        # 表现层
-└── common/             # 通用功能
+├── core/                 # 音频、缓存、下载、国际化、依赖注入等核心能力
+├── data/                 # API、数据模型与仓库实现
+├── presentation/         # ViewModel、布局与展示逻辑
+├── screens/              # 页面级 Screen
+├── widgets/              # 可复用 UI 组件
+├── common/               # 常量、扩展与工具方法
+└── l10n/                 # 本地化资源(ARB)
 
## 开始使用 @@ -56,19 +76,10 @@ flutter pub get flutter run ``` -## 功能特性 - -- 现代化UI设计 -- 流畅的动画效果 -- ASMR 播放控制 -- 播放列表管理 -- 搜索功能 -- 收藏功能 - ## 贡献指南 在提交贡献之前,请阅读我们的[开发准则](docs/guidelines_zh.md)。 ## 许可证 -本项目采用 Creative Commons 非商业性使用-相同方式共享许可证 (CC BY-NC-SA) - 查看 [LICENSE](LICENSE) 文件了解详细信息。该许可证允许他人修改和分享您的作品,但禁止商业用途,要求保留署名,并要求对修改后的作品以相同的许可证发布。 +本项目采用 Creative Commons 非商业性使用-相同方式共享许可证 (CC BY-NC-SA)。详情请参阅 [LICENSE](LICENSE)。 diff --git a/README_en.md b/README_en.md index eaba786..d26830d 100644 --- a/README_en.md +++ b/README_en.md @@ -1,12 +1,45 @@ -# ASMR One App +# Yuro [中文说明](README.md) | [日本語](README_ja.md) -A beautiful and modern ASMR player application built with Flutter. +Yuro is a Flutter-based third-party ASMR.ONE client focused on stable playback, download management, and multilingual usability. ## Project Overview -ASMR One App is designed to provide a smooth and enjoyable ASMR listening experience with beautiful animations and a modern user interface. +Yuro aims to provide a smooth, lightweight, and modern ASMR experience with practical optimizations around playback, caching, and file handling. + +## Recent Updates (mostly after `8882982`) + +- Added multilingual localization (Chinese / English / Japanese) and in-app language switching +- Improved localized metadata in detail views (titles, tags, and related labels) +- Added file downloads with file selection, configurable download directory, progress panel, and history +- Added file preview support (audio / image / text) with image navigation +- Added work rating, logout confirmation, and "open DLsite in browser" +- Improved playlist login-state handling with user guidance when authentication is required +- Enhanced build and release pipeline with Windows build support and Flutter 3.41.1 CI + +## Core Features + +- Stable background playback and Mini Player +- Smart caching for covers, subtitles, and audio files +- Search, favorites, and playlist management +- Recommendation views and related works browsing +- Work mark status (want/listening/listened/etc.) and rating +- Download task management with progress tracking +- Multilingual UI (Chinese, English, Japanese) + +## Supported Platforms + +- Android +- iOS (14.0+) +- Windows +- GitHub Actions builds Android / iOS / Windows artifacts + +## Development Requirements + +- Flutter 3.41.1 (stable) +- Dart >= 3.2.3 < 4.0.0 +- CI Java 21 ## Development Guidelines @@ -17,11 +50,13 @@ We maintain a comprehensive set of development guidelines to ensure code quality
 lib/
-├── core/                 # Core functionality
-├── data/                # Data layer
-├── domain/              # Domain layer
-├── presentation/        # Presentation layer
-└── common/             # Common functionality
+├── core/                 # Audio, cache, download, locale, DI, and other core modules
+├── data/                 # API layer, models, and repository implementations
+├── presentation/         # ViewModels, layouts, and presentation logic
+├── screens/              # Route-level screens
+├── widgets/              # Reusable UI components
+├── common/               # Shared constants, extensions, and utilities
+└── l10n/                 # Localization resources (ARB)
 
## Getting Started @@ -41,19 +76,10 @@ flutter pub get flutter run ``` -## Features - -- Modern UI design -- Smooth animations -- ASMR playback control -- Playlist management -- Search functionality -- Favorites collection - ## Contributing Please read our [Development Guidelines](docs/guidelines_en.md) before making a contribution. ## License -This project is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike License (CC BY-NC-SA) - see the [LICENSE](LICENSE) file for details. This license allows others to remix, tweak, and build upon your work non-commercially, as long as they credit you and license their new creations under the identical terms. +This project is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike License (CC BY-NC-SA). See [LICENSE](LICENSE) for details. diff --git a/README_ja.md b/README_ja.md index 3606327..a42cdba 100644 --- a/README_ja.md +++ b/README_ja.md @@ -2,26 +2,44 @@ [中文说明](README.md) | [English](README_en.md) -Flutter で構築された ASMR.ONE クライアントです。 +Yuro は Flutter で構築された ASMR.ONE のサードパーティクライアントで、安定した再生・ダウンロード管理・多言語対応に注力しています。 ## プロジェクト概要 -Yuro は、美しいアニメーションとモダンなユーザーインターフェースを通じて、快適でスムーズな ASMR リスニング体験を提供することを目指しています。 +Yuro は、再生・キャッシュ・ファイル処理を最適化し、軽快でモダンな ASMR 体験を提供することを目的としています。 -## 特徴 +## 最近の更新(主に `8882982` 以降) -- 安定したバックグラウンド再生 -- 美しいアニメーション表現 -- スムーズな再生体験 -- シンプルな UI デザイン -- 包括的なスマートキャッシュ機構 - - 画像キャッシュ最適化: カバー画像の読み込み速度を向上し、重複読み込みを削減 - - 字幕ローカルキャッシュ: 字幕のマッチングと読み込みを高速化 - - 音声ファイルキャッシュ: 再ダウンロードを減らし、通信量を節約 -- サーバー負荷の軽減 - - スマートなキャッシュ戦略でリソース利用を最適化 - - 遅延読み込みで不要なリクエストを回避 - - 適切なキャッシュクリアでローカルストレージと性能を両立 +- 多言語ローカライズ(中文 / English / 日本語)とアプリ内言語切り替えを追加 +- 詳細画面のローカライズを強化(作品タイトル、タグなど) +- ファイルダウンロード機能を追加(対象選択、保存先設定、進捗表示、履歴管理) +- ファイルプレビュー(音声 / 画像 / テキスト)と画像ナビゲーションに対応 +- 作品評価、ログアウト確認、DLsite をブラウザで開く機能を追加 +- プレイリストのログイン状態ハンドリングを改善し、未認証時の導線を追加 +- ビルド/リリース基盤を強化し、Windows ビルド対応と Flutter 3.41.1 CI を導入 + +## 主な機能 + +- 安定したバックグラウンド再生と Mini Player +- カバー画像・字幕・音声に対するスマートキャッシュ +- 検索、お気に入り、プレイリスト管理 +- レコメンド表示と関連作品の閲覧 +- 作品のマーク状態管理(聴きたい/聴いている/聴いた など)と評価 +- ダウンロードタスク管理と進捗トラッキング +- 多言語 UI(中文、English、日本語) + +## 対応プラットフォーム + +- Android +- iOS(14.0+) +- Windows +- GitHub Actions で Android / iOS / Windows 向け成果物をビルド + +## 開発要件 + +- Flutter 3.41.1(stable) +- Dart >= 3.2.3 < 4.0.0 +- CI Java 21 ## 開発ガイドライン @@ -32,11 +50,13 @@ Yuro は、美しいアニメーションとモダンなユーザーインター
 lib/
-├── core/                 # コア機能
-├── data/                # データレイヤー
-├── domain/              # ドメインレイヤー
-├── presentation/        # プレゼンテーションレイヤー
-└── common/             # 共通機能
+├── core/                 # 音声・キャッシュ・ダウンロード・ロケール・DI などのコア機能
+├── data/                 # API 層、データモデル、Repository 実装
+├── presentation/         # ViewModel、レイアウト、表示ロジック
+├── screens/              # 画面単位の Screen
+├── widgets/              # 再利用可能な UI コンポーネント
+├── common/               # 共通定数・拡張・ユーティリティ
+└── l10n/                 # ローカライズリソース(ARB)
 
## はじめに @@ -56,19 +76,10 @@ flutter pub get flutter run ``` -## 主な機能 - -- モダンな UI デザイン -- スムーズなアニメーション -- ASMR 再生コントロール -- プレイリスト管理 -- 検索機能 -- お気に入り機能 - ## コントリビュート コントリビュート前に [Development Guidelines](docs/guidelines_en.md) を確認してください。 ## ライセンス -本プロジェクトは Creative Commons Attribution-NonCommercial-ShareAlike License (CC BY-NC-SA) のもとで公開されています。詳細は [LICENSE](LICENSE) を参照してください。このライセンスでは、適切なクレジット表示と同一ライセンスでの公開を条件に、非商用での改変・再配布が可能です。 +本プロジェクトは Creative Commons Attribution-NonCommercial-ShareAlike License (CC BY-NC-SA) のもとで公開されています。詳細は [LICENSE](LICENSE) を参照してください。 diff --git a/pubspec.yaml b/pubspec.yaml index 6480144..828cbdb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.2.2 +version: 1.2.3 environment: sdk: ">=3.2.3 <4.0.0"