diff --git a/lib/core/data/services/watch_records_controller.dart b/lib/core/data/services/watch_records_controller.dart index 647d7fa..f3b819b 100644 --- a/lib/core/data/services/watch_records_controller.dart +++ b/lib/core/data/services/watch_records_controller.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import '../models/drama.dart'; import '../models/watch_progress.dart'; import '../models/watch_record.dart'; +import '../remote/api_client.dart'; import '../repositories/catalog_repository.dart'; import '../repositories/watch_history_repository.dart'; @@ -47,8 +48,18 @@ class WatchRecordsController extends ChangeNotifier { records = _uniqueRecords(recent); _notify(); await _hydrate(records.map((record) => record.dramaId).toList()); - } catch (_) { - if (!_disposed) error = 'progressError'; + } catch (failure, stackTrace) { + debugPrint( + '[WatchRecordsController] recent failed ' + '${failure.runtimeType}: $failure', + ); + debugPrintStack( + stackTrace: stackTrace, + label: '[WatchRecordsController] recent failed', + ); + if (!_disposed) { + error = failure is ApiException ? failure.message : 'progressError'; + } } finally { if (!_disposed) { loading = false; @@ -169,7 +180,15 @@ class WatchRecordsController extends ChangeNotifier { detailFailed: false, ), ); - } catch (_) { + } catch (failure, stackTrace) { + debugPrint( + '[WatchRecordsController] detail failed dramaId=$dramaId ' + '${failure.runtimeType}: $failure', + ); + debugPrintStack( + stackTrace: stackTrace, + label: '[WatchRecordsController] detail failed dramaId=$dramaId', + ); if (_disposed) return; final current = _recordIndex(dramaId); if (current < 0) return; diff --git a/lib/core/theme/app_colors.dart b/lib/core/theme/app_colors.dart index 6f09ce2..bd9aab7 100644 --- a/lib/core/theme/app_colors.dart +++ b/lib/core/theme/app_colors.dart @@ -17,4 +17,8 @@ abstract final class AppColors { static const bar = Color(0xFF101114); static const card = Color(0xFF17191F); static const muted = Color(0xFFB7BDC9); + static const outlineDark = Color(0xFF3A3C45); + static const outlineVariantDark = Color(0xFF242329); + static const outlineLight = Color(0xFF8A8E99); + static const outlineVariantLight = Color(0xFFD6D9E2); } diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index 0de77ba..b49620b 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -41,6 +41,8 @@ abstract final class AppTheme { onSecondaryContainer: AppColors.onPrimary, surface: _darkBackground, onSurface: Colors.white, + outline: AppColors.outlineDark, + outlineVariant: AppColors.outlineVariantDark, surfaceTint: AppColors.blueSoft, inversePrimary: AppColors.blueSoft, ) @@ -55,6 +57,8 @@ abstract final class AppTheme { onSecondaryContainer: AppColors.ink, surface: _lightBackground, onSurface: AppColors.ink, + outline: AppColors.outlineLight, + outlineVariant: AppColors.outlineVariantLight, surfaceTint: AppColors.blue, inversePrimary: AppColors.blueSoft, ); diff --git a/lib/pages/watch_records/watch_records_page.dart b/lib/pages/watch_records/watch_records_page.dart index 9401963..51c9f8f 100644 --- a/lib/pages/watch_records/watch_records_page.dart +++ b/lib/pages/watch_records/watch_records_page.dart @@ -371,6 +371,7 @@ class _FailureState extends StatelessWidget { ), const SizedBox(height: 14), FilledButton( + key: const Key('watch-records-retry'), onPressed: () => unawaited(onRetry()), child: Text(l10n.retry), ), diff --git a/test/home/home_nav_ui_test.dart b/test/home/home_nav_ui_test.dart index 4843dc3..156d863 100644 --- a/test/home/home_nav_ui_test.dart +++ b/test/home/home_nav_ui_test.dart @@ -55,6 +55,8 @@ void main() { final context = tester.element(barFinder); expect(decoration.color, AppColors.bar); expect(border.top.color, Theme.of(context).colorScheme.outlineVariant); + expect(border.top.color.computeLuminance(), lessThan(0.2)); + expect(border.top.color, isNot(Colors.white)); expect( tester .widget( diff --git a/test/theme/app_theme_test.dart b/test/theme/app_theme_test.dart index bc3229b..c556b6b 100644 --- a/test/theme/app_theme_test.dart +++ b/test/theme/app_theme_test.dart @@ -40,6 +40,18 @@ void main() { } }); + test('should_define_non_white_outline_tokens_in_both_themes', () { + final light = AppTheme.light().colorScheme; + final dark = AppTheme.dark().colorScheme; + + expect(light.outline, AppColors.outlineLight); + expect(light.outlineVariant, AppColors.outlineVariantLight); + expect(dark.outline, AppColors.outlineDark); + expect(dark.outlineVariant, AppColors.outlineVariantDark); + expect(dark.outlineVariant.computeLuminance(), lessThan(0.2)); + expect(light.outlineVariant.computeLuminance(), lessThan(0.92)); + }); + testWidgets('should_expose_klein_blue_through_widget_theme', (tester) async { late ThemeData applied; await tester.pumpWidget( diff --git a/test/watch/watch_records_controller_test.dart b/test/watch/watch_records_controller_test.dart index c3d787e..2fd55b2 100644 --- a/test/watch/watch_records_controller_test.dart +++ b/test/watch/watch_records_controller_test.dart @@ -8,6 +8,7 @@ import 'package:fruitshorts/core/data/models/home_feed.dart'; import 'package:fruitshorts/core/data/models/watch_progress.dart'; import 'package:fruitshorts/core/data/repositories/catalog_repository.dart'; import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart'; +import 'package:fruitshorts/core/data/remote/api_client.dart'; import 'package:fruitshorts/core/data/services/watch_records_controller.dart'; WatchProgress _progress( @@ -153,6 +154,19 @@ void main() { expect(controller.error, 'progressError'); expect(controller.records, isEmpty); }); + + test('should_preserve_backend_message_when_recent_request_fails', () async { + final history = ControlledWatchHistory([]) + ..recentError = const ApiException(100000, '账号不存在,请先注册'); + final controller = WatchRecordsController( + history: history, + catalog: ScriptedWatchCatalog(), + ); + + await controller.load(); + + expect(controller.error, '账号不存在,请先注册'); + }); } class ControlledWatchHistory implements WatchHistoryRepository { @@ -160,6 +174,7 @@ class ControlledWatchHistory implements WatchHistoryRepository { final List items; bool failRecent = false; + Object? recentError; bool failRemove = false; bool failClear = false; Completer? removeGate; @@ -178,6 +193,7 @@ class ControlledWatchHistory implements WatchHistoryRepository { @override Future> recent({int limit = 20}) async { + if (recentError != null) throw recentError!; if (failRecent) throw StateError('recent unavailable'); return items.take(limit).toList(); } diff --git a/test/watch/watch_records_page_test.dart b/test/watch/watch_records_page_test.dart index 4510ff0..37330b6 100644 --- a/test/watch/watch_records_page_test.dart +++ b/test/watch/watch_records_page_test.dart @@ -126,6 +126,23 @@ void main() { expect(find.text('No watch history yet.'), findsOneWidget); }, ); + + testWidgets('should_show_backend_message_when_recent_request_fails', ( + tester, + ) async { + final auth = await _auth(signedIn: true); + final history = _FailingRecentHistory(); + + await _pumpPage( + tester, + auth: auth, + history: history, + catalog: FakeCatalogRepository(), + ); + + expect(find.text('账号不存在,请先注册'), findsOneWidget); + expect(find.byKey(const Key('watch-records-retry')), findsOneWidget); + }); } WatchProgress _progress(String dramaId) => WatchProgress( @@ -202,6 +219,13 @@ class CountingWatchHistory extends InMemoryWatchHistoryRepository { } } +class _FailingRecentHistory extends InMemoryWatchHistoryRepository { + @override + Future> recent({int limit = 20}) async { + throw const ApiException(100000, '账号不存在,请先注册'); + } +} + class _FailingWatchCatalog implements CatalogRepository { final _delegate = FakeCatalogRepository();