diff --git a/lib/core/storage/storage_manager.dart b/lib/core/storage/storage_manager.dart index 7e2b274..197b81a 100644 --- a/lib/core/storage/storage_manager.dart +++ b/lib/core/storage/storage_manager.dart @@ -28,6 +28,7 @@ class StorageKeys { static const String fileCacheTTLMinutes = 'guangya.fileCacheTTLMinutes'; static const String defaultFilePageSize = 'guangya.defaultFilePageSize'; static const String fastTransferSession = 'guangya.fastTransferSession'; + static const String mediaScanHistory = 'guangya.mediaScanHistory'; } class StorageManager { diff --git a/lib/models/media_library.dart b/lib/models/media_library.dart index 7d71034..3c069ef 100644 --- a/lib/models/media_library.dart +++ b/lib/models/media_library.dart @@ -455,6 +455,32 @@ class MediaLibraryScanProgress { }); } +class MediaLibraryScanLog { + final DateTime createdAt; + final String message; + final bool isError; + + const MediaLibraryScanLog({ + required this.createdAt, + required this.message, + this.isError = false, + }); + + factory MediaLibraryScanLog.fromJson(Map json) { + return MediaLibraryScanLog( + createdAt: _parseDate(json['createdAt']) ?? DateTime.now(), + message: json['message']?.toString() ?? '', + isError: json['isError'] == true, + ); + } + + Map toJson() => { + 'createdAt': createdAt.toIso8601String(), + 'message': message, + 'isError': isError, + }; +} + class ParsedMediaName { final String title; final int? year; diff --git a/lib/pages/media_library_page.dart b/lib/pages/media_library_page.dart index 0be7ee6..d9a465e 100644 --- a/lib/pages/media_library_page.dart +++ b/lib/pages/media_library_page.dart @@ -554,7 +554,16 @@ class _MediaLibraryPageState extends ConsumerState { Expanded(child: wallContent), ], ); - if (!state.isScanning) return content; + if (!state.isScanning) { + if (state.scanLogs.isEmpty) return content; + return Column( + children: [ + _recentScanLogs(context, state), + const SizedBox(height: 10), + Expanded(child: content), + ], + ); + } return Column( children: [ _scanProgress(context, state), @@ -607,32 +616,130 @@ class _MediaLibraryPageState extends ConsumerState { borderRadius: BorderRadius.circular(8), border: Border.all(color: cs.border), ), - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(width: 120, child: ShadProgress()), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - state.progress.phase, - style: TextStyle(color: cs.foreground), + Row( + children: [ + const SizedBox(width: 120, child: ShadProgress()), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + state.progress.phase, + style: TextStyle(color: cs.foreground), + ), + const SizedBox(height: 2), + Text( + '已发现 ${state.progress.completed} 个视频文件,已入库资源会实时显示。', + style: TextStyle( + fontSize: 12, + color: cs.mutedForeground, + ), + ), + ], ), - const SizedBox(height: 2), - Text( - '已发现 ${state.progress.completed} 个视频文件,已入库资源会实时显示。', - style: TextStyle(fontSize: 12, color: cs.mutedForeground), - ), - ], - ), + ), + ], ), + if (state.scanLogs.isNotEmpty) ...[ + const SizedBox(height: 10), + Divider(height: 1, color: cs.border), + const SizedBox(height: 8), + Text( + '任务日志', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: cs.mutedForeground, + ), + ), + const SizedBox(height: 4), + SizedBox( + height: 84, + child: ListView.builder( + reverse: true, + itemCount: state.scanLogs.length, + itemBuilder: (context, index) { + final log = state.scanLogs.reversed.elementAt(index); + final time = TimeOfDay.fromDateTime( + log.createdAt, + ).format(context); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Text( + '$time ${log.message}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 11, + color: log.isError + ? cs.destructive + : cs.mutedForeground, + ), + ), + ); + }, + ), + ), + ], ], ), ), ); } + Widget _recentScanLogs(BuildContext context, MediaLibraryState state) { + final cs = ShadTheme.of(context).colorScheme; + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9), + decoration: BoxDecoration( + color: cs.muted.withValues(alpha: 0.68), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: cs.border), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '最近扫描日志', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: cs.mutedForeground, + ), + ), + const SizedBox(height: 4), + SizedBox( + height: 54, + child: ListView.builder( + reverse: true, + itemCount: state.scanLogs.length, + itemBuilder: (context, index) { + final log = state.scanLogs.reversed.elementAt(index); + final time = TimeOfDay.fromDateTime( + log.createdAt, + ).format(context); + return Text( + '$time ${log.message}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 11, + color: log.isError ? cs.destructive : cs.mutedForeground, + ), + ); + }, + ), + ), + ], + ), + ); + } + Widget _mainEmpty(BuildContext context, String title, String subtitle) { final cs = ShadTheme.of(context).colorScheme; return Center( diff --git a/lib/providers/media_library_provider.dart b/lib/providers/media_library_provider.dart index 028ee01..af63832 100644 --- a/lib/providers/media_library_provider.dart +++ b/lib/providers/media_library_provider.dart @@ -16,6 +16,7 @@ class MediaLibraryState { final bool isLoading; final bool isScanning; final MediaLibraryScanProgress progress; + final List scanLogs; final String searchQuery; final String? errorMessage; final String? statusMessage; @@ -27,6 +28,7 @@ class MediaLibraryState { this.isLoading = false, this.isScanning = false, this.progress = const MediaLibraryScanProgress(), + this.scanLogs = const [], this.searchQuery = '', this.errorMessage, this.statusMessage, @@ -60,6 +62,7 @@ class MediaLibraryState { bool? isLoading, bool? isScanning, MediaLibraryScanProgress? progress, + List? scanLogs, String? searchQuery, String? errorMessage, bool clearError = false, @@ -75,6 +78,7 @@ class MediaLibraryState { isLoading: isLoading ?? this.isLoading, isScanning: isScanning ?? this.isScanning, progress: progress ?? this.progress, + scanLogs: scanLogs ?? this.scanLogs, searchQuery: searchQuery ?? this.searchQuery, errorMessage: clearError ? null : (errorMessage ?? this.errorMessage), statusMessage: clearStatus ? null : (statusMessage ?? this.statusMessage), @@ -104,10 +108,12 @@ class MediaLibraryNotifier extends StateNotifier { final items = selectedID == null ? [] : await _loadItems(selectedID); + final logs = _loadScanHistory(); state = state.copyWith( libraries: libraries, selectedLibraryID: selectedID, items: items, + scanLogs: logs, ); } catch (e) { state = state.copyWith(errorMessage: e.toString()); @@ -268,6 +274,12 @@ class MediaLibraryNotifier extends StateNotifier { state = state.copyWith( isScanning: true, progress: const MediaLibraryScanProgress(phase: '准备扫描'), + scanLogs: [ + MediaLibraryScanLog( + createdAt: DateTime.now(), + message: '任务已创建,开始扫描「${library.name}」', + ), + ], clearError: true, clearStatus: true, ); @@ -322,6 +334,11 @@ class MediaLibraryNotifier extends StateNotifier { completed: completed, ), ); + _appendScanLog( + item.tmdbID == null + ? '已入库:${file.name}(未匹配 TMDB)' + : '已识别并入库:${file.name} → ${item.title}', + ); } } @@ -339,6 +356,7 @@ class MediaLibraryNotifier extends StateNotifier { for (final source in library.sources) { if (_cancelScan) break; + _appendScanLog('扫描目录:${source.path}'); state = state.copyWith( progress: MediaLibraryScanProgress( phase: '扫描 ${source.path}', @@ -374,13 +392,20 @@ class MediaLibraryNotifier extends StateNotifier { ? '扫描已停止,已保留 ${items.length} 个项目' : '扫描完成:${items.length} 个视频文件', ); + _appendScanLog( + _cancelScan + ? '扫描已停止,已保留 ${items.length} 个条目' + : '扫描完成,共入库 ${items.length} 个条目', + ); } catch (e) { state = state.copyWith(errorMessage: e.toString()); + _appendScanLog('扫描失败:$e', isError: true); } finally { state = state.copyWith( isScanning: false, progress: const MediaLibraryScanProgress(), ); + unawaited(_persistScanHistory()); } } @@ -389,8 +414,41 @@ class MediaLibraryNotifier extends StateNotifier { state = state.copyWith( progress: const MediaLibraryScanProgress(phase: '正在停止扫描'), ); + _appendScanLog('正在请求停止扫描…'); } + void _appendScanLog(String message, {bool isError = false}) { + final logs = [ + ...state.scanLogs, + MediaLibraryScanLog( + createdAt: DateTime.now(), + message: message, + isError: isError, + ), + ]; + if (logs.length > 120) logs.removeRange(0, logs.length - 120); + state = state.copyWith(scanLogs: logs); + } + + List _loadScanHistory() { + final raw = StorageManager.get(StorageKeys.mediaScanHistory); + if (raw is! List) return const []; + return raw + .whereType() + .map( + (entry) => + MediaLibraryScanLog.fromJson(Map.from(entry)), + ) + .where((entry) => entry.message.isNotEmpty) + .take(120) + .toList(); + } + + Future _persistScanHistory() => StorageManager.set( + StorageKeys.mediaScanHistory, + state.scanLogs.map((entry) => entry.toJson()).toList(), + ); + void setSearchQuery(String query) { state = state.copyWith(searchQuery: query); }