From ae29a8209b26b1f8b8ffce51cd6d7ac8bb606242 Mon Sep 17 00:00:00 2001 From: qzl Date: Fri, 27 Mar 2026 19:07:39 +0800 Subject: [PATCH] =?UTF-8?q?refactor(apps):=20=E4=B8=BB=E9=A2=98=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E8=BF=81=E7=A7=BB=E8=87=B3=20ColorScheme=20+=20?= =?UTF-8?q?=E6=89=A9=E5=B1=95=E6=9E=B6=E6=9E=84=E5=B9=B6=E6=94=AF=E6=8C=81?= =?UTF-8?q?=20Dark=20Mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/AGENTS.md | 11 +- apps/lib/app/app.dart | 114 ++++++- apps/lib/app/di/injection.dart | 59 ++-- apps/lib/app/router/app_router.dart | 71 +++-- apps/lib/app/router/app_routes.dart | 5 +- .../router}/home_return_policy.dart | 2 +- .../app/services/auth_session_controller.dart | 20 ++ .../startup/auth_session_bootstrapper.dart | 38 --- apps/lib/core/auth/session_controller.dart | 3 + apps/lib/core/cache/cache_key.dart | 17 - .../core/cache/cache_refresh_coordinator.dart | 29 -- .../bloc => core/chat}/agent_stage.dart | 2 +- .../models => core/chat}/chat_list_item.dart | 0 apps/lib/core/chat/chat_orchestrator.dart | 31 ++ apps/lib/core/config/env.dart | 12 + apps/lib/core/constants/app_constants.dart | 13 - apps/lib/core/storage/app_preferences.dart | 97 ++++++ apps/lib/core/theme/app_theme.dart | 164 +++++++--- apps/lib/core/theme/design_tokens.dart | 296 ++++++++---------- .../lib/{core => data}/cache/cache_entry.dart | 0 .../cache/cache_invalidator.dart | 0 .../{core => data}/cache/cache_policy.dart | 0 .../lib/{core => data}/cache/cache_store.dart | 0 apps/lib/data/cache/cached_repository.dart | 107 +++++++ .../cache/hybrid_cache_store.dart | 0 .../cache/memory_cache_store.dart | 0 .../cache/persistent_cache_store.dart | 0 .../models/reminder_payload.dart | 0 .../models/user_profile.dart} | 8 +- .../calendar_event_repository.dart | 60 ++++ .../repositories/calendar_repository.dart | 98 ++++++ .../data/repositories/friend_repository.dart | 88 ++++++ .../data/repositories/inbox_repository.dart | 42 +++ .../repositories/models/calendar_event.dart | 40 +++ .../repositories/models/friend_request.dart | 63 ++++ .../repositories/models/inbox_message.dart | 74 +++++ .../models/schedule_item_model.dart | 65 ++-- .../repositories/models/user_summary.dart | 19 ++ .../data/repositories/user_repository.dart | 36 +++ apps/lib/data/services/calendar_service.dart | 112 +++++++ .../ios_notification_payload_bridge.dart | 20 ++ .../services/local_notification_service.dart | 9 +- .../reminder_notification_callbacks.dart | 86 +++-- .../screens/auth_boot_screen.dart | 3 +- .../presentation/screens/login_screen.dart | 29 +- .../auth/presentation/widgets/auth_field.dart | 18 +- .../widgets/auth_page_scaffold.dart | 81 ++--- .../presentation/widgets/password_field.dart | 5 +- .../features/calendar/data/calendar_api.dart | 3 +- .../data/services/calendar_repository.dart | 144 --------- .../data/services/calendar_service.dart | 92 ------ .../dayweek/day_event_layout_engine.dart | 2 +- .../screens/calendar_dayweek_screen.dart | 68 ++-- .../screens/calendar_event_create_screen.dart | 4 +- .../screens/calendar_event_detail_screen.dart | 81 ++--- .../screens/calendar_event_edit_screen.dart | 7 +- .../screens/calendar_event_share_screen.dart | 7 +- .../screens/calendar_month_screen.dart | 58 ++-- .../utils/event_color_resolver.dart | 9 +- .../widgets/calendar_share_dialog.dart | 16 +- .../widgets/create_event_sheet.dart | 111 ++++--- .../widgets/date_time_picker_sheet.dart | 50 +-- .../chat/presentation/bloc/chat_bloc.dart | 27 +- .../contacts/data/users/users_api.dart | 14 +- .../contacts/data/users/users_repository.dart | 8 +- .../data/users/users_repository_impl.dart | 8 +- .../screens/add_contact_screen.dart | 34 +- .../presentation/screens/contacts_screen.dart | 241 +++++++------- .../presentation/screens/home_screen.dart | 25 +- .../screens/home_screen_interactions.dart | 4 +- .../home/presentation/screens/home_sheet.dart | 25 +- .../widgets/home_attachment_strip.dart | 21 +- .../widgets/home_background_field.dart | 18 +- .../widgets/home_chat_item_renderer.dart | 88 ++++-- .../widgets/home_composer_stack.dart | 28 +- .../widgets/home_conversation_chrome.dart | 26 +- .../widgets/home_floating_header.dart | 28 +- .../widgets/home_recording_overlay.dart | 23 +- .../widgets/home_unread_badge.dart | 12 +- .../screens/message_invite_detail_screen.dart | 130 ++++---- .../screens/message_invite_list_screen.dart | 133 ++++---- .../widgets/calendar_message_card.dart | 46 +-- .../widgets/message_action_sheet.dart | 35 ++- .../ios_notification_payload_bridge.dart | 31 -- .../services/reminder_action_executor.dart | 20 +- .../services/reminder_queue_manager.dart | 2 +- .../widgets/reminder_overlay.dart | 75 +++-- .../data/services/settings_user_cache.dart | 30 -- .../user_profile_cache_repository.dart | 114 +++---- .../data/services/user_profile_service.dart | 53 ++++ .../screens/edit_profile_screen.dart | 70 +++-- .../presentation/screens/features_screen.dart | 39 ++- .../screens/job_detail_screen.dart | 113 ++++--- .../presentation/screens/memory_screen.dart | 116 ++++--- .../presentation/screens/settings_screen.dart | 206 ++++++------ .../screens/user_memory_detail_screen.dart | 128 +++++--- .../screens/user_memory_view_screen.dart | 86 +++-- .../screens/work_memory_detail_screen.dart | 112 ++++--- .../screens/work_memory_view_screen.dart | 78 +++-- .../widgets/account_section_card.dart | 22 +- .../widgets/settings_page_scaffold.dart | 4 +- .../features/todo/data/todo_repository.dart | 48 ++- .../screens/todo_detail_screen.dart | 66 ++-- .../screens/todo_edit_screen.dart | 111 ++++--- .../screens/todo_quadrants_screen.dart | 68 ++-- .../presentation/widgets/todo_drag_item.dart | 13 +- .../widgets/ui_schema_renderer.dart | 260 ++++++--------- apps/lib/main.dart | 11 +- .../state}/calendar_state_manager.dart | 0 apps/lib/shared/widgets/app_button.dart | 45 ++- apps/lib/shared/widgets/app_input.dart | 6 +- .../shared/widgets/app_loading_indicator.dart | 69 ++-- apps/lib/shared/widgets/app_pressable.dart | 17 +- .../widgets/app_pull_refresh_feedback.dart | 13 +- .../shared/widgets/app_selection_sheet.dart | 24 +- .../shared/widgets/app_sheet_input_field.dart | 17 +- .../lib/shared/widgets/app_toggle_switch.dart | 13 +- .../widgets/back_title_page_header.dart | 5 +- .../widgets/bottom_dock.dart | 48 +-- apps/lib/shared/widgets/chat_bubble.dart | 15 +- apps/lib/shared/widgets/confirm_sheet.dart | 27 +- .../widgets/destructive_action_sheet.dart | 23 +- .../widgets/detail_header_action_menu.dart | 51 +-- .../shared/widgets/error_retry_surface.dart | 4 +- .../widgets/fixed_length_code_input.dart | 23 +- apps/lib/shared/widgets/link_button.dart | 5 +- apps/lib/shared/widgets/message_composer.dart | 63 ++-- apps/lib/shared/widgets/page_header.dart | 47 ++- .../shared/widgets/phone_prefix_selector.dart | 20 +- apps/lib/shared/widgets/toast/toast.dart | 3 +- .../widgets/toast/toast_type_config.dart | 35 ++- .../app/router/app_router_redirect_test.dart | 105 +++++++ .../data/cache/cached_repository_test.dart | 65 ++++ .../shared_repositories_test.dart | 182 +++++++++++ .../user_profile_cache_repository_test.dart | 71 +++++ .../config/static/route/frontend_routes.yaml | 4 +- docs/bugs/2026-03-27-repository缓存抽象.md | 140 --------- docs/bugs/AppTheme硬编码颜色且缺失DarkMode.md | 90 ------ .../AuthSessionBootstrapper旧代码应删除.md | 43 --- docs/bugs/LinksyApp强制依赖ChatBloc.md | 49 --- docs/bugs/main与AuthBloc耦合.md | 118 ------- .../bugs/sharedpreferences缺少统一管理模型.md | 85 ----- docs/bugs/服务层与Repository层职责混乱.md | 123 -------- docs/bugs/根路径定义为登录页.md | 34 -- ...026-03-27-app-theme-dark-mode-migration.md | 126 ++++++++ ...-03-27-data-repositories-cache-strategy.md | 144 +++++++++ 146 files changed, 4301 insertions(+), 3200 deletions(-) rename apps/lib/{features/home/presentation/navigation => app/router}/home_return_policy.dart (95%) create mode 100644 apps/lib/app/services/auth_session_controller.dart delete mode 100644 apps/lib/app/startup/auth_session_bootstrapper.dart create mode 100644 apps/lib/core/auth/session_controller.dart delete mode 100644 apps/lib/core/cache/cache_key.dart delete mode 100644 apps/lib/core/cache/cache_refresh_coordinator.dart rename apps/lib/{features/chat/presentation/bloc => core/chat}/agent_stage.dart (93%) rename apps/lib/{features/chat/data/models => core/chat}/chat_list_item.dart (100%) create mode 100644 apps/lib/core/chat/chat_orchestrator.dart delete mode 100644 apps/lib/core/constants/app_constants.dart create mode 100644 apps/lib/core/storage/app_preferences.dart rename apps/lib/{core => data}/cache/cache_entry.dart (100%) rename apps/lib/{core => data}/cache/cache_invalidator.dart (100%) rename apps/lib/{core => data}/cache/cache_policy.dart (100%) rename apps/lib/{core => data}/cache/cache_store.dart (100%) create mode 100644 apps/lib/data/cache/cached_repository.dart rename apps/lib/{core => data}/cache/hybrid_cache_store.dart (100%) rename apps/lib/{core => data}/cache/memory_cache_store.dart (100%) rename apps/lib/{core => data}/cache/persistent_cache_store.dart (100%) rename apps/lib/{features/notification/domain => data}/models/reminder_payload.dart (100%) rename apps/lib/{features/contacts/data/users/models/user_response.dart => data/models/user_profile.dart} (86%) create mode 100644 apps/lib/data/repositories/calendar_event_repository.dart create mode 100644 apps/lib/data/repositories/calendar_repository.dart create mode 100644 apps/lib/data/repositories/friend_repository.dart create mode 100644 apps/lib/data/repositories/inbox_repository.dart create mode 100644 apps/lib/data/repositories/models/calendar_event.dart create mode 100644 apps/lib/data/repositories/models/friend_request.dart create mode 100644 apps/lib/data/repositories/models/inbox_message.dart rename apps/lib/{features/calendar/data => data/repositories}/models/schedule_item_model.dart (82%) create mode 100644 apps/lib/data/repositories/models/user_summary.dart create mode 100644 apps/lib/data/repositories/user_repository.dart create mode 100644 apps/lib/data/services/calendar_service.dart create mode 100644 apps/lib/data/services/ios_notification_payload_bridge.dart rename apps/lib/{features/notification => }/data/services/local_notification_service.dart (97%) rename apps/lib/{features/notification => }/data/services/reminder_notification_callbacks.dart (59%) delete mode 100644 apps/lib/features/calendar/data/services/calendar_repository.dart delete mode 100644 apps/lib/features/calendar/data/services/calendar_service.dart delete mode 100644 apps/lib/features/notification/data/services/ios_notification_payload_bridge.dart delete mode 100644 apps/lib/features/settings/data/services/settings_user_cache.dart create mode 100644 apps/lib/features/settings/data/services/user_profile_service.dart rename apps/lib/{features/calendar/presentation => shared/state}/calendar_state_manager.dart (100%) rename apps/lib/{features/calendar/presentation => shared}/widgets/bottom_dock.dart (68%) create mode 100644 apps/test/app/router/app_router_redirect_test.dart create mode 100644 apps/test/data/cache/cached_repository_test.dart create mode 100644 apps/test/data/repositories/shared_repositories_test.dart create mode 100644 apps/test/features/settings/data/services/user_profile_cache_repository_test.dart delete mode 100644 docs/bugs/2026-03-27-repository缓存抽象.md delete mode 100644 docs/bugs/AppTheme硬编码颜色且缺失DarkMode.md delete mode 100644 docs/bugs/AuthSessionBootstrapper旧代码应删除.md delete mode 100644 docs/bugs/LinksyApp强制依赖ChatBloc.md delete mode 100644 docs/bugs/main与AuthBloc耦合.md delete mode 100644 docs/bugs/sharedpreferences缺少统一管理模型.md delete mode 100644 docs/bugs/服务层与Repository层职责混乱.md delete mode 100644 docs/bugs/根路径定义为登录页.md create mode 100644 docs/plans/2026-03-27-app-theme-dark-mode-migration.md create mode 100644 docs/plans/2026-03-27-data-repositories-cache-strategy.md diff --git a/apps/AGENTS.md b/apps/AGENTS.md index 0fbffe7..1deeabd 100644 --- a/apps/AGENTS.md +++ b/apps/AGENTS.md @@ -16,9 +16,11 @@ This file governs `apps/**` (Flutter). Keep rules strict, short, and reusable. ## UI Design System (Must) -- Use design tokens only from `apps/lib/core/theme/design_tokens.dart` (`AppColors`, `AppSpacing`, `AppRadius`). -- No hardcoded visual values. -- If semantics are missing, add token definitions first. +- **Semantic colors**: always use `Theme.of(context).colorScheme.*` (primary, surface, error, etc.). Never hardcode hex or `Colors.*`. +- **Brand palette colors** (event presets, avatar colors, Eisenhower matrix quadrants): use `Theme.of(context).extension()!.*`. +- **Spacing / Radius**: use `AppSpacing` / `AppRadius` from `design_tokens.dart`. No hardcoded values. +- `AppTheme.light` / `AppTheme.dark` provide complete `ColorScheme` (light + dark). `MaterialApp` wires them via `theme:` / `darkTheme:`. +- If a semantic slot is missing from `ColorScheme`, add it to `AppTheme` — do not bypass `colorScheme` with hardcoded values. ## Reuse & Composition (Must) @@ -63,6 +65,9 @@ This file governs `apps/**` (Flutter). Keep rules strict, short, and reusable. - Reads/writes that affect consistency must go through repository layer. - Cache keys and invalidation policy belong to repository, not UI/Bloc. +- Shared cache infrastructure must live under `apps/lib/data/cache/`; feature modules must not duplicate low-level cache store logic. +- Cross-feature data access must go through `apps/lib/data/repositories/`; do not import another feature's data implementation directly from UI/Bloc. +- Repository instances should be resolved from DI singletons to reuse cache and avoid per-feature re-creation. ## Testing Policy diff --git a/apps/lib/app/app.dart b/apps/lib/app/app.dart index 01fcd17..b13e4ec 100644 --- a/apps/lib/app/app.dart +++ b/apps/lib/app/app.dart @@ -1,37 +1,72 @@ +import 'dart:async'; +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; import 'di/injection.dart'; +import '../data/models/reminder_payload.dart'; +import '../data/services/calendar_service.dart'; +import '../data/services/local_notification_service.dart'; +import '../data/services/reminder_notification_callbacks.dart'; import '../core/l10n/l10n.dart'; -import '../core/network/i_api_client.dart'; import '../l10n/app_localizations.dart'; import '../features/auth/presentation/bloc/auth_bloc.dart'; +import '../features/auth/presentation/bloc/auth_event.dart'; import '../features/auth/presentation/bloc/auth_state.dart'; -import '../features/chat/presentation/bloc/chat_bloc.dart'; +import '../features/notification/domain/models/reminder_action.dart'; +import '../features/notification/domain/services/reminder_action_executor.dart'; import 'router/app_router.dart'; import '../core/theme/app_theme.dart'; -class LinksyApp extends StatelessWidget { - final AuthBloc authBloc; +class LinksyApp extends StatefulWidget { + const LinksyApp({super.key}); - const LinksyApp({super.key, required this.authBloc}); + @override + State createState() => _LinksyAppState(); +} + +class _LinksyAppState extends State { + late final AuthBloc _authBloc; + late final GoRouter _router; + String? _reminderBootstrapUserId; + + @override + void initState() { + super.initState(); + _authBloc = sl(); + _authBloc.add(AuthStarted()); + _router = createAppRouter(_authBloc); + unawaited(_bindNotificationResponseHandler()); + } + + @override + void dispose() { + _router.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider.value(value: authBloc), - BlocProvider( - create: (_) => ChatBloc(apiClient: sl()), - ), - ], + return BlocProvider.value( + value: _authBloc, child: BlocListener( listener: (context, state) { - // Handle auth state changes if needed + if (state is AuthAuthenticated && + state.user.id != _reminderBootstrapUserId) { + _reminderBootstrapUserId = state.user.id; + unawaited(_rebuildUpcomingReminders()); + } + if (state is AuthUnauthenticated) { + _reminderBootstrapUserId = null; + } }, child: MaterialApp.router( onGenerateTitle: (context) => AppLocalizations.of(context).appTitle, debugShowCheckedModeBanner: false, theme: AppTheme.light, + darkTheme: AppTheme.dark, + themeMode: ThemeMode.system, locale: const Locale('zh'), supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: AppLocalizations.localizationsDelegates, @@ -39,9 +74,60 @@ class LinksyApp extends StatelessWidget { L10n.setLocale(Localizations.localeOf(context)); return child ?? const SizedBox.shrink(); }, - routerConfig: createAppRouter(authBloc), + routerConfig: _router, ), ), ); } + + Future _rebuildUpcomingReminders() async { + final now = DateTime.now(); + final start = now.subtract(const Duration(days: 90)); + final end = now.add(const Duration(days: 90)); + try { + final events = await sl().getEventsForRange(start, end); + await sl().rebuildUpcomingReminders(events); + } catch (error) { + debugPrint('reminder bootstrap skipped: $error'); + } + } + + Future _bindNotificationResponseHandler() async { + await ReminderNotificationCallbacks.bindResponseHandler((response) async { + final payloadRaw = response.payload; + if (payloadRaw == null || payloadRaw.isEmpty) { + return; + } + + ReminderPayload payload; + try { + payload = ReminderPayload.fromJson( + Map.from(jsonDecode(payloadRaw) as Map), + ); + } catch (_) { + return; + } + + final actionId = response.actionId; + ReminderAction? action; + if (actionId != null) { + try { + action = ReminderAction.fromValue(actionId); + } catch (_) { + action = null; + } + } + if (action == null) { + ReminderNotificationCallbacks.onNotificationPayloadReceived?.call( + payload, + ); + return; + } + + await sl().handleAction( + action: action, + payload: payload, + ); + }); + } } diff --git a/apps/lib/app/di/injection.dart b/apps/lib/app/di/injection.dart index 77bb77b..4891628 100644 --- a/apps/lib/app/di/injection.dart +++ b/apps/lib/app/di/injection.dart @@ -2,35 +2,43 @@ import 'package:dio/dio.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:get_it/get_it.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import '../../core/cache/cache_invalidator.dart'; -import '../../core/cache/hybrid_cache_store.dart'; -import '../../core/cache/memory_cache_store.dart'; -import '../../core/cache/persistent_cache_store.dart'; +import '../../data/cache/cache_invalidator.dart'; +import '../../data/cache/hybrid_cache_store.dart'; +import '../../data/cache/memory_cache_store.dart'; +import '../../data/cache/persistent_cache_store.dart'; +import '../../data/repositories/calendar_event_repository.dart'; +import '../../data/repositories/calendar_repository.dart'; +import '../../data/repositories/friend_repository.dart'; +import '../../data/repositories/inbox_repository.dart'; +import '../../data/repositories/user_repository.dart'; +import '../../core/auth/session_controller.dart'; import '../../core/network/api_client.dart'; import '../../core/network/i_api_client.dart'; +import '../../core/storage/app_preferences.dart'; import '../../core/storage/token_storage.dart'; import '../../core/config/env.dart'; -import '../../features/notification/data/services/local_notification_service.dart'; +import '../../data/services/local_notification_service.dart'; import '../../features/auth/data/auth_api.dart'; import '../../features/auth/data/auth_repository.dart'; import '../../features/auth/data/auth_repository_impl.dart'; import '../../features/auth/presentation/bloc/auth_bloc.dart'; import '../../features/auth/presentation/bloc/auth_event.dart'; +import '../../features/chat/presentation/bloc/chat_bloc.dart'; import '../../features/calendar/data/calendar_api.dart'; -import '../../features/calendar/data/services/calendar_repository.dart'; -import '../../features/calendar/data/services/calendar_service.dart'; +import '../../data/services/calendar_service.dart'; import '../../features/notification/domain/services/reminder_action_executor.dart'; -import '../../features/calendar/presentation/calendar_state_manager.dart'; +import '../../shared/state/calendar_state_manager.dart'; import '../../features/contacts/data/friends_api.dart'; import '../../features/messages/data/inbox_api.dart'; import '../../features/settings/data/settings_api.dart'; import '../../features/settings/data/services/automation_jobs_api.dart'; -import '../../features/settings/data/services/settings_user_cache.dart'; import '../../features/settings/data/services/user_profile_cache_repository.dart'; +import '../../features/settings/data/services/user_profile_service.dart'; import '../../features/settings/data/services/memory_service.dart'; import '../../features/contacts/data/users/users_api.dart'; import '../../features/todo/data/todo_api.dart'; import '../../features/todo/data/todo_repository.dart'; +import '../services/auth_session_controller.dart'; final sl = GetIt.instance; @@ -63,6 +71,7 @@ Future configureDependencies() async { final sharedPreferences = await SharedPreferences.getInstance(); sl.registerSingleton(sharedPreferences); + sl.registerSingleton(AppPreferences(sharedPreferences)); final memoryCacheStore = MemoryCacheStore(); final persistentCacheStore = PersistentCacheStore(); @@ -79,23 +88,31 @@ Future configureDependencies() async { final usersApi = UsersApi(apiClient); sl.registerSingleton(usersApi); + sl.registerSingleton(UserRepositoryImpl(apiClient)); + final userProfileService = UserProfileService(apiClient); + sl.registerSingleton(userProfileService); final userProfileCacheRepository = UserProfileCacheRepository( store: hybridCacheStore, - remoteLoader: usersApi.getMe, + remoteLoader: userProfileService.getMe, ); sl.registerSingleton(userProfileCacheRepository); final calendarApi = CalendarApi(apiClient); sl.registerSingleton(calendarApi); + sl.registerSingleton( + CalendarEventRepositoryImpl(apiClient), + ); - final calendarService = CalendarService(apiClient: apiClient); + final calendarService = CalendarService( + apiClient: apiClient, + invalidator: sl(), + ); sl.registerSingleton(calendarService); final calendarRepository = CalendarRepository( store: hybridCacheStore, - loadDayFromRemote: calendarService.getEventsForDay, - loadMonthFromRemote: calendarService.getEventsForRange, + apiClient: apiClient, ); sl.registerSingleton(calendarRepository); @@ -109,6 +126,7 @@ Future configureDependencies() async { final friendsApi = FriendsApi(apiClient); sl.registerSingleton(friendsApi); + sl.registerSingleton(FriendRepositoryImpl(apiClient)); final settingsApi = SettingsApi(apiClient); sl.registerSingleton(settingsApi); @@ -119,12 +137,9 @@ Future configureDependencies() async { final memoryService = MemoryService(apiClient); sl.registerSingleton(memoryService); - sl.registerSingleton( - SettingsUserCache(userProfileCacheRepository), - ); - final inboxApi = InboxApi(apiClient); sl.registerSingleton(inboxApi); + sl.registerSingleton(InboxRepositoryImpl(apiClient)); final todoApi = TodoApi(apiClient); sl.registerSingleton(todoApi); @@ -141,8 +156,8 @@ Future configureDependencies() async { tokenStorage: tokenStorage, onLogout: () async { apiClient.resetInterceptor(); - if (sl.isRegistered()) { - sl().invalidate(); + if (sl.isRegistered()) { + await sl().invalidate(); } }, ); @@ -150,6 +165,8 @@ Future configureDependencies() async { final authBloc = AuthBloc(authRepository); sl.registerSingleton(authBloc); + sl.registerSingleton(AuthSessionController(authBloc)); + sl.registerSingleton(ChatBloc(apiClient: apiClient)); apiClient.setRefreshCallback((token) async { try { @@ -161,8 +178,8 @@ Future configureDependencies() async { }); apiClient.setAuthFailureCallback(() async { - if (sl.isRegistered()) { - sl().invalidate(); + if (sl.isRegistered()) { + await sl().invalidate(); } authBloc.add( const AuthSessionInvalidated( diff --git a/apps/lib/app/router/app_router.dart b/apps/lib/app/router/app_router.dart index e8025b5..23ccccd 100644 --- a/apps/lib/app/router/app_router.dart +++ b/apps/lib/app/router/app_router.dart @@ -1,7 +1,11 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'app_route_observer.dart'; +import '../di/injection.dart'; import '../../../features/auth/presentation/bloc/auth_bloc.dart'; import '../../../features/auth/presentation/bloc/auth_state.dart'; +import '../../../features/chat/presentation/bloc/chat_bloc.dart'; import 'app_routes.dart'; import 'go_router_refresh_stream.dart'; import '../../../features/auth/presentation/screens/auth_boot_screen.dart'; @@ -32,7 +36,6 @@ import '../../../features/settings/presentation/screens/work_memory_detail_scree import '../../../features/settings/presentation/screens/edit_profile_screen.dart'; final _homeSecondLevelRoutes = [ - AppRoutes.shellHomeBranch, AppRoutes.shellCalendarBranch, AppRoutes.calendarMonth, AppRoutes.shellTodoBranch, @@ -54,37 +57,53 @@ final _protectedRoutes = [ AppRoutes.messageInviteList, ]; +String? resolveAuthRedirect({ + required AuthState authState, + required String matchedLocation, +}) { + final isAuthenticated = authState is AuthAuthenticated; + final isAuthChecking = authState is AuthInitial || authState is AuthLoading; + final isBootRoute = matchedLocation == AppRoutes.authBoot; + final isAuthRoute = + matchedLocation == AppRoutes.authLogin || + matchedLocation.startsWith('/login'); + final isHomeRoute = matchedLocation == AppRoutes.homeMain; + final isProtected = + isHomeRoute || + _protectedRoutes.any((route) => matchedLocation.startsWith(route)); + + if (isAuthChecking && !isBootRoute) { + return AppRoutes.authBoot; + } + if (!isAuthChecking && isBootRoute) { + return isAuthenticated ? AppRoutes.homeMain : AppRoutes.authLogin; + } + if (!isAuthenticated && isProtected) { + return AppRoutes.authLogin; + } + if (isAuthenticated && isAuthRoute) { + return AppRoutes.homeMain; + } + return null; +} + +Widget buildHomeRouteScreen() { + return BlocProvider.value( + value: sl(), + child: const HomeScreen(), + ); +} + GoRouter createAppRouter(AuthBloc authBloc) { return GoRouter( initialLocation: AppRoutes.authBoot, observers: [appRouteObserver], refreshListenable: GoRouterRefreshStream(authBloc.stream), redirect: (context, state) { - final authState = authBloc.state; - final isAuthenticated = authState is AuthAuthenticated; - final isAuthChecking = - authState is AuthInitial || authState is AuthLoading; - final isBootRoute = state.matchedLocation == AppRoutes.authBoot; - final isAuthRoute = - state.matchedLocation == AppRoutes.authLogin || - state.matchedLocation.startsWith('/login'); - final isProtected = _protectedRoutes.any( - (route) => state.matchedLocation.startsWith(route), + return resolveAuthRedirect( + authState: authBloc.state, + matchedLocation: state.matchedLocation, ); - - if (isAuthChecking && !isBootRoute) { - return AppRoutes.authBoot; - } - if (!isAuthChecking && isBootRoute) { - return isAuthenticated ? AppRoutes.homeMain : AppRoutes.authLogin; - } - if (!isAuthenticated && isProtected) { - return AppRoutes.authLogin; - } - if (isAuthenticated && isAuthRoute) { - return AppRoutes.homeMain; - } - return null; }, routes: [ GoRoute( @@ -118,7 +137,7 @@ GoRouter createAppRouter(AuthBloc authBloc) { ), GoRoute( path: AppRoutes.homeMain, - builder: (context, state) => const HomeScreen(), + builder: (context, state) => buildHomeRouteScreen(), ), GoRoute( path: AppRoutes.messageInviteList, diff --git a/apps/lib/app/router/app_routes.dart b/apps/lib/app/router/app_routes.dart index b676581..3fe265c 100644 --- a/apps/lib/app/router/app_routes.dart +++ b/apps/lib/app/router/app_routes.dart @@ -2,10 +2,9 @@ class AppRoutes { AppRoutes._(); static const authBoot = '/boot'; - static const authLogin = '/'; + static const authLogin = '/login'; - static const homeMain = '/home'; - static const shellHomeBranch = homeMain; + static const homeMain = '/'; static const shellCalendarBranch = calendarDayWeek; static const shellTodoBranch = todoList; diff --git a/apps/lib/features/home/presentation/navigation/home_return_policy.dart b/apps/lib/app/router/home_return_policy.dart similarity index 95% rename from apps/lib/features/home/presentation/navigation/home_return_policy.dart rename to apps/lib/app/router/home_return_policy.dart index 75737ca..2e823af 100644 --- a/apps/lib/features/home/presentation/navigation/home_return_policy.dart +++ b/apps/lib/app/router/home_return_policy.dart @@ -1,7 +1,7 @@ import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; -import '../../../../app/router/app_routes.dart'; +import 'app_routes.dart'; enum HomeReturnAction { pop, goHome, goHomeForDock } diff --git a/apps/lib/app/services/auth_session_controller.dart b/apps/lib/app/services/auth_session_controller.dart new file mode 100644 index 0000000..68d843d --- /dev/null +++ b/apps/lib/app/services/auth_session_controller.dart @@ -0,0 +1,20 @@ +import 'dart:async'; + +import 'package:social_app/core/auth/session_controller.dart'; +import 'package:social_app/features/auth/presentation/bloc/auth_bloc.dart'; +import 'package:social_app/features/auth/presentation/bloc/auth_event.dart'; +import 'package:social_app/features/auth/presentation/bloc/auth_state.dart'; + +class AuthSessionController implements SessionController { + final AuthBloc _authBloc; + + AuthSessionController(this._authBloc); + + @override + Future logoutAndWaitUnauthenticated() async { + _authBloc.add(AuthLoggedOut()); + await _authBloc.stream + .firstWhere((state) => state is AuthUnauthenticated) + .timeout(const Duration(seconds: 5)); + } +} diff --git a/apps/lib/app/startup/auth_session_bootstrapper.dart b/apps/lib/app/startup/auth_session_bootstrapper.dart deleted file mode 100644 index bd8248a..0000000 --- a/apps/lib/app/startup/auth_session_bootstrapper.dart +++ /dev/null @@ -1,38 +0,0 @@ -import '../../features/auth/presentation/bloc/auth_state.dart'; -import '../../features/calendar/data/services/calendar_service.dart'; -import '../../features/notification/data/services/local_notification_service.dart'; - -class AuthSessionBootstrapper { - AuthSessionBootstrapper({ - required CalendarService calendarService, - required LocalNotificationService notificationService, - }) : _calendarService = calendarService, - _notificationService = notificationService; - - final CalendarService _calendarService; - final LocalNotificationService _notificationService; - - String? _syncedUserId; - - Future syncForAuthState(AuthState state) async { - if (state is! AuthAuthenticated) { - _syncedUserId = null; - return; - } - - if (_syncedUserId == state.user.id) { - return; - } - - try { - final now = DateTime.now(); - final start = now.subtract(const Duration(days: 90)); - final end = now.add(const Duration(days: 90)); - final events = await _calendarService.getEventsForRange(start, end); - await _notificationService.rebuildUpcomingReminders(events); - _syncedUserId = state.user.id; - } catch (_) { - // ignore reminder bootstrap failures - } - } -} diff --git a/apps/lib/core/auth/session_controller.dart b/apps/lib/core/auth/session_controller.dart new file mode 100644 index 0000000..b897021 --- /dev/null +++ b/apps/lib/core/auth/session_controller.dart @@ -0,0 +1,3 @@ +abstract class SessionController { + Future logoutAndWaitUnauthenticated(); +} diff --git a/apps/lib/core/cache/cache_key.dart b/apps/lib/core/cache/cache_key.dart deleted file mode 100644 index b5f75b6..0000000 --- a/apps/lib/core/cache/cache_key.dart +++ /dev/null @@ -1,17 +0,0 @@ -class CacheKey { - final String value; - - const CacheKey(this.value); - - @override - String toString() => value; - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other is CacheKey && other.value == value); - } - - @override - int get hashCode => value.hashCode; -} diff --git a/apps/lib/core/cache/cache_refresh_coordinator.dart b/apps/lib/core/cache/cache_refresh_coordinator.dart deleted file mode 100644 index 97c781d..0000000 --- a/apps/lib/core/cache/cache_refresh_coordinator.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter/widgets.dart'; - -class CacheRefreshCoordinator with WidgetsBindingObserver { - final Duration minInterval; - final void Function() onRefresh; - final DateTime Function() now; - - DateTime? _lastRefreshedAt; - - CacheRefreshCoordinator({ - required this.minInterval, - required this.onRefresh, - DateTime Function()? now, - }) : now = now ?? DateTime.now; - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (state != AppLifecycleState.resumed) { - return; - } - final current = now(); - final last = _lastRefreshedAt; - if (last != null && current.difference(last) < minInterval) { - return; - } - _lastRefreshedAt = current; - onRefresh(); - } -} diff --git a/apps/lib/features/chat/presentation/bloc/agent_stage.dart b/apps/lib/core/chat/agent_stage.dart similarity index 93% rename from apps/lib/features/chat/presentation/bloc/agent_stage.dart rename to apps/lib/core/chat/agent_stage.dart index 5cbfd3c..0f65f6d 100644 --- a/apps/lib/features/chat/presentation/bloc/agent_stage.dart +++ b/apps/lib/core/chat/agent_stage.dart @@ -1,4 +1,4 @@ -import '../../../../core/l10n/l10n.dart'; +import '../l10n/l10n.dart'; enum AgentStage { routing, execution, memory } diff --git a/apps/lib/features/chat/data/models/chat_list_item.dart b/apps/lib/core/chat/chat_list_item.dart similarity index 100% rename from apps/lib/features/chat/data/models/chat_list_item.dart rename to apps/lib/core/chat/chat_list_item.dart diff --git a/apps/lib/core/chat/chat_orchestrator.dart b/apps/lib/core/chat/chat_orchestrator.dart new file mode 100644 index 0000000..cc6483f --- /dev/null +++ b/apps/lib/core/chat/chat_orchestrator.dart @@ -0,0 +1,31 @@ +import 'package:image_picker/image_picker.dart'; + +import 'agent_stage.dart'; +import 'chat_list_item.dart'; + +abstract class ChatOrchestratorState { + List get items; + bool get isSending; + bool get isWaitingFirstToken; + bool get isStreaming; + bool get isCancelling; + bool get isLoadingHistory; + String? get currentMessageId; + String? get error; + DateTime? get oldestLoadedDate; + bool get hasEarlierHistory; + AgentStage? get currentStage; + bool get isLoading; +} + +abstract class ChatOrchestrator { + ChatOrchestratorState get state; + Stream get stream; + + Future sendMessage(String content, {List? images}); + Future loadHistory(); + Future loadMoreHistory(); + Future transcribeAudioFile(String filePath); + Future cancelCurrentRun(); + void clearError(); +} diff --git a/apps/lib/core/config/env.dart b/apps/lib/core/config/env.dart index ee2a734..c88424a 100644 --- a/apps/lib/core/config/env.dart +++ b/apps/lib/core/config/env.dart @@ -1,5 +1,7 @@ import 'dart:io'; +import 'package:package_info_plus/package_info_plus.dart'; + class Env { static String get apiUrl { final backendUrl = const String.fromEnvironment('BACKEND_URL'); @@ -11,4 +13,14 @@ class Env { } return 'http://localhost:5775'; } + + static String version = '0.1.0'; + static int build = 1; + + static Future init() async { + final info = await PackageInfo.fromPlatform(); + version = info.version; + final buildStr = info.buildNumber.isEmpty ? '1' : info.buildNumber; + build = int.tryParse(buildStr) ?? 1; + } } diff --git a/apps/lib/core/constants/app_constants.dart b/apps/lib/core/constants/app_constants.dart deleted file mode 100644 index 4997721..0000000 --- a/apps/lib/core/constants/app_constants.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:package_info_plus/package_info_plus.dart'; - -class AppConstants { - static String version = '0.1.0'; - static int build = 1; - - static Future init() async { - final info = await PackageInfo.fromPlatform(); - version = info.version; - final buildStr = info.buildNumber.isEmpty ? '1' : info.buildNumber; - build = int.tryParse(buildStr) ?? 1; - } -} diff --git a/apps/lib/core/storage/app_preferences.dart b/apps/lib/core/storage/app_preferences.dart new file mode 100644 index 0000000..b891d6f --- /dev/null +++ b/apps/lib/core/storage/app_preferences.dart @@ -0,0 +1,97 @@ +import 'dart:convert'; + +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../data/models/reminder_payload.dart'; + +class AppPreferences { + static const String _pendingNotificationsKey = + 'calendar_reminder_pending_notification_responses_v1'; + static const String _pendingNotificationPayloadKey = + 'pending_notification_payload'; + + final SharedPreferences _prefs; + + AppPreferences(this._prefs); + + List get pendingNotifications { + final list = _prefs.getStringList(_pendingNotificationsKey) ?? []; + return list + .map(_decodePendingNotification) + .whereType() + .toList(); + } + + Future setPendingNotifications( + List value, + ) { + return _prefs.setStringList( + _pendingNotificationsKey, + value.map(_encodePendingNotification).toList(), + ); + } + + Future clearPendingNotifications() { + return _prefs.remove(_pendingNotificationsKey); + } + + ReminderPayload? get pendingNotificationPayload { + final raw = _prefs.getString(_pendingNotificationPayloadKey); + if (raw == null || raw.isEmpty) { + return null; + } + try { + final json = Map.from(jsonDecode(raw) as Map); + return ReminderPayload.fromJson(json); + } catch (_) { + return null; + } + } + + Future setPendingNotificationPayload(ReminderPayload payload) { + return _prefs.setString( + _pendingNotificationPayloadKey, + jsonEncode(payload.toJson()), + ); + } + + Future clearPendingNotificationPayload() { + return _prefs.remove(_pendingNotificationPayloadKey); + } + + static String _encodePendingNotification( + PendingNotificationResponse response, + ) { + return jsonEncode({ + 'id': response.id, + 'actionId': response.actionId, + 'payload': response.payload, + 'type': response.notificationResponseType.index, + 'input': response.input, + }); + } + + static PendingNotificationResponse? _decodePendingNotification(String raw) { + try { + final parsed = Map.from(jsonDecode(raw) as Map); + final id = parsed['id'] as int?; + final actionId = parsed['actionId'] as String?; + final payload = parsed['payload'] as String?; + final typeIndex = (parsed['type'] as int?) ?? 0; + final input = parsed['input'] as String?; + final type = NotificationResponseType.values[typeIndex.clamp(0, 1)]; + return NotificationResponse( + id: id, + actionId: actionId, + payload: payload, + input: input, + notificationResponseType: type, + ); + } catch (_) { + return null; + } + } +} + +typedef PendingNotificationResponse = NotificationResponse; diff --git a/apps/lib/core/theme/app_theme.dart b/apps/lib/core/theme/app_theme.dart index f1da0cb..e958de4 100644 --- a/apps/lib/core/theme/app_theme.dart +++ b/apps/lib/core/theme/app_theme.dart @@ -4,50 +4,130 @@ import 'design_tokens.dart'; class AppTheme { AppTheme._(); - static ThemeData get light => ThemeData( - useMaterial3: true, - brightness: Brightness.light, - scaffoldBackgroundColor: AppColors.background, - colorScheme: ColorScheme.fromSeed( - seedColor: AppColors.primary, - brightness: Brightness.light, - ), - fontFamily: 'Inter', - appBarTheme: const AppBarTheme( - backgroundColor: AppColors.background, - foregroundColor: AppColors.slate900, - elevation: 0, - ), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primary, - foregroundColor: AppColors.primaryForeground, + static ThemeData get light => _buildTheme(Brightness.light); + static ThemeData get dark => _buildTheme(Brightness.dark); + + static ThemeData _buildTheme(Brightness brightness) { + final ColorScheme colorScheme; + if (brightness == Brightness.dark) { + colorScheme = ColorScheme( + brightness: Brightness.dark, + primary: const Color(0xFF60A5FA), + onPrimary: const Color(0xFFFFFFFF), + primaryContainer: const Color(0xFF1E3A5F), + onPrimaryContainer: const Color(0xFFBFDBFE), + secondary: const Color(0xFF93C5FD), + onSecondary: const Color(0xFF0F172A), + secondaryContainer: const Color(0xFF1E293B), + onSecondaryContainer: const Color(0xFFBFDBFE), + tertiary: const Color(0xFFC4B5FD), + onTertiary: const Color(0xFF0F172A), + tertiaryContainer: const Color(0xFF4C1D95), + onTertiaryContainer: const Color(0xFFE9D5FF), + error: const Color(0xFFD14343), + onError: const Color(0xFFFFFFFF), + errorContainer: const Color(0xFF7F1D1D), + onErrorContainer: const Color(0xFFFEE2E2), + surface: const Color(0xFF0F172A), + onSurface: const Color(0xFFE2E8F0), + surfaceContainerLowest: const Color(0xFF0F172A), + surfaceContainerLow: const Color(0xFF1E293B), + surfaceContainer: const Color(0xFF334155), + surfaceContainerHigh: const Color(0xFF475569), + surfaceContainerHighest: const Color(0xFF64748B), + onSurfaceVariant: const Color(0xFF94A3B8), + outline: const Color(0xFF64748B), + outlineVariant: const Color(0xFF475569), + shadow: Colors.black, + scrim: Colors.black, + inverseSurface: const Color(0xFFE2E8F0), + onInverseSurface: const Color(0xFF1E293B), + inversePrimary: const Color(0xFF2563EB), + ); + } else { + colorScheme = ColorScheme( + brightness: Brightness.light, + primary: const Color(0xFF3B82F6), + onPrimary: const Color(0xFFFAFAFA), + primaryContainer: const Color(0xFFDBEAFE), + onPrimaryContainer: const Color(0xFF0F172A), + secondary: const Color(0xFF2563EB), + onSecondary: const Color(0xFFFFFFFF), + secondaryContainer: const Color(0xFFDBEAFE), + onSecondaryContainer: const Color(0xFF1E293B), + tertiary: const Color(0xFF8B5CF6), + onTertiary: const Color(0xFFFFFFFF), + tertiaryContainer: const Color(0xFFF5F3FF), + onTertiaryContainer: const Color(0xFF0F172A), + error: const Color(0xFFEF4444), + onError: const Color(0xFFFFFFFF), + errorContainer: const Color(0xFFFEE2E2), + onErrorContainer: const Color(0xFF7F1D1D), + surface: const Color(0xFFFFFFFF), + onSurface: const Color(0xFF0F172A), + surfaceContainerLowest: const Color(0xFFFFFFFF), + surfaceContainerLow: const Color(0xFFF8FAFC), + surfaceContainer: const Color(0xFFF1F5F9), + surfaceContainerHigh: const Color(0xFFE2E8F0), + surfaceContainerHighest: const Color(0xFFCBD5E1), + onSurfaceVariant: const Color(0xFF475569), + outline: const Color(0xFF94A3B8), + outlineVariant: const Color(0xFFCBD5E1), + shadow: const Color(0xFF0F172A), + scrim: const Color(0xFF0F172A), + inverseSurface: const Color(0xFF1E293B), + onInverseSurface: const Color(0xFFF1F5F9), + inversePrimary: const Color(0xFF60A5FA), + ); + } + + final themeExtension = brightness == Brightness.dark + ? AppColorPalette.dark + : AppColorPalette.light; + + return ThemeData( + useMaterial3: true, + brightness: brightness, + colorScheme: colorScheme, + scaffoldBackgroundColor: colorScheme.surface, + fontFamily: 'Inter', + extensions: [themeExtension], + appBarTheme: AppBarTheme( + backgroundColor: colorScheme.surface, + foregroundColor: colorScheme.onSurface, elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.sm), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadius.sm), + ), ), ), - ), - inputDecorationTheme: InputDecorationTheme( - filled: true, - fillColor: AppColors.background, - contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppRadius.sm), - borderSide: const BorderSide(color: AppColors.input), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: colorScheme.surface, + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadius.sm), + borderSide: BorderSide(color: colorScheme.outlineVariant), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadius.sm), + borderSide: BorderSide(color: colorScheme.outlineVariant), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadius.sm), + borderSide: BorderSide(color: colorScheme.primary), + ), + hintStyle: TextStyle(color: colorScheme.onSurfaceVariant, fontSize: 14), ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppRadius.sm), - borderSide: const BorderSide(color: AppColors.input), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppRadius.sm), - borderSide: const BorderSide(color: AppColors.input), - ), - hintStyle: const TextStyle( - color: AppColors.mutedForeground, - fontSize: 14, - ), - ), - ); + ); + } } diff --git a/apps/lib/core/theme/design_tokens.dart b/apps/lib/core/theme/design_tokens.dart index 925a29a..c392cdd 100644 --- a/apps/lib/core/theme/design_tokens.dart +++ b/apps/lib/core/theme/design_tokens.dart @@ -1,170 +1,146 @@ import 'package:flutter/material.dart'; -class AppColors { - AppColors._(); +class AppColorPalette extends ThemeExtension { + final List eventPresetColors; + final List avatarColors; + final Color g1Text, g1Divider, g1Border; + final Color g2Text, g2Divider, g2Border; + final Color g3Text, g3Divider, g3Border; + final Color eventDefault; + final Color eventArchived; - static const primary = Color(0xFF171717); - static const primaryForeground = Color(0xFFFAFAFA); - static const background = Color(0xFFFAFAFA); - static const foreground = Color(0xFF0A0A0A); - static const mutedForeground = Color(0xFF737373); - static const input = Color(0xFFE5E5E5); - static const border = Color(0xFFE5E5E5); - static const white = Color(0xFFFFFFFF); - static const card = Color(0xFFFAFAFA); + const AppColorPalette({ + required this.eventPresetColors, + required this.avatarColors, + required this.g1Text, + required this.g1Divider, + required this.g1Border, + required this.g2Text, + required this.g2Divider, + required this.g2Border, + required this.g3Text, + required this.g3Divider, + required this.g3Border, + required this.eventDefault, + required this.eventArchived, + }); - static const slate900 = Color(0xFF0F172A); - static const slate800 = Color(0xFF1E293B); - static const slate700 = Color(0xFF334155); - static const slate600 = Color(0xFF475569); - static const slate500 = Color(0xFF64748B); - static const slate400 = Color(0xFF94A3B8); - static const slate300 = Color(0xFFCBD5E1); - static const slate200 = Color(0xFFE2E8F0); - static const slate100 = Color(0xFFF1F5F9); - static const slate50 = Color(0xFFF8FAFC); + static const light = AppColorPalette( + eventPresetColors: [ + Color(0xFF3B82F6), + Color(0xFF8B5CF6), + Color(0xFF10B981), + Color(0xFFF59E0B), + Color(0xFFEF4444), + ], + avatarColors: [ + Color(0xFF3B82F6), + Color(0xFF7C3AED), + Color(0xFF2563EB), + Color(0xFF2D6CDF), + Color(0xFF8B5CF6), + ], + g1Text: Color(0xFFB91C1C), + g1Divider: Color(0xFFFEE2E2), + g1Border: Color(0xFFF3C6C6), + g2Text: Color(0xFFB45309), + g2Divider: Color(0xFFFFEDD5), + g2Border: Color(0xFFFDE2B8), + g3Text: Color(0xFF1D4ED8), + g3Divider: Color(0xFFEAF3FF), + g3Border: Color(0xFFCFE1FB), + eventDefault: Color(0xFF3B82F6), + eventArchived: Color(0xFF64748B), + ); - static const blue600 = Color(0xFF2563EB); - static const blue500 = Color(0xFF3B82F6); - static const blue400 = Color(0xFF60A5FA); - static const blue300 = Color(0xFF93C5FD); - static const blue200 = Color(0xFFBFDBFE); - static const blue100 = Color(0xFFDBEAFE); - static const blue50 = Color(0xFFEFF6FF); + static const dark = AppColorPalette( + eventPresetColors: [ + Color(0xFF60A5FA), + Color(0xFFA78BFA), + Color(0xFF34D399), + Color(0xFFFBBF24), + Color(0xFFF87171), + ], + avatarColors: [ + Color(0xFF60A5FA), + Color(0xFFA78BFA), + Color(0xFF3B82F6), + Color(0xFF60A5FA), + Color(0xFF818CF8), + ], + g1Text: Color(0xFFFCA5A5), + g1Divider: Color(0xFF7F1D1D), + g1Border: Color(0xFFB91C1C), + g2Text: Color(0xFFFCD34D), + g2Divider: Color(0xFF78350F), + g2Border: Color(0xFFD97706), + g3Text: Color(0xFF93C5FD), + g3Divider: Color(0xFF1E3A8A), + g3Border: Color(0xFF2563EB), + eventDefault: Color(0xFF60A5FA), + eventArchived: Color(0xFF94A3B8), + ); - static const red600 = Color(0xFFDC2626); - static const red500 = Color(0xFFEF4444); - static const red400 = Color(0xFFD14343); + @override + AppColorPalette copyWith({ + List? eventPresetColors, + List? avatarColors, + Color? g1Text, + Color? g1Divider, + Color? g1Border, + Color? g2Text, + Color? g2Divider, + Color? g2Border, + Color? g3Text, + Color? g3Divider, + Color? g3Border, + Color? eventDefault, + Color? eventArchived, + }) { + return AppColorPalette( + eventPresetColors: eventPresetColors ?? this.eventPresetColors, + avatarColors: avatarColors ?? this.avatarColors, + g1Text: g1Text ?? this.g1Text, + g1Divider: g1Divider ?? this.g1Divider, + g1Border: g1Border ?? this.g1Border, + g2Text: g2Text ?? this.g2Text, + g2Divider: g2Divider ?? this.g2Divider, + g2Border: g2Border ?? this.g2Border, + g3Text: g3Text ?? this.g3Text, + g3Divider: g3Divider ?? this.g3Divider, + g3Border: g3Border ?? this.g3Border, + eventDefault: eventDefault ?? this.eventDefault, + eventArchived: eventArchived ?? this.eventArchived, + ); + } - static const surfaceSecondary = Color(0xFFF8FAFC); - static const surfaceTertiary = Color(0xFFF8FAFF); - static const surfaceInfo = Color(0xFFEAF3FF); - static const surfaceInfoLight = Color(0xFFF3F7FF); - - static const borderSecondary = Color(0xFFE2E8F0); - static const borderTertiary = Color(0xFFDCE5F4); - static const borderQuaternary = Color(0xFFCFE1FB); - - static const textSecondary = Color(0xFF475569); - - static const success = Color(0xFF10B981); - static const warning = Color(0xFFF59E0B); - static const error = Color(0xFFEF4444); - - static const messageBg = Color(0xFFF8FAFC); - static const messageCardBg = Color(0xFFFFFFFF); - static const messageTagBg = Color(0xFFEAF3FF); - static const messageBtnWrap = Color(0xFFF8FAFF); - static const messageBtnBorder = Color(0xFFDEE7F6); - static const messageCardBorder = Color(0xFFE3EAF6); - static const messageCalendarBg = Color(0xFFEEF4FF); - static const messageArrowColor = Color(0xFF9CAFC8); - static const messageTipBg = Color(0xFFF8FAFF); - static const messageTipBorder = Color(0xFFDCE6F4); - static const messageRejectBorder = Color(0xFFF1C9CE); - static const messageAcceptBorder = Color(0xFFCFE1FB); - static const messagePlaceholder = Color(0xFF9AAAC1); - static const messageInputBorder = Color(0xFFDCE5F4); - static const messageReasonBorder = Color(0xFFE6ECF7); - - static const amber600 = Color(0xFFD97706); - static const amber500 = Color(0xFFF59E0B); - - static const emerald600 = Color(0xFF059669); - static const emerald500 = Color(0xFF10B981); - - static const violet600 = Color(0xFF7C3AED); - static const violet500 = Color(0xFF8B5CF6); - - static const warningBackground = Color(0xFFFEF3C7); - static const warningText = Color(0xFF92400E); - - static const appIconRing = Color(0xFFE8F3FF); - static const appIconBorder = Color(0xFFC7DDFB); - static const appTitle = Color(0xFF1E293B); - - static const authBackgroundTop = Color(0xFFF4F8FF); - static const authBackgroundBottom = Color(0xFFF8FAFC); - static const authBackgroundOrb = Color(0xFFDCEBFF); - static const authCardBackground = Color(0xFFFCFDFE); - static const authCardBorder = Color(0xFFE5ECF6); - static const authCardHighlight = Color(0xFFFFFFFF); - static const authSectionBackground = Color(0xFFF7FAFE); - static const authSectionBorder = Color(0xFFE4EBF5); - static const authInputBackground = Color(0xFFF6F9FD); - static const authInputBorder = Color(0xFFD9E4F1); - static const authInputFocus = Color(0xFF8EB8F3); - static const authInputIcon = Color(0xFF8A9BB2); - static const authPrimaryButton = Color(0xFF2F6FD6); - static const authPrimaryButtonPressed = Color(0xFF245FC0); - static const authPrimaryButtonDisabled = Color(0xFFD9E3F2); - static const authPrimaryButtonText = Color(0xFFF8FBFF); - static const authSecondaryButtonBackground = Color(0xFFF4F8FF); - static const authSecondaryButtonBorder = Color(0xFFD8E4F6); - static const authSecondaryButtonText = Color(0xFF315D9C); - static const authLinkText = Color(0xFF356CC8); - static const authLinkMuted = Color(0xFF70839E); - - static const homeBackgroundTop = Color(0xFFF5F9FF); - static const homeBackgroundBottom = Color(0xFFF7FAFE); - static const homeBackgroundGlow = Color(0xFFDCEBFF); - static const homeBackgroundGlowSoft = Color(0xFFF1F6FF); - static const homeToolbarSurface = Color(0xF2FFFFFF); - static const homeToolbarBorder = Color(0xFFD9E6F7); - static const homeConversationSurface = Color(0xBFFFFFFF); - static const homeConversationBorder = Color(0xFFDDE8F6); - static const homeComposerShell = Color(0xFDFCFEFF); - static const homeComposerInner = Color(0xFFF7FAFE); - static const homeComposerBorder = Color(0xFFD7E3F3); - static const homeComposerAccent = Color(0xFFEAF3FF); - static const homeAttachmentSurface = Color(0xFFF3F7FD); - - static const feedbackInfoSurface = Color(0xFFF3F8FF); - static const feedbackInfoBorder = Color(0xFFD6E5FB); - static const feedbackInfoIcon = Color(0xFF2D6CDF); - static const feedbackInfoText = Color(0xFF26476F); - - static const feedbackSuccessSurface = Color(0xFFF1FBF6); - static const feedbackSuccessBorder = Color(0xFFCDECD9); - static const feedbackSuccessIcon = Color(0xFF129268); - static const feedbackSuccessText = Color(0xFF1E5A46); - - static const feedbackWarningSurface = Color(0xFFFFF8ED); - static const feedbackWarningBorder = Color(0xFFF4DFC0); - static const feedbackWarningIcon = Color(0xFFD68A18); - static const feedbackWarningText = Color(0xFF7A5821); - - static const feedbackErrorSurface = Color(0xFFFFF4F3); - static const feedbackErrorBorder = Color(0xFFF1D2D0); - static const feedbackErrorIcon = Color(0xFFD14F4B); - static const feedbackErrorText = Color(0xFF7E3735); - - static const todoBg = Color(0xFFF8FAFC); - static const todoCardBg = Color(0xFFFFFFFF); - - static const g1Text = Color(0xFFB91C1C); - static const g1Divider = Color(0xFFFEE2E2); - static const g1Border = Color(0xFFF3C6C6); - - static const g2Text = Color(0xFFB45309); - static const g2Divider = Color(0xFFFFEDD5); - static const g2Border = Color(0xFFFDE2B8); - - static const g3Text = Color(0xFF1D4ED8); - static const g3Divider = Color(0xFFEAF3FF); - static const g3Border = Color(0xFFCFE1FB); - - static const todoDetailCardBorder = Color(0xFFDCE5F4); - static const todoEventBorder1 = Color(0xFFDCE5F4); - static const todoEventBorder2 = Color(0xFFDCC8FF); - static const todoEventBorder3 = Color(0xFFCFE1FB); - - static const todoToggleBg = Color(0xFFFDFEFF); - static const todoToggleBorder = Color(0xFFDCE6F4); - static const todoToggleActiveBg = Color(0xFFD6E6FF); - static const todoToggleActiveBorder = Color(0xFFBFD6FB); - static const todoHomeBtnBg = Color(0xFFE6EEFB); - static const todoHomeBtnBorder = Color(0xFFC9D8EE); + @override + AppColorPalette lerp(ThemeExtension? other, double t) { + if (other is! AppColorPalette) return this; + return AppColorPalette( + eventPresetColors: eventPresetColors + .asMap() + .entries + .map((e) => Color.lerp(e.value, other.eventPresetColors[e.key], t)!) + .toList(), + avatarColors: avatarColors + .asMap() + .entries + .map((e) => Color.lerp(e.value, other.avatarColors[e.key], t)!) + .toList(), + g1Text: Color.lerp(g1Text, other.g1Text, t)!, + g1Divider: Color.lerp(g1Divider, other.g1Divider, t)!, + g1Border: Color.lerp(g1Border, other.g1Border, t)!, + g2Text: Color.lerp(g2Text, other.g2Text, t)!, + g2Divider: Color.lerp(g2Divider, other.g2Divider, t)!, + g2Border: Color.lerp(g2Border, other.g2Border, t)!, + g3Text: Color.lerp(g3Text, other.g3Text, t)!, + g3Divider: Color.lerp(g3Divider, other.g3Divider, t)!, + g3Border: Color.lerp(g3Border, other.g3Border, t)!, + eventDefault: Color.lerp(eventDefault, other.eventDefault, t)!, + eventArchived: Color.lerp(eventArchived, other.eventArchived, t)!, + ); + } } class AppSpacing { diff --git a/apps/lib/core/cache/cache_entry.dart b/apps/lib/data/cache/cache_entry.dart similarity index 100% rename from apps/lib/core/cache/cache_entry.dart rename to apps/lib/data/cache/cache_entry.dart diff --git a/apps/lib/core/cache/cache_invalidator.dart b/apps/lib/data/cache/cache_invalidator.dart similarity index 100% rename from apps/lib/core/cache/cache_invalidator.dart rename to apps/lib/data/cache/cache_invalidator.dart diff --git a/apps/lib/core/cache/cache_policy.dart b/apps/lib/data/cache/cache_policy.dart similarity index 100% rename from apps/lib/core/cache/cache_policy.dart rename to apps/lib/data/cache/cache_policy.dart diff --git a/apps/lib/core/cache/cache_store.dart b/apps/lib/data/cache/cache_store.dart similarity index 100% rename from apps/lib/core/cache/cache_store.dart rename to apps/lib/data/cache/cache_store.dart diff --git a/apps/lib/data/cache/cached_repository.dart b/apps/lib/data/cache/cached_repository.dart new file mode 100644 index 0000000..e80c2fc --- /dev/null +++ b/apps/lib/data/cache/cached_repository.dart @@ -0,0 +1,107 @@ +import 'dart:async'; + +import 'cache_entry.dart'; +import 'cache_policy.dart'; +import 'hybrid_cache_store.dart'; + +abstract class CachedRepository { + final HybridCacheStore store; + final CachePolicy policy; + final DateTime Function() now; + final Map> _refreshInFlight = >{}; + + CachedRepository({ + required this.store, + required this.policy, + DateTime Function()? now, + }) : now = now ?? DateTime.now; + + Future getOrLoad({ + required String key, + required Future Function() loadFromRemote, + bool Function(T loaded)? shouldWriteLoaded, + bool forceRefresh = false, + }) async { + if (forceRefresh) { + return _refreshAndWrite( + key, + loadFromRemote, + shouldWriteLoaded: shouldWriteLoaded, + ); + } + + final cached = await readCacheEntry(key); + if (cached == null) { + return _refreshAndWrite( + key, + loadFromRemote, + shouldWriteLoaded: shouldWriteLoaded, + ); + } + + final decision = policy.evaluate(now: now(), fetchedAt: cached.fetchedAt); + if (decision.shouldRefreshInBackground) { + refreshInBackground( + key: key, + loadFromRemote: loadFromRemote, + shouldWriteLoaded: shouldWriteLoaded, + ); + } + if (decision.mustBlockForNetwork || !decision.canUseCached) { + return _refreshAndWrite( + key, + loadFromRemote, + shouldWriteLoaded: shouldWriteLoaded, + ); + } + return cached.value; + } + + Future?> readCacheEntry(String key) { + return store.read>(key); + } + + Future writeCacheEntry(String key, T value) { + return store.write>( + key, + CacheEntry(value: value, fetchedAt: now()), + ); + } + + Future removeCacheKey(String key) { + return store.remove(key); + } + + void refreshInBackground({ + required String key, + required Future Function() loadFromRemote, + bool Function(T loaded)? shouldWriteLoaded, + }) { + if (_refreshInFlight.containsKey(key)) { + return; + } + final task = _refreshAndWrite( + key, + loadFromRemote, + shouldWriteLoaded: shouldWriteLoaded, + ).then((_) {}); + final tracked = task.whenComplete(() { + _refreshInFlight.remove(key); + }); + _refreshInFlight[key] = tracked; + unawaited(tracked); + } + + Future _refreshAndWrite( + String key, + Future Function() loadFromRemote, { + bool Function(T loaded)? shouldWriteLoaded, + }) async { + final remote = await loadFromRemote(); + if (shouldWriteLoaded != null && !shouldWriteLoaded(remote)) { + return remote; + } + await writeCacheEntry(key, remote); + return remote; + } +} diff --git a/apps/lib/core/cache/hybrid_cache_store.dart b/apps/lib/data/cache/hybrid_cache_store.dart similarity index 100% rename from apps/lib/core/cache/hybrid_cache_store.dart rename to apps/lib/data/cache/hybrid_cache_store.dart diff --git a/apps/lib/core/cache/memory_cache_store.dart b/apps/lib/data/cache/memory_cache_store.dart similarity index 100% rename from apps/lib/core/cache/memory_cache_store.dart rename to apps/lib/data/cache/memory_cache_store.dart diff --git a/apps/lib/core/cache/persistent_cache_store.dart b/apps/lib/data/cache/persistent_cache_store.dart similarity index 100% rename from apps/lib/core/cache/persistent_cache_store.dart rename to apps/lib/data/cache/persistent_cache_store.dart diff --git a/apps/lib/features/notification/domain/models/reminder_payload.dart b/apps/lib/data/models/reminder_payload.dart similarity index 100% rename from apps/lib/features/notification/domain/models/reminder_payload.dart rename to apps/lib/data/models/reminder_payload.dart diff --git a/apps/lib/features/contacts/data/users/models/user_response.dart b/apps/lib/data/models/user_profile.dart similarity index 86% rename from apps/lib/features/contacts/data/users/models/user_response.dart rename to apps/lib/data/models/user_profile.dart index e7c2899..b1625f2 100644 --- a/apps/lib/features/contacts/data/users/models/user_response.dart +++ b/apps/lib/data/models/user_profile.dart @@ -1,11 +1,11 @@ -class UserResponse { +class UserProfile { final String id; final String username; final String? phone; final String? avatarUrl; final String? bio; - const UserResponse({ + const UserProfile({ required this.id, required this.username, this.phone, @@ -13,8 +13,8 @@ class UserResponse { this.bio, }); - factory UserResponse.fromJson(Map json) { - return UserResponse( + factory UserProfile.fromJson(Map json) { + return UserProfile( id: json['id'] as String, username: json['username'] as String, phone: json['phone'] as String?, diff --git a/apps/lib/data/repositories/calendar_event_repository.dart b/apps/lib/data/repositories/calendar_event_repository.dart new file mode 100644 index 0000000..9797dd4 --- /dev/null +++ b/apps/lib/data/repositories/calendar_event_repository.dart @@ -0,0 +1,60 @@ +import '../../core/network/i_api_client.dart'; +import 'models/calendar_event.dart'; + +abstract class CalendarEventRepository { + Future> listByRange({ + required DateTime startAt, + required DateTime endAt, + }); + + Future getById(String id); + Future acceptSubscription(String itemId); + Future rejectSubscription(String itemId); +} + +class CalendarEventRepositoryImpl implements CalendarEventRepository { + final IApiClient _apiClient; + static const _prefix = '/api/v1/schedule-items'; + + CalendarEventRepositoryImpl(this._apiClient); + + @override + Future> listByRange({ + required DateTime startAt, + required DateTime endAt, + }) async { + final start = Uri.encodeQueryComponent(startAt.toUtc().toIso8601String()); + final end = Uri.encodeQueryComponent(endAt.toUtc().toIso8601String()); + final response = await _apiClient.get>( + '$_prefix?start_at=$start&end_at=$end', + ); + final data = response.data; + if (data == null) { + throw StateError('Invalid listByRange response: empty payload'); + } + return data + .whereType>() + .map(CalendarEvent.fromJson) + .toList(growable: false); + } + + @override + Future getById(String id) async { + final response = await _apiClient.get>('$_prefix/$id'); + final event = response.data; + if (event == null) { + throw StateError('Invalid getById response: empty payload'); + } + return CalendarEvent.fromJson(event); + } + + @override + Future acceptSubscription(String itemId) { + return _apiClient.post('$_prefix/$itemId/accept'); + } + + @override + Future rejectSubscription(String itemId) { + return _apiClient.post('$_prefix/$itemId/reject'); + } +} diff --git a/apps/lib/data/repositories/calendar_repository.dart b/apps/lib/data/repositories/calendar_repository.dart new file mode 100644 index 0000000..9be41ce --- /dev/null +++ b/apps/lib/data/repositories/calendar_repository.dart @@ -0,0 +1,98 @@ +import '../cache/cache_policy.dart'; +import '../cache/cached_repository.dart'; +import '../../core/network/i_api_client.dart'; +import 'models/schedule_item_model.dart'; + +class CalendarRepository extends CachedRepository> { + final IApiClient _apiClient; + static const _prefix = '/api/v1/schedule-items'; + + CalendarRepository({ + required super.store, + required IApiClient apiClient, + CachePolicy? policy, + super.now, + }) : _apiClient = apiClient, + super( + policy: + policy ?? + const CachePolicy( + softTtl: Duration(minutes: 2), + hardTtl: Duration(minutes: 30), + minRefreshInterval: Duration(minutes: 1), + ), + ); + + static String dayKey(DateTime date) { + final day = + '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; + return 'calendar:day:$day'; + } + + static String monthKey(DateTime date) { + return 'calendar:month:${date.year}-${date.month.toString().padLeft(2, '0')}'; + } + + Future> getDayEvents( + DateTime date, { + bool forceRefresh = false, + }) async { + final key = dayKey(date); + final start = DateTime(date.year, date.month, date.day); + final end = DateTime(date.year, date.month, date.day, 23, 59, 59); + return getOrLoad( + key: key, + forceRefresh: forceRefresh, + loadFromRemote: () => _listByRange(startAt: start, endAt: end), + ); + } + + Future> getMonthEvents( + DateTime monthStart, { + bool forceRefresh = false, + }) async { + final key = monthKey(monthStart); + final start = DateTime(monthStart.year, monthStart.month, 1); + final end = DateTime(monthStart.year, monthStart.month + 1, 0, 23, 59, 59); + return getOrLoad( + key: key, + forceRefresh: forceRefresh, + loadFromRemote: () => _listByRange(startAt: start, endAt: end), + ); + } + + Future getEventById(String id) async { + final response = await _apiClient.get>('$_prefix/$id'); + final data = response.data; + if (data == null) { + throw StateError('Invalid getEventById response: empty payload'); + } + return ScheduleItemModel.fromJson(data); + } + + Future> listEventsByRange({ + required DateTime start, + required DateTime end, + }) { + return _listByRange(startAt: start, endAt: end); + } + + Future> _listByRange({ + required DateTime startAt, + required DateTime endAt, + }) async { + final start = Uri.encodeQueryComponent(startAt.toUtc().toIso8601String()); + final end = Uri.encodeQueryComponent(endAt.toUtc().toIso8601String()); + final response = await _apiClient.get>( + '$_prefix?start_at=$start&end_at=$end', + ); + final data = response.data; + if (data == null) { + throw StateError('Invalid listByRange response: empty payload'); + } + return data + .whereType>() + .map(ScheduleItemModel.fromJson) + .toList(growable: false); + } +} diff --git a/apps/lib/data/repositories/friend_repository.dart b/apps/lib/data/repositories/friend_repository.dart new file mode 100644 index 0000000..855a203 --- /dev/null +++ b/apps/lib/data/repositories/friend_repository.dart @@ -0,0 +1,88 @@ +import '../../core/network/i_api_client.dart'; +import 'models/friend_request.dart'; + +abstract class FriendRepository { + Future> getFriends(); + Future getRequestById(String friendshipId); + Future> getRequestsByIds( + List friendshipIds, + ); + Future acceptRequest(String friendshipId); + Future declineRequest(String friendshipId); +} + +class FriendRepositoryImpl implements FriendRepository { + final IApiClient _apiClient; + static const _prefix = '/api/v1/friends'; + + FriendRepositoryImpl(this._apiClient); + + @override + Future> getFriends() async { + final response = await _apiClient.get>(_prefix); + final data = response.data; + if (data == null) { + throw StateError('Invalid getFriends response: empty payload'); + } + return data + .map((item) => item as Map) + .map( + (item) => FriendUser.fromJson(item['friend'] as Map), + ) + .toList(growable: false); + } + + @override + Future getRequestById(String friendshipId) async { + final response = await _apiClient.get>( + '$_prefix/requests/$friendshipId', + ); + final data = response.data; + if (data == null) { + throw StateError('Invalid getRequestById response: empty payload'); + } + return FriendRequest.fromJson(data); + } + + @override + Future> getRequestsByIds( + List friendshipIds, + ) async { + if (friendshipIds.isEmpty) { + return const {}; + } + + final pairs = await Future.wait( + friendshipIds.map((id) async { + final request = await getRequestById(id); + return MapEntry(id, request); + }), + ); + + return Map.fromEntries(pairs); + } + + @override + Future acceptRequest(String friendshipId) async { + final response = await _apiClient.post>( + '$_prefix/requests/$friendshipId/accept', + ); + final data = response.data; + if (data == null) { + throw StateError('Invalid acceptRequest response: empty payload'); + } + return FriendRequest.fromJson(data); + } + + @override + Future declineRequest(String friendshipId) async { + final response = await _apiClient.post>( + '$_prefix/requests/$friendshipId/decline', + ); + final data = response.data; + if (data == null) { + throw StateError('Invalid declineRequest response: empty payload'); + } + return FriendRequest.fromJson(data); + } +} diff --git a/apps/lib/data/repositories/inbox_repository.dart b/apps/lib/data/repositories/inbox_repository.dart new file mode 100644 index 0000000..1101c05 --- /dev/null +++ b/apps/lib/data/repositories/inbox_repository.dart @@ -0,0 +1,42 @@ +import '../../core/network/i_api_client.dart'; +import 'models/inbox_message.dart'; + +abstract class InboxRepository { + Future> getMessages({bool? isRead}); + Future markAsRead(String messageId); +} + +class InboxRepositoryImpl implements InboxRepository { + final IApiClient _apiClient; + static const _prefix = '/api/v1/inbox/messages'; + + InboxRepositoryImpl(this._apiClient); + + @override + Future> getMessages({bool? isRead}) async { + final queryParams = isRead != null ? '?is_read=$isRead' : ''; + final response = await _apiClient.get>( + '$_prefix$queryParams', + ); + final data = response.data; + if (data == null) { + throw StateError('Invalid getMessages response: empty payload'); + } + return data + .whereType>() + .map(InboxMessage.fromJson) + .toList(growable: false); + } + + @override + Future markAsRead(String messageId) async { + final response = await _apiClient.patch>( + '$_prefix/$messageId/read', + ); + final data = response.data; + if (data == null) { + throw StateError('Invalid markAsRead response: empty payload'); + } + return InboxMessage.fromJson(data); + } +} diff --git a/apps/lib/data/repositories/models/calendar_event.dart b/apps/lib/data/repositories/models/calendar_event.dart new file mode 100644 index 0000000..5dbcec9 --- /dev/null +++ b/apps/lib/data/repositories/models/calendar_event.dart @@ -0,0 +1,40 @@ +enum CalendarEventStatus { active, archived } + +class CalendarEvent { + final String id; + final String title; + final DateTime startAt; + final DateTime? endAt; + final CalendarEventStatus status; + + const CalendarEvent({ + required this.id, + required this.title, + required this.startAt, + required this.endAt, + required this.status, + }); + + factory CalendarEvent.fromJson(Map json) { + return CalendarEvent( + id: json['id'] as String, + title: json['title'] as String, + startAt: DateTime.parse(json['start_at'] as String).toLocal(), + endAt: json['end_at'] == null + ? null + : DateTime.parse(json['end_at'] as String).toLocal(), + status: _calendarEventStatusFromApi(json['status'] as String), + ); + } +} + +CalendarEventStatus _calendarEventStatusFromApi(String raw) { + switch (raw) { + case 'active': + return CalendarEventStatus.active; + case 'archived': + return CalendarEventStatus.archived; + default: + throw StateError('Unsupported calendar event status: $raw'); + } +} diff --git a/apps/lib/data/repositories/models/friend_request.dart b/apps/lib/data/repositories/models/friend_request.dart new file mode 100644 index 0000000..7874713 --- /dev/null +++ b/apps/lib/data/repositories/models/friend_request.dart @@ -0,0 +1,63 @@ +enum FriendRequestStatus { pending, accepted, rejected } + +class FriendUser { + final String id; + final String username; + final String? avatarUrl; + + const FriendUser({ + required this.id, + required this.username, + required this.avatarUrl, + }); + + factory FriendUser.fromJson(Map json) { + return FriendUser( + id: json['id'] as String, + username: json['username'] as String, + avatarUrl: json['avatar_url'] as String?, + ); + } +} + +class FriendRequest { + final String id; + final FriendUser sender; + final FriendUser recipient; + final String? content; + final FriendRequestStatus status; + final DateTime createdAt; + + const FriendRequest({ + required this.id, + required this.sender, + required this.recipient, + required this.content, + required this.status, + required this.createdAt, + }); + + factory FriendRequest.fromJson(Map json) { + return FriendRequest( + id: json['id'] as String, + sender: FriendUser.fromJson(json['sender'] as Map), + recipient: FriendUser.fromJson(json['recipient'] as Map), + content: json['content'] as String?, + status: _friendRequestStatusFromApi(json['status'] as String), + createdAt: DateTime.parse(json['created_at'] as String), + ); + } +} + +FriendRequestStatus _friendRequestStatusFromApi(String raw) { + switch (raw) { + case 'pending': + return FriendRequestStatus.pending; + case 'accepted': + return FriendRequestStatus.accepted; + case 'rejected': + return FriendRequestStatus.rejected; + default: + throw StateError('Unsupported friend request status: $raw'); + } +} diff --git a/apps/lib/data/repositories/models/inbox_message.dart b/apps/lib/data/repositories/models/inbox_message.dart new file mode 100644 index 0000000..b6687f3 --- /dev/null +++ b/apps/lib/data/repositories/models/inbox_message.dart @@ -0,0 +1,74 @@ +enum InboxMessageType { friendRequest, calendar, system, group } + +enum InboxMessageStatus { pending, accepted, rejected, dismissed } + +class InboxMessage { + final String id; + final String recipientId; + final String? senderId; + final InboxMessageType messageType; + final String? scheduleItemId; + final String? friendshipId; + final Map? content; + final bool isRead; + final InboxMessageStatus status; + final DateTime createdAt; + + const InboxMessage({ + required this.id, + required this.recipientId, + required this.senderId, + required this.messageType, + required this.scheduleItemId, + required this.friendshipId, + required this.content, + required this.isRead, + required this.status, + required this.createdAt, + }); + + factory InboxMessage.fromJson(Map json) { + return InboxMessage( + id: json['id'] as String, + recipientId: json['recipient_id'] as String, + senderId: json['sender_id'] as String?, + messageType: _messageTypeFromApi(json['message_type'] as String), + scheduleItemId: json['schedule_item_id'] as String?, + friendshipId: json['friendship_id'] as String?, + content: json['content'] as Map?, + isRead: json['is_read'] as bool, + status: _messageStatusFromApi(json['status'] as String), + createdAt: DateTime.parse(json['created_at'] as String), + ); + } +} + +InboxMessageType _messageTypeFromApi(String raw) { + switch (raw) { + case 'friend_request': + return InboxMessageType.friendRequest; + case 'calendar': + return InboxMessageType.calendar; + case 'system': + return InboxMessageType.system; + case 'group': + return InboxMessageType.group; + default: + throw StateError('Unsupported inbox message type: $raw'); + } +} + +InboxMessageStatus _messageStatusFromApi(String raw) { + switch (raw) { + case 'pending': + return InboxMessageStatus.pending; + case 'accepted': + return InboxMessageStatus.accepted; + case 'rejected': + return InboxMessageStatus.rejected; + case 'dismissed': + return InboxMessageStatus.dismissed; + default: + throw StateError('Unsupported inbox message status: $raw'); + } +} diff --git a/apps/lib/features/calendar/data/models/schedule_item_model.dart b/apps/lib/data/repositories/models/schedule_item_model.dart similarity index 82% rename from apps/lib/features/calendar/data/models/schedule_item_model.dart rename to apps/lib/data/repositories/models/schedule_item_model.dart index 5c3795b..a421386 100644 --- a/apps/lib/features/calendar/data/models/schedule_item_model.dart +++ b/apps/lib/data/repositories/models/schedule_item_model.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - enum ScheduleSourceType { manual, imported, agentGenerated } enum ScheduleStatus { active, archived } @@ -83,27 +81,23 @@ class ScheduleItemModel { factory ScheduleItemModel.fromJson(Map json) { return ScheduleItemModel( id: json['id'] as String, - ownerId: json['owner_id'] as String? ?? '', - permission: json['permission'] as int? ?? 1, - isOwner: json['is_owner'] as bool? ?? false, + ownerId: json['owner_id'] as String, + permission: json['permission'] as int, + isOwner: json['is_owner'] as bool, title: json['title'] as String, description: json['description'] as String?, startAt: DateTime.parse(json['start_at'] as String).toLocal(), endAt: json['end_at'] != null ? DateTime.parse(json['end_at'] as String).toLocal() : null, - timezone: (json['timezone'] as String?) ?? 'UTC', + timezone: json['timezone'] as String, metadata: json['metadata'] is Map ? ScheduleMetadata.fromJson(json['metadata'] as Map) : null, - sourceType: _sourceTypeFromApi(json['source_type'] as String?), - status: _statusFromApi(json['status'] as String?), - createdAt: json['created_at'] != null - ? DateTime.parse(json['created_at'] as String).toLocal() - : DateTime.now(), - updatedAt: json['updated_at'] != null - ? DateTime.parse(json['updated_at'] as String).toLocal() - : DateTime.now(), + sourceType: _sourceTypeFromApi(json['source_type'] as String), + status: _statusFromApi(json['status'] as String), + createdAt: DateTime.parse(json['created_at'] as String).toLocal(), + updatedAt: DateTime.parse(json['updated_at'] as String).toLocal(), ); } @@ -172,20 +166,17 @@ class ScheduleMetadata { } factory ScheduleMetadata.fromJson(Map json) { - final rawAttachments = json['attachments']; - final attachments = rawAttachments is List - ? rawAttachments - .whereType>() - .map(Attachment.fromJson) - .toList() - : []; + final rawAttachments = json['attachments'] as List; + final attachments = rawAttachments + .map((item) => Attachment.fromJson(item as Map)) + .toList(growable: false); return ScheduleMetadata( color: json['color'] as String?, location: json['location'] as String?, notes: json['notes'] as String?, reminderMinutes: json['reminder_minutes'] as int?, attachments: attachments, - version: (json['version'] as int?) ?? 1, + version: json['version'] as int, raw: Map.from(json), ); } @@ -238,17 +229,15 @@ class Attachment { } factory Attachment.fromJson(Map json) { - final rawVisibleTo = json['visible_to']; - final visibleTo = rawVisibleTo is List - ? rawVisibleTo.map((item) => item.toString()).toList() - : []; + final rawVisibleTo = json['visible_to'] as List; + final visibleTo = rawVisibleTo.map((item) => item.toString()).toList(); return Attachment( - name: (json['name'] as String?) ?? '', + name: json['name'] as String, visibleTo: visibleTo, url: json['url'] as String?, note: json['note'] as String?, content: json['content'] as String?, - type: (json['type'] as String?) ?? 'document', + type: json['type'] as String, ); } @@ -264,27 +253,27 @@ class Attachment { } } -ScheduleSourceType _sourceTypeFromApi(String? raw) { +ScheduleSourceType _sourceTypeFromApi(String raw) { switch (raw) { case 'imported': return ScheduleSourceType.imported; case 'agent_generated': return ScheduleSourceType.agentGenerated; case 'manual': - default: return ScheduleSourceType.manual; + default: + throw StateError('Unsupported schedule source type: $raw'); } } -ScheduleStatus _statusFromApi(String? raw) { +ScheduleStatus _statusFromApi(String raw) { switch (raw) { - case 'completed': - case 'canceled': case 'archived': return ScheduleStatus.archived; case 'active': - default: return ScheduleStatus.active; + default: + throw StateError('Unsupported schedule status: $raw'); } } @@ -296,11 +285,3 @@ String _statusToApi(ScheduleStatus status) { return 'archived'; } } - -const defaultColors = [ - Color(0xFF3B82F6), - Color(0xFF8B5CF6), - Color(0xFF10B981), - Color(0xFFF59E0B), - Color(0xFFEF4444), -]; diff --git a/apps/lib/data/repositories/models/user_summary.dart b/apps/lib/data/repositories/models/user_summary.dart new file mode 100644 index 0000000..7c7ce16 --- /dev/null +++ b/apps/lib/data/repositories/models/user_summary.dart @@ -0,0 +1,19 @@ +class UserSummary { + final String id; + final String username; + final String? avatarUrl; + + const UserSummary({ + required this.id, + required this.username, + required this.avatarUrl, + }); + + factory UserSummary.fromJson(Map json) { + return UserSummary( + id: json['id'] as String, + username: json['username'] as String, + avatarUrl: json['avatar_url'] as String?, + ); + } +} diff --git a/apps/lib/data/repositories/user_repository.dart b/apps/lib/data/repositories/user_repository.dart new file mode 100644 index 0000000..86512fc --- /dev/null +++ b/apps/lib/data/repositories/user_repository.dart @@ -0,0 +1,36 @@ +import '../../core/network/i_api_client.dart'; +import 'models/user_summary.dart'; + +abstract class UserRepository { + Future getById(String userId); + Future getMe(); +} + +class UserRepositoryImpl implements UserRepository { + final IApiClient _apiClient; + static const _prefix = '/api/v1/users'; + + UserRepositoryImpl(this._apiClient); + + @override + Future getById(String userId) async { + final response = await _apiClient.get>( + '$_prefix/$userId', + ); + final user = response.data; + if (user == null) { + throw StateError('Invalid getById response: empty payload'); + } + return UserSummary.fromJson(user); + } + + @override + Future getMe() async { + final response = await _apiClient.get>('$_prefix/me'); + final user = response.data; + if (user == null) { + throw StateError('Invalid getMe response: empty payload'); + } + return UserSummary.fromJson(user); + } +} diff --git a/apps/lib/data/services/calendar_service.dart b/apps/lib/data/services/calendar_service.dart new file mode 100644 index 0000000..53ae478 --- /dev/null +++ b/apps/lib/data/services/calendar_service.dart @@ -0,0 +1,112 @@ +import '../../core/network/i_api_client.dart'; +import '../cache/cache_invalidator.dart'; +import '../repositories/models/schedule_item_model.dart'; + +class CalendarService { + static const _prefix = '/api/v1/schedule-items'; + + final IApiClient _apiClient; + final CacheInvalidator _invalidator; + + CalendarService({ + required IApiClient apiClient, + required CacheInvalidator invalidator, + }) : _apiClient = apiClient, + _invalidator = invalidator; + + Future> getEventsForDay(DateTime date) async { + final start = DateTime(date.year, date.month, date.day); + final end = DateTime(date.year, date.month, date.day, 23, 59, 59); + return getEventsForRange(start, end); + } + + Future> getEventsForRange( + DateTime start, + DateTime end, + ) async { + final startParam = Uri.encodeQueryComponent( + start.toUtc().toIso8601String(), + ); + final endParam = Uri.encodeQueryComponent(end.toUtc().toIso8601String()); + final response = await _apiClient.get>( + '$_prefix?start_at=$startParam&end_at=$endParam', + ); + final data = response.data; + if (data == null) { + throw StateError('Invalid getEventsForRange response: empty payload'); + } + return data + .map((item) => item as Map) + .map(ScheduleItemModel.fromJson) + .toList(growable: false); + } + + Future getEventById(String id) async { + final response = await _apiClient.get>('$_prefix/$id'); + final data = response.data; + if (data == null) { + throw StateError('Invalid getEventById response: empty payload'); + } + return ScheduleItemModel.fromJson(data); + } + + Future addEvent(ScheduleItemModel event) async { + final response = await _apiClient.post>( + _prefix, + data: event.toCreateJson(), + ); + final data = response.data; + if (data == null) { + throw StateError('Invalid addEvent response: empty payload'); + } + final created = ScheduleItemModel.fromJson(data); + _invalidateEventCache(created); + return created; + } + + Future updateEvent(ScheduleItemModel event) async { + final response = await _apiClient.patch>( + '$_prefix/${event.id}', + data: event.toUpdateJson(), + ); + final data = response.data; + if (data == null) { + throw StateError('Invalid updateEvent response: empty payload'); + } + final updated = ScheduleItemModel.fromJson(data); + _invalidateEventCache(updated); + return updated; + } + + Future archiveEvent(String id) async { + final event = await getEventById(id); + final updatedEvent = await updateEvent( + event.copyWith(status: ScheduleStatus.archived), + ); + _invalidateEventCache(updatedEvent); + return updatedEvent; + } + + Future deleteEvent(String id) async { + final event = await getEventById(id); + _invalidateEventCache(event); + await _apiClient.delete('$_prefix/$id'); + } + + void _invalidateEventCache(ScheduleItemModel event) { + var current = DateTime( + event.startAt.year, + event.startAt.month, + event.startAt.day, + ); + final end = DateTime( + event.endAt?.year ?? event.startAt.year, + event.endAt?.month ?? event.startAt.month, + event.endAt?.day ?? event.startAt.day, + ); + while (!current.isAfter(end)) { + _invalidator.invalidateCalendarDay(current); + current = current.add(const Duration(days: 1)); + } + } +} diff --git a/apps/lib/data/services/ios_notification_payload_bridge.dart b/apps/lib/data/services/ios_notification_payload_bridge.dart new file mode 100644 index 0000000..9e15b7b --- /dev/null +++ b/apps/lib/data/services/ios_notification_payload_bridge.dart @@ -0,0 +1,20 @@ +import '../../core/storage/app_preferences.dart'; +import '../models/reminder_payload.dart'; + +class IOSNotificationPayloadBridge { + final AppPreferences _prefs; + + IOSNotificationPayloadBridge(this._prefs); + + ReminderPayload? getPendingPayload() { + return _prefs.pendingNotificationPayload; + } + + Future setPendingPayload(ReminderPayload payload) { + return _prefs.setPendingNotificationPayload(payload); + } + + Future clearPendingPayload() { + return _prefs.clearPendingNotificationPayload(); + } +} diff --git a/apps/lib/features/notification/data/services/local_notification_service.dart b/apps/lib/data/services/local_notification_service.dart similarity index 97% rename from apps/lib/features/notification/data/services/local_notification_service.dart rename to apps/lib/data/services/local_notification_service.dart index 81ba293..9754730 100644 --- a/apps/lib/features/notification/data/services/local_notification_service.dart +++ b/apps/lib/data/services/local_notification_service.dart @@ -5,10 +5,10 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:timezone/data/latest.dart' as tz_data; import 'package:timezone/timezone.dart' as tz; -import '../../../../core/l10n/l10n.dart'; +import '../../core/l10n/l10n.dart'; +import '../models/reminder_payload.dart'; +import '../repositories/models/schedule_item_model.dart'; import 'reminder_notification_callbacks.dart'; -import '../../../calendar/data/models/schedule_item_model.dart'; -import '../../domain/models/reminder_payload.dart'; class LocalNotificationService { final FlutterLocalNotificationsPlugin _plugin; @@ -262,8 +262,7 @@ class LocalNotificationService { return null; } try { - final json = Map.from(jsonDecode(raw) as Map); - return ReminderPayload.fromJson(json); + return ReminderPayload.fromJson(jsonDecode(raw) as Map); } catch (_) { return null; } diff --git a/apps/lib/features/notification/data/services/reminder_notification_callbacks.dart b/apps/lib/data/services/reminder_notification_callbacks.dart similarity index 59% rename from apps/lib/features/notification/data/services/reminder_notification_callbacks.dart rename to apps/lib/data/services/reminder_notification_callbacks.dart index afbc38f..ccae7cb 100644 --- a/apps/lib/features/notification/data/services/reminder_notification_callbacks.dart +++ b/apps/lib/data/services/reminder_notification_callbacks.dart @@ -1,28 +1,42 @@ import 'dart:async'; -import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import '../../domain/models/reminder_payload.dart'; +import '../../core/storage/app_preferences.dart'; +import '../models/reminder_payload.dart'; typedef ReminderNotificationResponseHandler = Future Function(NotificationResponse response); class ReminderNotificationCallbacks { - static const String _pendingKey = - 'calendar_reminder_pending_notification_responses_v1'; static ReminderNotificationResponseHandler? _responseHandler; static Future _pendingStorageLock = Future.value(); static void Function(ReminderPayload)? onNotificationPayloadReceived; + static AppPreferences? _appPreferences; + + static Future _resolvePrefs() async { + final prefs = _appPreferences; + if (prefs != null) { + return prefs; + } + final sharedPreferences = await SharedPreferences.getInstance(); + final fallback = AppPreferences(sharedPreferences); + _appPreferences = fallback; + return fallback; + } + + static void setAppPreferences(AppPreferences prefs) { + _appPreferences = prefs; + } @visibleForTesting static Future resetForTest() async { _responseHandler = null; _pendingStorageLock = Future.value(); - final prefs = await SharedPreferences.getInstance(); - await prefs.remove(_pendingKey); + final prefs = await _resolvePrefs(); + await prefs.clearPendingNotifications(); } static Future bindResponseHandler( @@ -77,17 +91,19 @@ class ReminderNotificationCallbacks { static Future _enqueuePendingResponse( NotificationResponse response, ) async { + final prefs = await _resolvePrefs(); await _withPendingStorageLock(() async { - final prefs = await SharedPreferences.getInstance(); - final current = prefs.getStringList(_pendingKey) ?? const []; - final encoded = jsonEncode({ - 'id': response.id, - 'actionId': response.actionId, - 'payload': response.payload, - 'type': response.notificationResponseType.index, - 'input': response.input, - }); - await prefs.setStringList(_pendingKey, [...current, encoded]); + final current = prefs.pendingNotifications; + await prefs.setPendingNotifications([ + ...current, + NotificationResponse( + id: response.id, + actionId: response.actionId, + payload: response.payload, + input: response.input, + notificationResponseType: response.notificationResponseType, + ), + ]); }); } @@ -96,50 +112,28 @@ class ReminderNotificationCallbacks { if (handler == null) { return; } + final prefs = await _resolvePrefs(); await _withPendingStorageLock(() async { - final prefs = await SharedPreferences.getInstance(); - final pending = prefs.getStringList(_pendingKey) ?? const []; + final pending = prefs.pendingNotifications; if (pending.isEmpty) { return; } - final remaining = []; - for (final raw in pending) { - Map parsed; + final remaining = []; + for (final response in pending) { try { - parsed = Map.from(jsonDecode(raw) as Map); + await handler(response); } catch (_) { - continue; - } - - final id = parsed['id'] as int?; - final actionId = parsed['actionId'] as String?; - final payload = parsed['payload'] as String?; - final typeIndex = (parsed['type'] as int?) ?? 0; - final input = parsed['input'] as String?; - final type = NotificationResponseType.values[typeIndex.clamp(0, 1)]; - - try { - await handler( - NotificationResponse( - id: id, - actionId: actionId, - payload: payload, - input: input, - notificationResponseType: type, - ), - ); - } catch (_) { - remaining.add(raw); + remaining.add(response); } } if (remaining.isEmpty) { - await prefs.remove(_pendingKey); + await prefs.clearPendingNotifications(); return; } - await prefs.setStringList(_pendingKey, remaining); + await prefs.setPendingNotifications(remaining); }); } } diff --git a/apps/lib/features/auth/presentation/screens/auth_boot_screen.dart b/apps/lib/features/auth/presentation/screens/auth_boot_screen.dart index ccf685e..4bf3154 100644 --- a/apps/lib/features/auth/presentation/screens/auth_boot_screen.dart +++ b/apps/lib/features/auth/presentation/screens/auth_boot_screen.dart @@ -5,8 +5,9 @@ class AuthBootScreen extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Scaffold( - backgroundColor: const Color(0xFFEFF8FF), + backgroundColor: colorScheme.surface, body: SafeArea( child: Center( child: Image.asset( diff --git a/apps/lib/features/auth/presentation/screens/login_screen.dart b/apps/lib/features/auth/presentation/screens/login_screen.dart index 837f1ee..8ce2003 100644 --- a/apps/lib/features/auth/presentation/screens/login_screen.dart +++ b/apps/lib/features/auth/presentation/screens/login_screen.dart @@ -97,6 +97,8 @@ class _LoginViewState extends State { } Widget _buildAgreementCheckbox() { + final colorScheme = Theme.of(context).colorScheme; + return Center( child: Row( mainAxisSize: MainAxisSize.min, @@ -107,7 +109,7 @@ class _LoginViewState extends State { checked: _agreedToTerms, button: true, child: InkWell( - borderRadius: BorderRadius.circular(AppRadius.md), + borderRadius: BorderRadius.circular(AppRadius.sm), onTap: () => setState(() => _agreedToTerms = !_agreedToTerms), child: SizedBox( width: 44, @@ -119,21 +121,21 @@ class _LoginViewState extends State { margin: const EdgeInsets.only(right: AppSpacing.sm), decoration: BoxDecoration( color: _agreedToTerms - ? AppColors.blue600 - : Colors.transparent, + ? colorScheme.primary + : colorScheme.surface.withValues(alpha: 0), borderRadius: BorderRadius.circular(4), border: Border.all( color: _agreedToTerms - ? AppColors.blue600 - : AppColors.slate400, + ? colorScheme.primary + : colorScheme.onSurfaceVariant, width: 1.5, ), ), child: _agreedToTerms - ? const Icon( + ? Icon( Icons.check, size: 14, - color: AppColors.white, + color: colorScheme.onPrimary, ) : null, ), @@ -143,21 +145,24 @@ class _LoginViewState extends State { ), RichText( text: TextSpan( - style: const TextStyle(fontSize: 13, color: AppColors.slate600), + style: TextStyle( + fontSize: 13, + color: colorScheme.onSurfaceVariant, + ), children: [ TextSpan(text: context.l10n.authAgreementPrefix), TextSpan( text: context.l10n.authAgreementTerms, - style: const TextStyle( - color: AppColors.blue600, + style: TextStyle( + color: colorScheme.primary, decoration: TextDecoration.underline, ), ), TextSpan(text: context.l10n.authAgreementAnd), TextSpan( text: context.l10n.authAgreementPrivacy, - style: const TextStyle( - color: AppColors.blue600, + style: TextStyle( + color: colorScheme.primary, decoration: TextDecoration.underline, ), ), diff --git a/apps/lib/features/auth/presentation/widgets/auth_field.dart b/apps/lib/features/auth/presentation/widgets/auth_field.dart index af9c9f9..aa5c6bb 100644 --- a/apps/lib/features/auth/presentation/widgets/auth_field.dart +++ b/apps/lib/features/auth/presentation/widgets/auth_field.dart @@ -29,16 +29,18 @@ class AuthField extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (label != null) ...[ Text( label!, - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, - color: AppColors.slate700, + color: colorScheme.onSurface, ), ), SizedBox(height: AppSpacing.sm), @@ -52,15 +54,15 @@ class AuthField extends StatelessWidget { obscureText: obscureText, onChanged: onChanged, inputFormatters: inputFormatters, - style: const TextStyle(fontSize: 16, color: AppColors.slate900), + style: TextStyle(fontSize: 16, color: colorScheme.onSurface), decoration: InputDecoration( hintText: hint, - hintStyle: const TextStyle( + hintStyle: TextStyle( fontSize: 15, - color: AppColors.slate400, + color: colorScheme.onSurfaceVariant, ), filled: true, - fillColor: AppColors.authInputBackground, + fillColor: colorScheme.surface, contentPadding: const EdgeInsets.symmetric( horizontal: AppSpacing.lg, vertical: AppSpacing.lg, @@ -73,11 +75,11 @@ class AuthField extends StatelessWidget { ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.lg), - borderSide: const BorderSide(color: AppColors.authInputBorder), + borderSide: BorderSide(color: colorScheme.outlineVariant), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.lg), - borderSide: const BorderSide(color: AppColors.authInputFocus), + borderSide: BorderSide(color: colorScheme.primary), ), ), ), diff --git a/apps/lib/features/auth/presentation/widgets/auth_page_scaffold.dart b/apps/lib/features/auth/presentation/widgets/auth_page_scaffold.dart index 65de6c7..55cb0af 100644 --- a/apps/lib/features/auth/presentation/widgets/auth_page_scaffold.dart +++ b/apps/lib/features/auth/presentation/widgets/auth_page_scaffold.dart @@ -21,24 +21,26 @@ class AuthPageScaffold extends StatelessWidget { @override Widget build(BuildContext context) { final keyboardInset = MediaQuery.viewInsetsOf(context).bottom; + final colorScheme = Theme.of(context).colorScheme; return Scaffold( - backgroundColor: AppColors.authBackgroundBottom, + backgroundColor: colorScheme.surface, resizeToAvoidBottomInset: resizeOnKeyboard, body: DecoratedBox( - decoration: const BoxDecoration( + decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [ - AppColors.authBackgroundTop, - AppColors.authBackgroundBottom, - ], + colors: [colorScheme.surfaceContainerLow, colorScheme.surface], ), ), child: Stack( children: [ - const _AuthBackgroundOrbs(), + _AuthBackgroundOrbs( + topColor: colorScheme.primary.withValues(alpha: 0.2), + rightColor: colorScheme.primaryContainer.withValues(alpha: 0.25), + bottomColor: colorScheme.tertiaryContainer.withValues(alpha: 0.3), + ), SafeArea( maintainBottomViewPadding: !resizeOnKeyboard, child: LayoutBuilder( @@ -122,6 +124,8 @@ class AuthHeroHeader extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -130,12 +134,12 @@ class AuthHeroHeader extends StatelessWidget { width: 88, height: 88, decoration: BoxDecoration( - color: AppColors.appIconRing, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.full), - border: Border.all(color: AppColors.appIconBorder), + border: Border.all(color: colorScheme.outlineVariant), boxShadow: [ BoxShadow( - color: AppColors.blue300.withValues(alpha: 0.28), + color: colorScheme.primary.withValues(alpha: 0.28), blurRadius: 30, offset: const Offset(0, 16), ), @@ -154,14 +158,14 @@ class AuthHeroHeader extends StatelessWidget { ), ), SizedBox(height: AppSpacing.lg), - const Text( + Text( 'linksy', style: TextStyle( fontFamily: 'Playfair Display', fontSize: 34, fontWeight: FontWeight.w700, fontStyle: FontStyle.italic, - color: AppColors.appTitle, + color: colorScheme.onSurface, letterSpacing: 0.4, ), ), @@ -171,10 +175,10 @@ class AuthHeroHeader extends StatelessWidget { Text( title!, textAlign: TextAlign.center, - style: const TextStyle( + style: TextStyle( fontSize: 28, fontWeight: FontWeight.w700, - color: AppColors.slate900, + color: colorScheme.onSurface, letterSpacing: -0.2, ), ), @@ -184,10 +188,10 @@ class AuthHeroHeader extends StatelessWidget { Text( subtitle!, textAlign: TextAlign.center, - style: const TextStyle( + style: TextStyle( fontSize: 14, height: 1.45, - color: AppColors.authLinkMuted, + color: colorScheme.onSurfaceVariant, ), ), ], @@ -203,21 +207,23 @@ class AuthSurfaceCard extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Container( width: double.infinity, padding: const EdgeInsets.all(AppSpacing.xl), decoration: BoxDecoration( - color: AppColors.authCardBackground, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.xxl), - border: Border.all(color: AppColors.authCardBorder), + border: Border.all(color: colorScheme.outlineVariant), boxShadow: [ BoxShadow( - color: AppColors.blue200.withValues(alpha: 0.18), + color: colorScheme.primary.withValues(alpha: 0.12), blurRadius: 34, offset: const Offset(0, 18), ), BoxShadow( - color: AppColors.slate900.withValues(alpha: 0.06), + color: colorScheme.shadow.withValues(alpha: 0.06), blurRadius: 20, offset: const Offset(0, 10), ), @@ -242,26 +248,28 @@ class AuthSection extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (title != null) ...[ Text( title!, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w700, - color: AppColors.slate800, + color: colorScheme.onSurface, ), ), if (description != null) ...[ SizedBox(height: AppSpacing.xs), Text( description!, - style: const TextStyle( + style: TextStyle( fontSize: 13, height: 1.4, - color: AppColors.authLinkMuted, + color: colorScheme.onSurfaceVariant, ), ), ], @@ -274,7 +282,15 @@ class AuthSection extends StatelessWidget { } class _AuthBackgroundOrbs extends StatelessWidget { - const _AuthBackgroundOrbs(); + const _AuthBackgroundOrbs({ + required this.topColor, + required this.rightColor, + required this.bottomColor, + }); + + final Color topColor; + final Color rightColor; + final Color bottomColor; @override Widget build(BuildContext context) { @@ -284,26 +300,17 @@ class _AuthBackgroundOrbs extends StatelessWidget { Positioned( top: -72, left: -38, - child: _Orb( - size: 168, - color: AppColors.authBackgroundOrb.withValues(alpha: 0.42), - ), + child: _Orb(size: 168, color: topColor), ), Positioned( top: 108, right: -32, - child: _Orb( - size: 120, - color: AppColors.blue100.withValues(alpha: 0.32), - ), + child: _Orb(size: 120, color: rightColor), ), Positioned( bottom: 36, left: 24, - child: _Orb( - size: 92, - color: AppColors.blue50.withValues(alpha: 0.7), - ), + child: _Orb(size: 92, color: bottomColor), ), ], ), diff --git a/apps/lib/features/auth/presentation/widgets/password_field.dart b/apps/lib/features/auth/presentation/widgets/password_field.dart index 9427c45..b910061 100644 --- a/apps/lib/features/auth/presentation/widgets/password_field.dart +++ b/apps/lib/features/auth/presentation/widgets/password_field.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import '../../../../core/l10n/l10n.dart'; -import '../../../../core/theme/design_tokens.dart'; import 'auth_field.dart'; class PasswordField extends StatefulWidget { @@ -33,6 +32,8 @@ class _PasswordFieldState extends State { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return AuthField( label: widget.label, hint: widget.hint, @@ -46,7 +47,7 @@ class _PasswordFieldState extends State { : context.l10n.authHidePassword, icon: Icon( _obscured ? Icons.visibility_off_rounded : Icons.visibility_rounded, - color: AppColors.authInputIcon, + color: colorScheme.onSurfaceVariant, ), ), ); diff --git a/apps/lib/features/calendar/data/calendar_api.dart b/apps/lib/features/calendar/data/calendar_api.dart index 16b2bb2..209da7f 100644 --- a/apps/lib/features/calendar/data/calendar_api.dart +++ b/apps/lib/features/calendar/data/calendar_api.dart @@ -1,6 +1,5 @@ import 'package:social_app/core/network/i_api_client.dart'; - -import 'models/schedule_item_model.dart'; +import 'package:social_app/data/repositories/models/schedule_item_model.dart'; class CalendarApi { final IApiClient _client; diff --git a/apps/lib/features/calendar/data/services/calendar_repository.dart b/apps/lib/features/calendar/data/services/calendar_repository.dart deleted file mode 100644 index 02a63dc..0000000 --- a/apps/lib/features/calendar/data/services/calendar_repository.dart +++ /dev/null @@ -1,144 +0,0 @@ -import 'dart:async'; - -import '../../../../core/cache/cache_entry.dart'; -import '../../../../core/cache/cache_policy.dart'; -import '../../../../core/cache/hybrid_cache_store.dart'; -import '../models/schedule_item_model.dart'; - -class CalendarRepository { - final HybridCacheStore store; - final CachePolicy policy; - final DateTime Function() now; - final Future> Function(DateTime date) - loadDayFromRemote; - final Future> Function(DateTime start, DateTime end) - loadMonthFromRemote; - - final Map> _refreshInFlight = >{}; - - CalendarRepository({ - required this.store, - required this.loadDayFromRemote, - required this.loadMonthFromRemote, - CachePolicy? policy, - DateTime Function()? now, - }) : policy = - policy ?? - const CachePolicy( - softTtl: Duration(minutes: 2), - hardTtl: Duration(minutes: 30), - minRefreshInterval: Duration(minutes: 1), - ), - now = now ?? DateTime.now; - - static String dayKey(DateTime date) { - final day = - '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; - return 'calendar:day:$day'; - } - - static String monthKey(DateTime date) { - return 'calendar:month:${date.year}-${date.month.toString().padLeft(2, '0')}'; - } - - Future> getDayEvents( - DateTime date, { - bool forceRefresh = false, - }) async { - final key = dayKey(date); - if (forceRefresh) { - return _refreshDayAndRead(date, key); - } - - final cached = await store.read>>(key); - if (cached == null) { - return _refreshDayAndRead(date, key); - } - - final decision = policy.evaluate(now: now(), fetchedAt: cached.fetchedAt); - if (decision.shouldRefreshInBackground) { - _refreshDayInBackground(date, key); - } - if (decision.mustBlockForNetwork || !decision.canUseCached) { - return _refreshDayAndRead(date, key); - } - return cached.value; - } - - Future> getMonthEvents( - DateTime monthStart, { - bool forceRefresh = false, - }) async { - final key = monthKey(monthStart); - if (forceRefresh) { - return _refreshMonthAndRead(monthStart, key); - } - final cached = await store.read>>(key); - if (cached == null) { - return _refreshMonthAndRead(monthStart, key); - } - final decision = policy.evaluate(now: now(), fetchedAt: cached.fetchedAt); - if (decision.shouldRefreshInBackground) { - _refreshMonthInBackground(monthStart, key); - } - if (decision.mustBlockForNetwork || !decision.canUseCached) { - return _refreshMonthAndRead(monthStart, key); - } - return cached.value; - } - - Future> _refreshDayAndRead( - DateTime date, - String key, - ) async { - await _refreshDay(date, key); - final cached = await store.read>>(key); - return cached?.value ?? const []; - } - - Future> _refreshMonthAndRead( - DateTime monthStart, - String key, - ) async { - await _refreshMonth(monthStart, key); - final cached = await store.read>>(key); - return cached?.value ?? const []; - } - - Future _refreshDay(DateTime date, String key) async { - final remote = await loadDayFromRemote(date); - await store.write>>( - key, - CacheEntry>(value: remote, fetchedAt: now()), - ); - } - - Future _refreshMonth(DateTime monthStart, String key) async { - final start = DateTime(monthStart.year, monthStart.month, 1); - final end = DateTime(monthStart.year, monthStart.month + 1, 0, 23, 59, 59); - final remote = await loadMonthFromRemote(start, end); - await store.write>>( - key, - CacheEntry>(value: remote, fetchedAt: now()), - ); - } - - void _refreshDayInBackground(DateTime date, String key) { - _refreshInBackground(key, () => _refreshDay(date, key)); - } - - void _refreshMonthInBackground(DateTime monthStart, String key) { - _refreshInBackground(key, () => _refreshMonth(monthStart, key)); - } - - void _refreshInBackground(String key, Future Function() taskFactory) { - if (_refreshInFlight.containsKey(key)) { - return; - } - final task = taskFactory().whenComplete(() { - _refreshInFlight.remove(key); - }); - _refreshInFlight[key] = task; - unawaited(task); - } -} diff --git a/apps/lib/features/calendar/data/services/calendar_service.dart b/apps/lib/features/calendar/data/services/calendar_service.dart deleted file mode 100644 index 61fade5..0000000 --- a/apps/lib/features/calendar/data/services/calendar_service.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'package:social_app/core/network/i_api_client.dart'; -import 'package:social_app/core/cache/cache_invalidator.dart'; -import 'package:social_app/app/di/injection.dart'; - -import '../calendar_api.dart'; -import '../models/schedule_item_model.dart'; - -class CalendarService { - final IApiClient _apiClient; - CalendarApi? _calendarApi; - - CalendarService({required IApiClient apiClient}) : _apiClient = apiClient; - - CalendarApi get _api { - final api = _calendarApi; - if (api != null) { - return api; - } - final created = CalendarApi(_apiClient); - _calendarApi = created; - return created; - } - - Future> getEventsForDay(DateTime date) async { - final start = DateTime(date.year, date.month, date.day); - final end = DateTime(date.year, date.month, date.day, 23, 59, 59); - return getEventsForRange(start, end); - } - - Future> getEventsForRange( - DateTime start, - DateTime end, - ) async { - return _api.listByRange(startAt: start, endAt: end); - } - - Future getEventById(String id) async { - return _api.getById(id); - } - - Future addEvent(ScheduleItemModel event) async { - final created = await _api.create(event); - _invalidateEventCache(created); - return created; - } - - Future updateEvent(ScheduleItemModel event) async { - final updated = await _api.update(event); - _invalidateEventCache(updated); - return updated; - } - - Future archiveEvent(String id) async { - final event = await getEventById(id); - if (event == null) { - return null; - } - final updatedEvent = await updateEvent( - event.copyWith(status: ScheduleStatus.archived), - ); - _invalidateEventCache(updatedEvent); - return updatedEvent; - } - - void _invalidateEventCache(ScheduleItemModel event) { - try { - final invalidator = sl(); - var current = DateTime( - event.startAt.year, - event.startAt.month, - event.startAt.day, - ); - final end = DateTime( - event.endAt?.year ?? event.startAt.year, - event.endAt?.month ?? event.startAt.month, - event.endAt?.day ?? event.startAt.day, - ); - while (!current.isAfter(end)) { - invalidator.invalidateCalendarDay(current); - current = current.add(const Duration(days: 1)); - } - } catch (_) {} - } - - Future deleteEvent(String id) async { - final event = await getEventById(id); - if (event != null) { - _invalidateEventCache(event); - } - await _api.delete(id); - } -} diff --git a/apps/lib/features/calendar/presentation/dayweek/day_event_layout_engine.dart b/apps/lib/features/calendar/presentation/dayweek/day_event_layout_engine.dart index d2e8ae2..22dbf56 100644 --- a/apps/lib/features/calendar/presentation/dayweek/day_event_layout_engine.dart +++ b/apps/lib/features/calendar/presentation/dayweek/day_event_layout_engine.dart @@ -1,4 +1,4 @@ -import '../../data/models/schedule_item_model.dart'; +import '../../../../data/repositories/models/schedule_item_model.dart'; import 'day_timeline_metrics.dart'; import 'day_view_scale.dart'; diff --git a/apps/lib/features/calendar/presentation/screens/calendar_dayweek_screen.dart b/apps/lib/features/calendar/presentation/screens/calendar_dayweek_screen.dart index bd1510f..e55e6db 100644 --- a/apps/lib/features/calendar/presentation/screens/calendar_dayweek_screen.dart +++ b/apps/lib/features/calendar/presentation/screens/calendar_dayweek_screen.dart @@ -2,21 +2,21 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../../../../app/router/app_routes.dart'; -import '../../../home/presentation/navigation/home_return_policy.dart'; +import '../../../../app/router/home_return_policy.dart'; import '../../../../app/di/injection.dart'; import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; +import '../../../../data/repositories/calendar_repository.dart'; import '../../../../shared/widgets/app_pressable.dart'; -import '../../data/models/schedule_item_model.dart'; -import '../../data/services/calendar_repository.dart'; -import '../calendar_state_manager.dart'; +import '../../../../shared/widgets/bottom_dock.dart'; +import '../../../../shared/state/calendar_state_manager.dart'; +import '../../../../data/repositories/models/schedule_item_model.dart'; import '../calendar_time_utils.dart'; import '../utils/event_color_resolver.dart'; import '../dayweek/day_event_layout_engine.dart'; import '../dayweek/day_timeline_metrics.dart'; import '../dayweek/day_view_scale.dart'; -import '../widgets/bottom_dock.dart'; class CalendarDayWeekScreen extends StatefulWidget { final DateTime? initialDate; @@ -52,6 +52,8 @@ class _CalendarDayWeekScreenState extends State late List _monthDates; List _events = const []; + ColorScheme get _colorScheme => Theme.of(context).colorScheme; + @override void initState() { super.initState(); @@ -107,7 +109,7 @@ class _CalendarDayWeekScreenState extends State @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: AppColors.todoBg, + backgroundColor: _colorScheme.surface, body: PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) { @@ -244,18 +246,18 @@ class _CalendarDayWeekScreenState extends State height: 36, padding: const EdgeInsets.symmetric(horizontal: 10), decoration: BoxDecoration( - color: AppColors.messageBtnWrap, + color: _colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.messageBtnBorder), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Icon( + Icon( LucideIcons.chevronLeft, size: 16, - color: AppColors.slate700, + color: _colorScheme.onSurface, ), const SizedBox(width: 6), AnimatedSwitcher( @@ -277,10 +279,10 @@ class _CalendarDayWeekScreenState extends State child: Text( monthLabel, key: ValueKey(monthLabel), - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: AppColors.slate700, + color: _colorScheme.onSurface, ), ), ), @@ -297,17 +299,17 @@ class _CalendarDayWeekScreenState extends State height: 36, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( - color: AppColors.messageBtnWrap, + color: _colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.messageBtnBorder), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Center( child: Text( context.l10n.calendarToday, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: AppColors.slate700, + color: _colorScheme.onSurface, ), ), ), @@ -328,13 +330,13 @@ class _CalendarDayWeekScreenState extends State width: 36, height: 36, decoration: BoxDecoration( - color: AppColors.blue600, + color: _colorScheme.primary, borderRadius: BorderRadius.circular(AppRadius.full), ), - child: const Icon( + child: Icon( LucideIcons.plus, size: 20, - color: AppColors.white, + color: _colorScheme.onPrimary, ), ), ), @@ -454,7 +456,9 @@ class _CalendarDayWeekScreenState extends State _weekdayLabel(date), style: TextStyle( fontSize: 11, - color: isWeekend ? AppColors.slate400 : AppColors.slate600, + color: isWeekend + ? _colorScheme.onSurfaceVariant + : _colorScheme.onSurface, ), ), const SizedBox(height: 2), @@ -464,7 +468,9 @@ class _CalendarDayWeekScreenState extends State width: 32, height: 32, decoration: BoxDecoration( - color: isSelected ? AppColors.blue100 : Colors.transparent, + color: isSelected + ? _colorScheme.primaryContainer + : _colorScheme.surface.withValues(alpha: 0), borderRadius: BorderRadius.circular(AppRadius.full), ), child: Center( @@ -474,8 +480,10 @@ class _CalendarDayWeekScreenState extends State fontSize: 17, fontWeight: isSelected ? FontWeight.w700 : FontWeight.w600, color: isSelected - ? AppColors.blue600 - : (isWeekend ? AppColors.slate400 : AppColors.slate900), + ? _colorScheme.primary + : (isWeekend + ? _colorScheme.onSurfaceVariant + : _colorScheme.onSurface), ), ), ), @@ -572,7 +580,7 @@ class _CalendarDayWeekScreenState extends State top: adjustedY, left: eventAreaLeft, right: 0, - child: Container(height: 1, color: AppColors.border), + child: Container(height: 1, color: _colorScheme.outlineVariant), ), Positioned( top: labelTop, @@ -584,7 +592,7 @@ class _CalendarDayWeekScreenState extends State style: TextStyle( fontSize: 10, fontWeight: FontWeight.w600, - color: AppColors.slate400, + color: _colorScheme.onSurfaceVariant, ), ), ), @@ -613,16 +621,16 @@ class _CalendarDayWeekScreenState extends State width: DayTimelineMetrics.timeLabelWidth, height: 18, decoration: BoxDecoration( - color: AppColors.red500, + color: _colorScheme.error, borderRadius: BorderRadius.circular(9), ), child: Center( child: Text( formatHm(now), - style: const TextStyle( + style: TextStyle( fontSize: 10, fontWeight: FontWeight.w700, - color: Colors.white, + color: _colorScheme.onError, ), ), ), @@ -632,7 +640,7 @@ class _CalendarDayWeekScreenState extends State child: Container( height: 2, decoration: BoxDecoration( - color: AppColors.red500, + color: _colorScheme.error, borderRadius: BorderRadius.circular(99), ), ), @@ -651,7 +659,7 @@ class _CalendarDayWeekScreenState extends State final isArchived = layout.event.status == ScheduleStatus.archived; Color eventColor; if (isArchived) { - eventColor = AppColors.slate400; + eventColor = _colorScheme.onSurfaceVariant; } else { eventColor = resolveEventColor( status: layout.event.status, diff --git a/apps/lib/features/calendar/presentation/screens/calendar_event_create_screen.dart b/apps/lib/features/calendar/presentation/screens/calendar_event_create_screen.dart index 10e08ea..b0099d4 100644 --- a/apps/lib/features/calendar/presentation/screens/calendar_event_create_screen.dart +++ b/apps/lib/features/calendar/presentation/screens/calendar_event_create_screen.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import '../../../../core/theme/design_tokens.dart'; import '../widgets/create_event_sheet.dart'; class CalendarEventCreateScreen extends StatelessWidget { @@ -10,8 +9,9 @@ class CalendarEventCreateScreen extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Scaffold( - backgroundColor: AppColors.background, + backgroundColor: colorScheme.surface, body: SafeArea( child: CreateEventSheet(initialDate: initialDate, pageMode: true), ), diff --git a/apps/lib/features/calendar/presentation/screens/calendar_event_detail_screen.dart b/apps/lib/features/calendar/presentation/screens/calendar_event_detail_screen.dart index f29bd15..0c579bb 100644 --- a/apps/lib/features/calendar/presentation/screens/calendar_event_detail_screen.dart +++ b/apps/lib/features/calendar/presentation/screens/calendar_event_detail_screen.dart @@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:social_app/core/l10n/l10n.dart'; import '../../../../app/di/injection.dart'; import '../../../../app/router/app_routes.dart'; -import '../../../../features/notification/data/services/local_notification_service.dart'; +import '../../../../data/services/local_notification_service.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/back_title_page_header.dart'; @@ -12,8 +12,8 @@ import '../../../../shared/widgets/detail_header_action_menu.dart'; import '../../../../shared/widgets/destructive_action_sheet.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; -import '../../data/services/calendar_service.dart'; -import '../../data/models/schedule_item_model.dart'; +import '../../../../data/services/calendar_service.dart'; +import '../../../../data/repositories/models/schedule_item_model.dart'; import '../utils/event_color_resolver.dart'; enum _CalendarHeaderAction { edit, delete, share, archive } @@ -32,6 +32,8 @@ class _CalendarEventDetailScreenState extends State { ScheduleItemModel? _event; bool _loading = true; + ColorScheme get _colorScheme => Theme.of(context).colorScheme; + @override void initState() { super.initState(); @@ -60,14 +62,17 @@ class _CalendarEventDetailScreenState extends State { } if (_event == null) { return Scaffold( - backgroundColor: AppColors.background, + backgroundColor: _colorScheme.surface, body: SafeArea( child: Container( - decoration: const BoxDecoration( + decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [AppColors.homeBackgroundTop, AppColors.background], + colors: [ + _colorScheme.surfaceContainerLow, + _colorScheme.surface, + ], ), ), child: Column( @@ -89,7 +94,7 @@ class _CalendarEventDetailScreenState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: AppColors.slate700, + color: _colorScheme.onSurface, ), ), const SizedBox(height: AppSpacing.sm), @@ -98,7 +103,7 @@ class _CalendarEventDetailScreenState extends State { textAlign: TextAlign.center, style: TextStyle( fontSize: 13, - color: AppColors.slate500, + color: _colorScheme.onSurfaceVariant, ), ), ], @@ -115,14 +120,14 @@ class _CalendarEventDetailScreenState extends State { final event = _event!; return Scaffold( - backgroundColor: AppColors.background, + backgroundColor: _colorScheme.surface, body: SafeArea( child: Container( - decoration: const BoxDecoration( + decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [AppColors.homeBackgroundTop, AppColors.background], + colors: [_colorScheme.surfaceContainerLow, _colorScheme.surface], ), ), child: Column( @@ -255,12 +260,12 @@ class _CalendarEventDetailScreenState extends State { return Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( - color: AppColors.white, + color: _colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.borderTertiary), + border: Border.all(color: _colorScheme.outlineVariant), boxShadow: [ BoxShadow( - color: AppColors.slate200.withValues(alpha: 0.38), + color: _colorScheme.shadow.withValues(alpha: 0.18), blurRadius: AppRadius.lg, offset: const Offset(0, AppSpacing.xs), ), @@ -289,10 +294,10 @@ class _CalendarEventDetailScreenState extends State { event.title, maxLines: 2, overflow: TextOverflow.ellipsis, - style: const TextStyle( + style: TextStyle( fontSize: 20, fontWeight: FontWeight.w700, - color: AppColors.slate900, + color: _colorScheme.onSurface, ), ), const SizedBox(height: AppSpacing.xs), @@ -307,9 +312,9 @@ class _CalendarEventDetailScreenState extends State { width: double.infinity, padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.surfaceInfoLight, + color: _colorScheme.primaryContainer, borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all(color: AppColors.borderQuaternary), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -319,7 +324,7 @@ class _CalendarEventDetailScreenState extends State { style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, - color: AppColors.slate500, + color: _colorScheme.onSurfaceVariant, ), ), const SizedBox(height: AppSpacing.xs), @@ -329,7 +334,7 @@ class _CalendarEventDetailScreenState extends State { style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, - color: AppColors.slate800, + color: _colorScheme.onSurface, ), ), ], @@ -356,9 +361,9 @@ class _CalendarEventDetailScreenState extends State { return Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( - color: AppColors.white, + color: _colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -368,7 +373,7 @@ class _CalendarEventDetailScreenState extends State { style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, - color: AppColors.slate500, + color: _colorScheme.onSurfaceVariant, ), ), const SizedBox(height: AppSpacing.md), @@ -386,10 +391,10 @@ class _CalendarEventDetailScreenState extends State { width: AppSpacing.xxl * 3, child: Text( context.l10n.calendarDetailColor, - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, - color: AppColors.slate500, + color: _colorScheme.onSurfaceVariant, ), ), ), @@ -399,7 +404,7 @@ class _CalendarEventDetailScreenState extends State { decoration: BoxDecoration( color: color, shape: BoxShape.circle, - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: _colorScheme.outlineVariant), ), ), ], @@ -419,9 +424,9 @@ class _CalendarEventDetailScreenState extends State { return Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: _colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -431,7 +436,7 @@ class _CalendarEventDetailScreenState extends State { style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, - color: AppColors.slate500, + color: _colorScheme.onSurfaceVariant, ), ), if (event.metadata?.location?.trim().isNotEmpty ?? false) ...[ @@ -570,13 +575,11 @@ class _CalendarEventDetailScreenState extends State { ), decoration: BoxDecoration( color: isArchived - ? AppColors.feedbackWarningSurface - : AppColors.feedbackSuccessSurface, + ? _colorScheme.errorContainer + : _colorScheme.tertiaryContainer, borderRadius: BorderRadius.circular(AppRadius.full), border: Border.all( - color: isArchived - ? AppColors.feedbackWarningBorder - : AppColors.feedbackSuccessBorder, + color: isArchived ? _colorScheme.error : _colorScheme.tertiary, ), ), child: Text( @@ -587,8 +590,8 @@ class _CalendarEventDetailScreenState extends State { fontSize: 12, fontWeight: FontWeight.w700, color: isArchived - ? AppColors.feedbackWarningText - : AppColors.feedbackSuccessText, + ? _colorScheme.onErrorContainer + : _colorScheme.onTertiaryContainer, ), ), ); @@ -604,10 +607,10 @@ class _CalendarEventDetailScreenState extends State { width: AppSpacing.xxl * 3, child: Text( label, - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, - color: AppColors.slate500, + color: _colorScheme.onSurfaceVariant, ), ), ), @@ -619,7 +622,7 @@ class _CalendarEventDetailScreenState extends State { fontSize: 14, height: multiline ? 1.4 : 1.0, fontWeight: FontWeight.w600, - color: AppColors.slate800, + color: _colorScheme.onSurface, ), ), ), diff --git a/apps/lib/features/calendar/presentation/screens/calendar_event_edit_screen.dart b/apps/lib/features/calendar/presentation/screens/calendar_event_edit_screen.dart index 644c285..d19ec5f 100644 --- a/apps/lib/features/calendar/presentation/screens/calendar_event_edit_screen.dart +++ b/apps/lib/features/calendar/presentation/screens/calendar_event_edit_screen.dart @@ -2,11 +2,10 @@ import 'package:flutter/material.dart'; import '../../../../app/di/injection.dart'; import '../../../../core/l10n/l10n.dart'; -import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../widgets/create_event_sheet.dart'; -import '../../data/models/schedule_item_model.dart'; -import '../../data/services/calendar_service.dart'; +import '../../../../data/repositories/models/schedule_item_model.dart'; +import '../../../../data/services/calendar_service.dart'; class CalendarEventEditScreen extends StatefulWidget { final String eventId; @@ -61,7 +60,7 @@ class _CalendarEventEditScreenState extends State { } return Scaffold( - backgroundColor: AppColors.background, + backgroundColor: Theme.of(context).colorScheme.surface, body: SafeArea( child: CreateEventSheet(editingEvent: _event, pageMode: true), ), diff --git a/apps/lib/features/calendar/presentation/screens/calendar_event_share_screen.dart b/apps/lib/features/calendar/presentation/screens/calendar_event_share_screen.dart index 1bed1d6..cb71cc7 100644 --- a/apps/lib/features/calendar/presentation/screens/calendar_event_share_screen.dart +++ b/apps/lib/features/calendar/presentation/screens/calendar_event_share_screen.dart @@ -2,11 +2,10 @@ import 'package:flutter/material.dart'; import '../../../../app/di/injection.dart'; import '../../../../core/l10n/l10n.dart'; -import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/back_title_page_header.dart'; -import '../../data/models/schedule_item_model.dart'; -import '../../data/services/calendar_service.dart'; +import '../../../../data/repositories/models/schedule_item_model.dart'; +import '../../../../data/services/calendar_service.dart'; import '../widgets/calendar_share_dialog.dart'; class CalendarEventShareScreen extends StatefulWidget { @@ -63,7 +62,7 @@ class _CalendarEventShareScreenState extends State { } return Scaffold( - backgroundColor: AppColors.background, + backgroundColor: Theme.of(context).colorScheme.surface, body: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/apps/lib/features/calendar/presentation/screens/calendar_month_screen.dart b/apps/lib/features/calendar/presentation/screens/calendar_month_screen.dart index 9f298ee..710f209 100644 --- a/apps/lib/features/calendar/presentation/screens/calendar_month_screen.dart +++ b/apps/lib/features/calendar/presentation/screens/calendar_month_screen.dart @@ -6,14 +6,14 @@ import 'package:social_app/core/l10n/l10n.dart'; import '../../../../app/di/injection.dart'; import '../../../../app/router/app_routes.dart'; import '../../../../core/theme/design_tokens.dart'; +import '../../../../data/repositories/calendar_repository.dart'; import '../../../../shared/widgets/app_pressable.dart'; -import '../../../home/presentation/navigation/home_return_policy.dart'; -import '../calendar_state_manager.dart'; +import '../../../../shared/widgets/bottom_dock.dart'; +import '../../../../shared/state/calendar_state_manager.dart'; +import '../../../../app/router/home_return_policy.dart'; import '../calendar_time_utils.dart'; import '../utils/event_color_resolver.dart'; -import '../widgets/bottom_dock.dart'; -import '../../data/models/schedule_item_model.dart'; -import '../../data/services/calendar_repository.dart'; +import '../../../../data/repositories/models/schedule_item_model.dart'; class CalendarMonthScreen extends StatefulWidget { final bool resetToToday; @@ -31,6 +31,8 @@ class _CalendarMonthScreenState extends State late DateTime _selectedDate; final Map> _eventsByDay = {}; + ColorScheme get _colorScheme => Theme.of(context).colorScheme; + @override void initState() { super.initState(); @@ -79,7 +81,7 @@ class _CalendarMonthScreenState extends State @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: AppColors.todoBg, + backgroundColor: _colorScheme.surface, body: PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) { @@ -136,18 +138,18 @@ class _CalendarMonthScreenState extends State child: Text( l10n.calendarMonthHeader(_currentMonth.month), key: ValueKey(_currentMonth.month), - style: const TextStyle( + style: TextStyle( fontSize: 22, fontWeight: FontWeight.w700, - color: AppColors.slate900, + color: _colorScheme.onSurface, ), ), ), const SizedBox(width: 6), - const Icon( + Icon( LucideIcons.chevronDown, size: 16, - color: AppColors.slate900, + color: _colorScheme.onSurface, ), ], ), @@ -161,9 +163,11 @@ class _CalendarMonthScreenState extends State height: 36, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( - color: AppColors.messageBtnWrap, + color: _colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.messageBtnBorder), + border: Border.all( + color: _colorScheme.outlineVariant, + ), ), child: Center( child: Text( @@ -171,7 +175,7 @@ class _CalendarMonthScreenState extends State style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: AppColors.slate700, + color: _colorScheme.onSurface, ), ), ), @@ -192,13 +196,13 @@ class _CalendarMonthScreenState extends State width: 36, height: 36, decoration: BoxDecoration( - color: AppColors.blue600, + color: _colorScheme.primary, borderRadius: BorderRadius.circular(AppRadius.full), ), - child: const Icon( + child: Icon( LucideIcons.plus, size: 20, - color: AppColors.white, + color: _colorScheme.onPrimary, ), ), ), @@ -229,7 +233,7 @@ class _CalendarMonthScreenState extends State return Column( children: [ _buildWeekdayHeader(), - Container(height: 1, color: AppColors.border), + Container(height: 1, color: _colorScheme.outlineVariant), ..._buildWeeks(), ], ); @@ -259,10 +263,10 @@ class _CalendarMonthScreenState extends State child: Center( child: Text( day, - style: const TextStyle( + style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, - color: AppColors.slate400, + color: _colorScheme.onSurfaceVariant, ), ), ), @@ -294,7 +298,7 @@ class _CalendarMonthScreenState extends State for (var weekStart = 0; weekStart < totalCells; weekStart += 7) { weeks.add(_buildWeekRow(weekStart, startWeekday, daysInMonth)); if (weekStart + 7 < totalCells) { - weeks.add(Container(height: 1, color: AppColors.border)); + weeks.add(Container(height: 1, color: _colorScheme.outlineVariant)); } } @@ -337,7 +341,9 @@ class _CalendarMonthScreenState extends State width: 36, height: 36, decoration: BoxDecoration( - color: isSelected ? AppColors.blue100 : Colors.transparent, + color: isSelected + ? _colorScheme.primaryContainer + : _colorScheme.surface.withValues(alpha: 0), borderRadius: BorderRadius.circular(AppRadius.full), ), child: Center( @@ -349,8 +355,8 @@ class _CalendarMonthScreenState extends State ? FontWeight.w600 : FontWeight.normal, color: isSelected - ? AppColors.blue600 - : AppColors.slate900, + ? _colorScheme.primary + : _colorScheme.onSurface, ), ), ), @@ -447,9 +453,9 @@ class _CalendarMonthScreenState extends State }, child: Text( '+$remainingCount', - style: const TextStyle( + style: TextStyle( fontSize: 9, - color: AppColors.slate500, + color: _colorScheme.onSurfaceVariant, fontWeight: FontWeight.w500, ), ), @@ -469,7 +475,7 @@ class _CalendarMonthScreenState extends State showModalBottomSheet( context: context, - backgroundColor: AppColors.white, + backgroundColor: _colorScheme.surface, builder: (context) { return StatefulBuilder( builder: (context, setSheetState) { diff --git a/apps/lib/features/calendar/presentation/utils/event_color_resolver.dart b/apps/lib/features/calendar/presentation/utils/event_color_resolver.dart index 2d630e0..eed81ed 100644 --- a/apps/lib/features/calendar/presentation/utils/event_color_resolver.dart +++ b/apps/lib/features/calendar/presentation/utils/event_color_resolver.dart @@ -1,23 +1,22 @@ import 'package:flutter/material.dart'; -import '../../../../core/theme/design_tokens.dart'; -import '../../data/models/schedule_item_model.dart'; +import '../../../../data/repositories/models/schedule_item_model.dart'; Color resolveEventColor({ required ScheduleStatus status, required String? colorHex, }) { if (status == ScheduleStatus.archived) { - return AppColors.slate400; + return const Color(0xFF64748B); } if (colorHex == null || colorHex.isEmpty) { - return AppColors.blue600; + return const Color(0xFF3B82F6); } try { return Color(int.parse(colorHex.replaceFirst('#', '0xFF'))); } catch (_) { - return AppColors.blue600; + return const Color(0xFF3B82F6); } } diff --git a/apps/lib/features/calendar/presentation/widgets/calendar_share_dialog.dart b/apps/lib/features/calendar/presentation/widgets/calendar_share_dialog.dart index b0fbaf8..1b11e4b 100644 --- a/apps/lib/features/calendar/presentation/widgets/calendar_share_dialog.dart +++ b/apps/lib/features/calendar/presentation/widgets/calendar_share_dialog.dart @@ -32,7 +32,9 @@ class CalendarShareDialog extends StatefulWidget { return showModalBottomSheet( context: context, isScrollControlled: true, - backgroundColor: Colors.transparent, + backgroundColor: Theme.of( + context, + ).colorScheme.surface.withValues(alpha: 0), builder: (context) => CalendarShareDialog( eventId: eventId, eventTitle: eventTitle, @@ -108,13 +110,14 @@ class _CalendarShareDialogState extends State { @override Widget build(BuildContext context) { final l10n = context.l10n; + final colorScheme = Theme.of(context).colorScheme; return Container( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), decoration: BoxDecoration( - color: AppColors.background, + color: colorScheme.surface, borderRadius: const BorderRadius.vertical( top: Radius.circular(AppRadius.lg), ), @@ -200,6 +203,7 @@ class _CalendarShareDialogState extends State { bool value, ValueChanged? onChanged, ) { + final colorScheme = Theme.of(context).colorScheme; final enabled = onChanged != null; return Padding( padding: const EdgeInsets.symmetric(vertical: AppSpacing.xs), @@ -211,13 +215,17 @@ class _CalendarShareDialogState extends State { children: [ Text( title, - style: TextStyle(color: enabled ? null : Colors.grey), + style: TextStyle( + color: enabled + ? colorScheme.onSurface + : colorScheme.onSurfaceVariant, + ), ), Text( description, style: TextStyle( fontSize: 12, - color: enabled ? Colors.grey : Colors.grey.shade400, + color: colorScheme.onSurfaceVariant, ), ), ], diff --git a/apps/lib/features/calendar/presentation/widgets/create_event_sheet.dart b/apps/lib/features/calendar/presentation/widgets/create_event_sheet.dart index 54bfabc..d4bd961 100644 --- a/apps/lib/features/calendar/presentation/widgets/create_event_sheet.dart +++ b/apps/lib/features/calendar/presentation/widgets/create_event_sheet.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:social_app/core/l10n/l10n.dart'; import '../../../../app/di/injection.dart'; -import '../../../../features/notification/data/services/local_notification_service.dart'; +import '../../../../data/services/local_notification_service.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/app_selection_sheet.dart'; @@ -11,8 +11,10 @@ import '../../../../shared/widgets/back_title_page_header.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; import 'date_time_picker_sheet.dart'; -import '../../data/models/schedule_item_model.dart'; -import '../../data/services/calendar_service.dart'; +import '../../../../data/repositories/models/schedule_item_model.dart'; +import '../../../../data/services/calendar_service.dart'; + +final _defaultColors = AppColorPalette.light.eventPresetColors; class CreateEventSheet extends StatefulWidget { final DateTime? initialDate; @@ -36,7 +38,9 @@ class CreateEventSheet extends StatefulWidget { return showModalBottomSheet( context: context, isScrollControlled: true, - backgroundColor: Colors.transparent, + backgroundColor: Theme.of( + context, + ).colorScheme.surface.withValues(alpha: 0), builder: (context) => CreateEventSheet(initialDate: initialDate, onSaved: onSaved), ); @@ -50,7 +54,9 @@ class CreateEventSheet extends StatefulWidget { return showModalBottomSheet( context: context, isScrollControlled: true, - backgroundColor: Colors.transparent, + backgroundColor: Theme.of( + context, + ).colorScheme.surface.withValues(alpha: 0), builder: (context) => CreateEventSheet(editingEvent: event, onSaved: onSaved), ); @@ -149,12 +155,13 @@ class _CreateEventSheetState extends State @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; if (widget.pageMode) { return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () => FocusScope.of(context).unfocus(), child: Container( - color: AppColors.background, + color: colorScheme.surface, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -175,8 +182,8 @@ class _CreateEventSheetState extends State ), child: Container( height: MediaQuery.of(context).size.height * 0.85, - decoration: const BoxDecoration( - color: AppColors.white, + decoration: BoxDecoration( + color: colorScheme.surface, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( @@ -188,7 +195,7 @@ class _CreateEventSheetState extends State width: AppSpacing.xl + AppSpacing.sm, height: AppSpacing.xs, decoration: BoxDecoration( - color: AppColors.slate200, + color: colorScheme.outlineVariant, borderRadius: BorderRadius.circular(AppRadius.full), ), ), @@ -204,6 +211,7 @@ class _CreateEventSheetState extends State } Widget _buildPageHeader() { + final colorScheme = Theme.of(context).colorScheme; return BackTitlePageHeader( title: _isEditing ? context.l10n.calendarCreateEditTitle @@ -226,14 +234,16 @@ class _CreateEventSheetState extends State ? const AppLoadingIndicator( variant: AppLoadingVariant.button, size: 18, - trackColor: AppColors.blue200, + trackColor: null, ) : Text( context.l10n.commonSave, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: enabled ? AppColors.blue600 : AppColors.slate400, + color: enabled + ? colorScheme.primary + : colorScheme.outline, ), ), ), @@ -244,6 +254,7 @@ class _CreateEventSheetState extends State } Widget _buildHeader() { + final colorScheme = Theme.of(context).colorScheme; return Container( height: 56, padding: const EdgeInsets.symmetric(horizontal: 16), @@ -261,10 +272,10 @@ class _CreateEventSheetState extends State borderRadius: BorderRadius.circular(AppRadius.full), ), ), - child: const Icon( + child: Icon( LucideIcons.x, size: AppSpacing.xxl, - color: AppColors.slate700, + color: colorScheme.onSurfaceVariant, ), ), ), @@ -272,10 +283,10 @@ class _CreateEventSheetState extends State _isEditing ? context.l10n.calendarCreateEditTitle : context.l10n.calendarCreateNewTitle, - style: const TextStyle( + style: TextStyle( fontSize: 17, fontWeight: FontWeight.w600, - color: AppColors.slate900, + color: colorScheme.onSurface, ), ), ValueListenableBuilder( @@ -297,7 +308,7 @@ class _CreateEventSheetState extends State ? const AppLoadingIndicator( variant: AppLoadingVariant.button, size: 18, - trackColor: AppColors.blue200, + trackColor: null, ) : Text( context.l10n.commonSave, @@ -305,8 +316,8 @@ class _CreateEventSheetState extends State fontSize: 17, fontWeight: FontWeight.w600, color: enabled - ? AppColors.blue600 - : AppColors.slate400, + ? colorScheme.primary + : colorScheme.outline, ), ), ), @@ -319,15 +330,16 @@ class _CreateEventSheetState extends State } Widget _buildTabBar() { + final colorScheme = Theme.of(context).colorScheme; return Container( - decoration: const BoxDecoration( - border: Border(bottom: BorderSide(color: AppColors.border)), + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: colorScheme.outlineVariant)), ), child: TabBar( controller: _tabController, - labelColor: AppColors.blue600, - unselectedLabelColor: AppColors.slate600, - indicatorColor: AppColors.blue600, + labelColor: colorScheme.primary, + unselectedLabelColor: colorScheme.onSurfaceVariant, + indicatorColor: colorScheme.primary, tabs: [ Tab(text: context.l10n.calendarCreateTabBasic), Tab(text: context.l10n.calendarCreateTabAdvanced), @@ -491,15 +503,16 @@ class _CreateEventSheetState extends State bool isOptional = false, DateTime? minTime, }) { + final colorScheme = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( isOptional ? context.l10n.calendarCreateOptionalField(label) : label, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: AppColors.slate700, + color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 8), @@ -515,33 +528,33 @@ class _CreateEventSheetState extends State child: Container( padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.slate50, + color: colorScheme.surfaceContainerLowest, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: colorScheme.outlineVariant), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Icon( + Icon( LucideIcons.calendar, size: 16, - color: AppColors.slate600, + color: colorScheme.onSurfaceVariant, ), const SizedBox(width: AppSpacing.sm), Expanded( child: Text( _formatDateTimeLabel(date, time), - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, - color: AppColors.slate900, + color: colorScheme.onSurface, ), ), ), - const Icon( + Icon( LucideIcons.chevronRight, size: 16, - color: AppColors.slate400, + color: colorScheme.outline, ), ], ), @@ -568,7 +581,9 @@ class _CreateEventSheetState extends State }) async { final result = await showModalBottomSheet<(DateTime, DateTime)>( context: context, - backgroundColor: Colors.transparent, + backgroundColor: Theme.of( + context, + ).colorScheme.surface.withValues(alpha: 0), isScrollControlled: true, builder: (context) => DateTimePickerSheet( initialDate: date, @@ -580,6 +595,7 @@ class _CreateEventSheetState extends State } Widget _buildColorPicker() { + final colorScheme = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -588,15 +604,19 @@ class _CreateEventSheetState extends State style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: AppColors.slate700, + color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 8), Row( - children: defaultColors.map((color) { + children: _defaultColors.map((color) { final colorHex = '#${color.toARGB32().toRadixString(16).substring(2).toUpperCase()}'; final isSelected = _selectedColor == colorHex; + final checkColor = + ThemeData.estimateBrightnessForColor(color) == Brightness.dark + ? Colors.white + : Colors.black; return GestureDetector( onTap: () => setState(() => _selectedColor = colorHex), child: Container( @@ -607,11 +627,11 @@ class _CreateEventSheetState extends State color: color, shape: BoxShape.circle, border: isSelected - ? Border.all(color: AppColors.slate900, width: 2) + ? Border.all(color: colorScheme.onSurface, width: 2) : null, ), child: isSelected - ? const Icon(Icons.check, size: 16, color: Colors.white) + ? Icon(Icons.check, size: 16, color: checkColor) : null, ), ); @@ -622,6 +642,7 @@ class _CreateEventSheetState extends State } Widget _buildReminderPicker() { + final colorScheme = Theme.of(context).colorScheme; String labelOf(int? value) { if (value == null) { return context.l10n.calendarCreateReminderNone; @@ -640,7 +661,7 @@ class _CreateEventSheetState extends State style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: AppColors.slate700, + color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 8), @@ -666,9 +687,9 @@ class _CreateEventSheetState extends State width: double.infinity, padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.slate50, + color: colorScheme.surfaceContainerLowest, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: colorScheme.outlineVariant), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -676,17 +697,17 @@ class _CreateEventSheetState extends State Expanded( child: Text( labelOf(_reminderMinutes), - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, - color: AppColors.slate900, + color: colorScheme.onSurface, ), ), ), - const Icon( + Icon( LucideIcons.chevronRight, size: 16, - color: AppColors.slate400, + color: colorScheme.outline, ), ], ), diff --git a/apps/lib/features/calendar/presentation/widgets/date_time_picker_sheet.dart b/apps/lib/features/calendar/presentation/widgets/date_time_picker_sheet.dart index d8eef02..80e611c 100644 --- a/apps/lib/features/calendar/presentation/widgets/date_time_picker_sheet.dart +++ b/apps/lib/features/calendar/presentation/widgets/date_time_picker_sheet.dart @@ -1,8 +1,7 @@ import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:social_app/core/l10n/l10n.dart'; -import '../../../../core/theme/design_tokens.dart'; - class DateTimePickerSheet extends StatefulWidget { final DateTime initialDate; final DateTime initialTime; @@ -132,11 +131,12 @@ class _DateTimePickerSheetState extends State { @override Widget build(BuildContext context) { final l10n = context.l10n; + final colorScheme = Theme.of(context).colorScheme; return Container( height: 420, - decoration: const BoxDecoration( - color: AppColors.white, + decoration: BoxDecoration( + color: colorScheme.surface, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( @@ -172,7 +172,7 @@ class _DateTimePickerSheetState extends State { l10n.calendarDateTimePickerYearUnit, style: TextStyle( fontSize: 14, - color: AppColors.slate600, + color: colorScheme.onSurfaceVariant, ), ), Expanded( @@ -193,7 +193,7 @@ class _DateTimePickerSheetState extends State { l10n.calendarDateTimePickerMonthUnit, style: TextStyle( fontSize: 14, - color: AppColors.slate600, + color: colorScheme.onSurfaceVariant, ), ), Expanded( @@ -208,7 +208,7 @@ class _DateTimePickerSheetState extends State { l10n.calendarDateTimePickerDayUnit, style: TextStyle( fontSize: 14, - color: AppColors.slate600, + color: colorScheme.onSurfaceVariant, ), ), ], @@ -217,7 +217,11 @@ class _DateTimePickerSheetState extends State { ], ), ), - Container(width: 1, height: 180, color: AppColors.border), + Container( + width: 1, + height: 180, + color: colorScheme.outlineVariant, + ), Expanded( flex: 2, child: Column( @@ -256,12 +260,12 @@ class _DateTimePickerSheetState extends State { itemExtent: 50, ), ), - const Text( + Text( ' : ', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, - color: AppColors.slate600, + color: colorScheme.onSurfaceVariant, ), ), Expanded( @@ -289,12 +293,13 @@ class _DateTimePickerSheetState extends State { Widget _buildHeader() { final l10n = context.l10n; + final colorScheme = Theme.of(context).colorScheme; return Container( height: 56, padding: const EdgeInsets.symmetric(horizontal: 16), - decoration: const BoxDecoration( - border: Border(bottom: BorderSide(color: AppColors.border)), + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: colorScheme.outlineVariant)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -304,7 +309,10 @@ class _DateTimePickerSheetState extends State { onTap: () => Navigator.pop(context), child: Text( l10n.commonCancel, - style: const TextStyle(fontSize: 17, color: AppColors.slate600), + style: TextStyle( + fontSize: 17, + color: colorScheme.onSurfaceVariant, + ), ), ), Text( @@ -312,7 +320,7 @@ class _DateTimePickerSheetState extends State { style: TextStyle( fontSize: 17, fontWeight: FontWeight.w600, - color: AppColors.slate900, + color: colorScheme.onSurface, ), ), GestureDetector( @@ -324,10 +332,10 @@ class _DateTimePickerSheetState extends State { }, child: Text( l10n.commonConfirm, - style: const TextStyle( + style: TextStyle( fontSize: 17, fontWeight: FontWeight.w600, - color: AppColors.blue600, + color: colorScheme.primary, ), ), ), @@ -337,14 +345,15 @@ class _DateTimePickerSheetState extends State { } Widget _buildPickerLabel(String label) { + final colorScheme = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: Text( label, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: AppColors.slate700, + color: colorScheme.onSurfaceVariant, ), textAlign: TextAlign.center, ), @@ -358,6 +367,7 @@ class _DateTimePickerSheetState extends State { String Function(int) formatter, { double itemExtent = 40, }) { + final colorScheme = Theme.of(context).colorScheme; return CupertinoPicker( scrollController: controller, itemExtent: itemExtent, @@ -369,7 +379,7 @@ class _DateTimePickerSheetState extends State { decoration: BoxDecoration( border: Border.symmetric( horizontal: BorderSide( - color: AppColors.blue100.withValues(alpha: 0.5), + color: colorScheme.primary.withValues(alpha: 0.3), width: 1, ), ), @@ -379,7 +389,7 @@ class _DateTimePickerSheetState extends State { return Center( child: Text( formatter(items[index]), - style: const TextStyle(fontSize: 18, color: AppColors.slate900), + style: TextStyle(fontSize: 18, color: colorScheme.onSurface), ), ); }), diff --git a/apps/lib/features/chat/presentation/bloc/chat_bloc.dart b/apps/lib/features/chat/presentation/bloc/chat_bloc.dart index 1af8a26..63720c8 100644 --- a/apps/lib/features/chat/presentation/bloc/chat_bloc.dart +++ b/apps/lib/features/chat/presentation/bloc/chat_bloc.dart @@ -2,25 +2,37 @@ import 'dart:typed_data'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:social_app/core/chat/agent_stage.dart'; +import 'package:social_app/core/chat/chat_list_item.dart'; +import 'package:social_app/core/chat/chat_orchestrator.dart'; import 'package:social_app/core/network/i_api_client.dart'; import 'package:social_app/core/l10n/l10n.dart'; import '../../data/models/ag_ui_event.dart'; -import '../../data/models/chat_list_item.dart'; import '../../data/services/ag_ui_service.dart'; -import 'agent_stage.dart'; -class ChatState { +class ChatState implements ChatOrchestratorState { + @override final List items; + @override final bool isSending; + @override final bool isWaitingFirstToken; + @override final bool isStreaming; + @override final bool isCancelling; + @override final bool isLoadingHistory; + @override final String? currentMessageId; + @override final String? error; + @override final DateTime? oldestLoadedDate; + @override final bool hasEarlierHistory; + @override final AgentStage? currentStage; const ChatState({ @@ -37,6 +49,7 @@ class ChatState { this.currentStage, }); + @override bool get isLoading => isSending || isWaitingFirstToken || @@ -81,7 +94,7 @@ class ChatState { } } -class ChatBloc extends Cubit { +class ChatBloc extends Cubit implements ChatOrchestrator { ChatBloc({AgUiService? service, required IApiClient apiClient}) : _service = service ?? AgUiService(apiClient: apiClient), super(const ChatState()) { @@ -382,6 +395,7 @@ class ChatBloc extends Cubit { .reduce((a, b) => a.isBefore(b) ? a : b); } + @override Future sendMessage(String content, {List? images}) async { final messageId = 'user-${DateTime.now().millisecondsSinceEpoch}'; final attachments = (images ?? const []) @@ -527,6 +541,7 @@ class ChatBloc extends Cubit { emit(state.copyWith(items: items)); } + @override Future loadHistory() async { if (state.isLoadingHistory) return; emit(state.copyWith(isLoadingHistory: true)); @@ -554,6 +569,7 @@ class ChatBloc extends Cubit { } } + @override Future loadMoreHistory() async { if (state.isLoadingHistory || !state.hasEarlierHistory) return; if (state.oldestLoadedDate == null) return; @@ -584,10 +600,12 @@ class ChatBloc extends Cubit { } } + @override Future transcribeAudioFile(String filePath) { return _service.transcribeAudio(filePath); } + @override Future cancelCurrentRun() async { if (!(state.isWaitingFirstToken || state.isStreaming || @@ -637,6 +655,7 @@ class ChatBloc extends Cubit { return future; } + @override void clearError() { emit(state.copyWith(error: null)); } diff --git a/apps/lib/features/contacts/data/users/users_api.dart b/apps/lib/features/contacts/data/users/users_api.dart index 8ca60aa..c2a8f6a 100644 --- a/apps/lib/features/contacts/data/users/users_api.dart +++ b/apps/lib/features/contacts/data/users/users_api.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:social_app/core/network/i_api_client.dart'; -import 'models/user_response.dart'; +import 'package:social_app/data/models/user_profile.dart'; class UserBasicInfo { final String id; @@ -30,14 +30,14 @@ class UsersApi { return UserBasicInfo.fromJson(response.data); } - Future getMe() async { + Future getMe() async { final response = await _client.get('$_prefix/me'); - return UserResponse.fromJson(response.data); + return UserProfile.fromJson(response.data); } - Future updateMe(UserUpdateRequest request) async { + Future updateMe(UserUpdateRequest request) async { final response = await _client.patch('$_prefix/me', data: request.toJson()); - return UserResponse.fromJson(response.data); + return UserProfile.fromJson(response.data); } Future uploadAvatar(File file) async { @@ -62,12 +62,12 @@ class UsersApi { return url; } - Future> searchUsers(String query) async { + Future> searchUsers(String query) async { final response = await _client.post( '$_prefix/search', data: {'query': query}, ); final List data = response.data; - return data.map((json) => UserResponse.fromJson(json)).toList(); + return data.map((json) => UserProfile.fromJson(json)).toList(); } } diff --git a/apps/lib/features/contacts/data/users/users_repository.dart b/apps/lib/features/contacts/data/users/users_repository.dart index e6c7b0e..bc79107 100644 --- a/apps/lib/features/contacts/data/users/users_repository.dart +++ b/apps/lib/features/contacts/data/users/users_repository.dart @@ -1,7 +1,7 @@ -import 'models/user_response.dart'; +import '../../../../data/models/user_profile.dart'; abstract class UsersRepository { - Future getMe(); - Future updateMe(UserUpdateRequest request); - Future> searchUsers(String query); + Future getMe(); + Future updateMe(UserUpdateRequest request); + Future> searchUsers(String query); } diff --git a/apps/lib/features/contacts/data/users/users_repository_impl.dart b/apps/lib/features/contacts/data/users/users_repository_impl.dart index 6e08cf4..dbd15d9 100644 --- a/apps/lib/features/contacts/data/users/users_repository_impl.dart +++ b/apps/lib/features/contacts/data/users/users_repository_impl.dart @@ -1,6 +1,6 @@ import 'users_api.dart'; import 'users_repository.dart'; -import 'models/user_response.dart'; +import '../../../../data/models/user_profile.dart'; class UsersRepositoryImpl implements UsersRepository { final UsersApi _api; @@ -8,17 +8,17 @@ class UsersRepositoryImpl implements UsersRepository { UsersRepositoryImpl({required UsersApi api}) : _api = api; @override - Future getMe() { + Future getMe() { return _api.getMe(); } @override - Future updateMe(UserUpdateRequest request) { + Future updateMe(UserUpdateRequest request) { return _api.updateMe(request); } @override - Future> searchUsers(String query) { + Future> searchUsers(String query) { return _api.searchUsers(query); } } diff --git a/apps/lib/features/contacts/presentation/screens/add_contact_screen.dart b/apps/lib/features/contacts/presentation/screens/add_contact_screen.dart index bf31324..60b03aa 100644 --- a/apps/lib/features/contacts/presentation/screens/add_contact_screen.dart +++ b/apps/lib/features/contacts/presentation/screens/add_contact_screen.dart @@ -34,8 +34,9 @@ class _AddContactScreenState extends State { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Scaffold( - backgroundColor: AppColors.surfaceSecondary, + backgroundColor: colorScheme.surfaceContainerLow, resizeToAvoidBottomInset: false, body: SafeArea( maintainBottomViewPadding: true, @@ -73,6 +74,7 @@ class _AddContactScreenState extends State { } Widget _buildConfirmButton() { + final colorScheme = Theme.of(context).colorScheme; return SizedBox( width: AppSpacing.xxl * 2, height: AppSpacing.xxl * 2, @@ -80,47 +82,45 @@ class _AddContactScreenState extends State { onPressed: _handleConfirm, style: TextButton.styleFrom( padding: const EdgeInsets.all(AppSpacing.none), - backgroundColor: AppColors.surfaceInfo, + backgroundColor: colorScheme.primaryContainer, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.full), - side: const BorderSide(color: AppColors.borderQuaternary), + side: BorderSide(color: colorScheme.outlineVariant), ), ), - child: const Icon( + child: Icon( Icons.check, size: AppSpacing.lg, - color: AppColors.blue600, + color: colorScheme.primary, ), ), ); } Widget _buildAvatarSection() { + final colorScheme = Theme.of(context).colorScheme; return Center( child: Container( width: 72, height: 72, decoration: BoxDecoration( - color: AppColors.surfaceInfoLight, + color: colorScheme.primaryContainer.withValues(alpha: 0.45), borderRadius: BorderRadius.circular(36), - border: Border.all(color: Colors.transparent), - ), - child: const Icon( - Icons.person_outline, - size: 24, - color: AppColors.slate400, + border: Border.all(color: colorScheme.surface.withValues(alpha: 0)), ), + child: Icon(Icons.person_outline, size: 24, color: colorScheme.outline), ), ); } Widget _buildFormCard() { + final colorScheme = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColors.messageCardBorder), + border: Border.all(color: colorScheme.outlineVariant), ), child: Column( children: [ @@ -149,12 +149,13 @@ class _AddContactScreenState extends State { } Widget _buildDeleteRow() { + final colorScheme = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.only(bottom: AppSpacing.sm), child: LinkButton( text: context.l10n.contactDelete, onTap: _handleDelete, - foregroundColor: AppColors.red600, + foregroundColor: colorScheme.error, ), ); } @@ -177,6 +178,7 @@ class _AddContactScreenState extends State { } void _handleDelete() { + final colorScheme = Theme.of(context).colorScheme; showDialog( context: context, builder: (context) => AlertDialog( @@ -195,7 +197,7 @@ class _AddContactScreenState extends State { }, child: Text( context.l10n.commonDelete, - style: const TextStyle(color: AppColors.red600), + style: TextStyle(color: colorScheme.error), ), ), ], diff --git a/apps/lib/features/contacts/presentation/screens/contacts_screen.dart b/apps/lib/features/contacts/presentation/screens/contacts_screen.dart index d1b2cab..f873bae 100644 --- a/apps/lib/features/contacts/presentation/screens/contacts_screen.dart +++ b/apps/lib/features/contacts/presentation/screens/contacts_screen.dart @@ -2,13 +2,13 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../../../../app/di/injection.dart'; import '../../../../core/l10n/l10n.dart'; +import '../../../../data/models/user_profile.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/toast/index.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/back_title_page_header.dart'; import '../../../contacts/data/friends_api.dart'; -import '../../../contacts/data/users/models/user_response.dart'; import '../../../contacts/data/users/users_api.dart'; class ContactsScreen extends StatefulWidget { @@ -24,7 +24,7 @@ class _ContactsScreenState extends State { List _friends = []; List _pendingRequests = []; - List _searchResults = []; + List _searchResults = []; bool _isLoading = true; bool _isSearching = false; bool _hasSearched = false; @@ -142,13 +142,16 @@ class _ContactsScreenState extends State { } } - void _showAddFriendDialog(UserResponse user) { + void _showAddFriendDialog(UserProfile user) { final controller = TextEditingController(); showModalBottomSheet( context: context, isScrollControlled: true, - backgroundColor: Colors.transparent, + backgroundColor: Theme.of( + context, + ).colorScheme.surface.withValues(alpha: 0), builder: (sheetContext) { + final colorScheme = Theme.of(sheetContext).colorScheme; return Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(sheetContext).viewInsets.bottom, @@ -164,8 +167,8 @@ class _ContactsScreenState extends State { AppSpacing.xxl, AppSpacing.lg, ), - decoration: const BoxDecoration( - color: AppColors.white, + decoration: BoxDecoration( + color: colorScheme.surface, borderRadius: BorderRadius.vertical( top: Radius.circular(AppRadius.xxl), ), @@ -180,7 +183,7 @@ class _ContactsScreenState extends State { width: 40, height: AppSpacing.xs, decoration: BoxDecoration( - color: AppColors.slate300, + color: colorScheme.outlineVariant, borderRadius: BorderRadius.circular(AppRadius.full), ), ), @@ -188,23 +191,26 @@ class _ContactsScreenState extends State { const SizedBox(height: AppSpacing.lg), Text( context.l10n.contactsAddSheetTitle(user.username), - style: const TextStyle( + style: TextStyle( fontSize: 20, fontWeight: FontWeight.w600, - color: AppColors.slate900, + color: colorScheme.onSurface, ), ), const SizedBox(height: AppSpacing.sm), Text( context.l10n.contactsAddSheetDesc, - style: TextStyle(fontSize: 13, color: AppColors.slate500), + style: TextStyle( + fontSize: 13, + color: colorScheme.onSurfaceVariant, + ), ), const SizedBox(height: AppSpacing.lg), Container( decoration: BoxDecoration( - color: AppColors.slate50, + color: colorScheme.surfaceContainerLowest, borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: colorScheme.outlineVariant), ), child: TextField( controller: controller, @@ -215,13 +221,13 @@ class _ContactsScreenState extends State { hintText: context.l10n.contactsAddSheetMessageHint, hintStyle: TextStyle( fontSize: 13, - color: AppColors.slate400, + color: colorScheme.outline, ), border: InputBorder.none, contentPadding: EdgeInsets.all(AppSpacing.lg), counterStyle: TextStyle( fontSize: 11, - color: AppColors.slate400, + color: colorScheme.outline, ), ), ), @@ -270,8 +276,9 @@ class _ContactsScreenState extends State { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Scaffold( - backgroundColor: AppColors.surfaceSecondary, + backgroundColor: colorScheme.surfaceContainerLow, resizeToAvoidBottomInset: false, body: SafeArea( maintainBottomViewPadding: true, @@ -321,15 +328,16 @@ class _ContactsScreenState extends State { } Widget _buildSearchRow() { + final colorScheme = Theme.of(context).colorScheme; return Row( children: [ Expanded( child: Container( height: 40, decoration: BoxDecoration( - color: AppColors.surfaceTertiary, + color: colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(12), - border: Border.all(color: const Color(0xFFE4EBF7)), + border: Border.all(color: colorScheme.outlineVariant), ), child: TextField( controller: _searchController, @@ -339,12 +347,12 @@ class _ContactsScreenState extends State { hintStyle: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, - color: AppColors.slate400, + color: colorScheme.outline, ), prefixIcon: Icon( Icons.search, size: 16, - color: AppColors.slate400, + color: colorScheme.outline, ), border: InputBorder.none, contentPadding: EdgeInsets.symmetric( @@ -374,22 +382,22 @@ class _ContactsScreenState extends State { width: 40, height: 40, decoration: BoxDecoration( - color: const Color(0xFFF1F7FF), + color: colorScheme.primaryContainer.withValues(alpha: 0.4), borderRadius: BorderRadius.circular(12), - border: Border.all(color: const Color(0xFFD7E6FF)), + border: Border.all(color: colorScheme.primaryContainer), ), child: _isSearching - ? const Padding( + ? Padding( padding: EdgeInsets.all(10), child: AppLoadingIndicator( size: 16, strokeWidth: 2, - color: AppColors.blue500, - trackColor: AppColors.blue100, + color: colorScheme.primary, + trackColor: colorScheme.primaryContainer, withContainer: false, ), ) - : const Icon(Icons.search, size: 16, color: AppColors.blue500), + : Icon(Icons.search, size: 16, color: colorScheme.primary), ), ), ], @@ -397,6 +405,7 @@ class _ContactsScreenState extends State { } Widget _buildSearchResults() { + final colorScheme = Theme.of(context).colorScheme; if (!_hasSearched) { return const SizedBox.shrink(); } @@ -404,11 +413,11 @@ class _ContactsScreenState extends State { return Container( margin: const EdgeInsets.only(top: 8), decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.08), + color: colorScheme.shadow.withValues(alpha: 0.08), blurRadius: 8, offset: const Offset(0, 2), ), @@ -427,7 +436,10 @@ class _ContactsScreenState extends State { child: Center( child: Text( context.l10n.contactsSearchNoUser, - style: TextStyle(fontSize: 14, color: AppColors.slate500), + style: TextStyle( + fontSize: 14, + color: colorScheme.onSurfaceVariant, + ), ), ), ) @@ -445,17 +457,17 @@ class _ContactsScreenState extends State { ); } - Widget _buildSearchResultItem(UserResponse user) { + Widget _buildSearchResultItem(UserProfile user) { + final colorScheme = Theme.of(context).colorScheme; final isFriend = _friendIds.contains(user.id); final isSent = _sentRequestIds.contains(user.id); - final avatarColor = _getAvatarColor(user.id); return Container( height: 70, padding: const EdgeInsets.symmetric(horizontal: 14), child: Row( children: [ - _buildAvatar(user.avatarUrl, user.id, avatarColor), + _buildAvatar(user.avatarUrl, user.id, colorScheme), const SizedBox(width: 12), Expanded( child: Column( @@ -464,19 +476,19 @@ class _ContactsScreenState extends State { children: [ Text( user.username, - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, - color: AppColors.slate900, + color: colorScheme.onSurface, ), ), if (user.bio != null) ...[ const SizedBox(height: 2), Text( user.bio!, - style: const TextStyle( + style: TextStyle( fontSize: 12, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -492,16 +504,17 @@ class _ContactsScreenState extends State { } Widget _buildAddButton(String userId, bool isFriend, bool isSent) { + final colorScheme = Theme.of(context).colorScheme; if (isFriend) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( - color: AppColors.slate300, + color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), child: Text( context.l10n.contactsStatusAlreadyFriend, - style: TextStyle(fontSize: 12, color: AppColors.slate500), + style: TextStyle(fontSize: 12, color: colorScheme.onSurfaceVariant), ), ); } @@ -510,12 +523,12 @@ class _ContactsScreenState extends State { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( - color: AppColors.slate300, + color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), child: Text( context.l10n.contactsStatusSent, - style: TextStyle(fontSize: 12, color: AppColors.slate500), + style: TextStyle(fontSize: 12, color: colorScheme.onSurfaceVariant), ), ); } @@ -528,21 +541,21 @@ class _ContactsScreenState extends State { child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( - color: const Color(0xFFF1F7FF), + color: colorScheme.primaryContainer.withValues(alpha: 0.4), borderRadius: BorderRadius.circular(8), - border: Border.all(color: const Color(0xFFD7E6FF)), + border: Border.all(color: colorScheme.primaryContainer), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.person_add, size: 14, color: AppColors.blue500), + Icon(Icons.person_add, size: 14, color: colorScheme.primary), const SizedBox(width: 4), Text( context.l10n.contactsAdd, - style: const TextStyle( + style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, - color: AppColors.blue500, + color: colorScheme.primary, ), ), ], @@ -552,30 +565,31 @@ class _ContactsScreenState extends State { } Widget _buildEmptyState() { + final colorScheme = Theme.of(context).colorScheme; return Container( width: double.infinity, padding: const EdgeInsets.all(32), decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFFE3EAF6)), + border: Border.all(color: colorScheme.outlineVariant), ), child: Column( children: [ - const Icon(Icons.person_outline, size: 48, color: AppColors.slate400), + Icon(Icons.person_outline, size: 48, color: colorScheme.outline), const SizedBox(height: 12), Text( context.l10n.contactsEmptyTitle, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 4), Text( context.l10n.contactsEmptyDesc, - style: TextStyle(fontSize: 13, color: AppColors.slate400), + style: TextStyle(fontSize: 13, color: colorScheme.outline), ), ], ), @@ -583,22 +597,24 @@ class _ContactsScreenState extends State { } Widget _buildSectionTitle(String title) { + final colorScheme = Theme.of(context).colorScheme; return Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), ); } Widget _buildPendingRequestCard(List requests) { + final colorScheme = Theme.of(context).colorScheme; return Container( decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFFE3EAF6)), + border: Border.all(color: colorScheme.outlineVariant), ), child: Column( children: [ @@ -612,15 +628,15 @@ class _ContactsScreenState extends State { } Widget _buildPendingRequestItem(FriendRequestResponse request) { + final colorScheme = Theme.of(context).colorScheme; final recipient = request.recipient; - final color = _getAvatarColor(recipient.id); return Container( height: 70, padding: const EdgeInsets.symmetric(horizontal: 14), child: Row( children: [ - _buildAvatar(recipient.avatarUrl, recipient.id, color), + _buildAvatar(recipient.avatarUrl, recipient.id, colorScheme), const SizedBox(width: 12), Expanded( child: Column( @@ -629,16 +645,19 @@ class _ContactsScreenState extends State { children: [ Text( recipient.username, - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, - color: AppColors.slate900, + color: colorScheme.onSurface, ), ), const SizedBox(height: 2), Text( context.l10n.contactsPendingConfirm, - style: TextStyle(fontSize: 12, color: AppColors.slate500), + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), ), ], ), @@ -649,11 +668,12 @@ class _ContactsScreenState extends State { } Widget _buildContactCard(List friends) { + final colorScheme = Theme.of(context).colorScheme; return Container( decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFFE3EAF6)), + border: Border.all(color: colorScheme.outlineVariant), ), child: Column( children: [ @@ -667,8 +687,8 @@ class _ContactsScreenState extends State { } Widget _buildContactItem(FriendResponse friend) { + final colorScheme = Theme.of(context).colorScheme; final friendInfo = friend.friend; - final color = _getAvatarColor(friendInfo.id); return GestureDetector( onTap: () => context.push('/contacts/add?id=${friendInfo.id}'), @@ -677,7 +697,7 @@ class _ContactsScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 14), child: Row( children: [ - _buildAvatar(friendInfo.avatarUrl, friendInfo.id, color), + _buildAvatar(friendInfo.avatarUrl, friendInfo.id, colorScheme), const SizedBox(width: 12), Expanded( child: Column( @@ -686,10 +706,10 @@ class _ContactsScreenState extends State { children: [ Text( friendInfo.username, - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, - color: AppColors.slate900, + color: colorScheme.onSurface, ), ), ], @@ -701,52 +721,65 @@ class _ContactsScreenState extends State { ); } - Color _getAvatarColor(String id) { - final colors = [ - AppColors.blue500, - AppColors.violet600, - AppColors.blue600, - const Color(0xFF0EA5E9), - AppColors.violet500, - ]; - final index = id.hashCode.abs() % colors.length; - return colors[index]; + Color _getAvatarBackground( + Color color, + ColorScheme colorScheme, + AppColorPalette palette, + ) { + final avatarIndex = palette.avatarColors.indexOf(color); + final opacities = [0.30, 0.20, 0.35, 0.15, 0.30]; + final opacity = avatarIndex >= 0 && avatarIndex < opacities.length + ? opacities[avatarIndex] + : 0.30; + final isTertiary = avatarIndex == 4; + return isTertiary + ? colorScheme.tertiaryContainer.withValues(alpha: opacity) + : colorScheme.primaryContainer.withValues(alpha: opacity); } - Color _getAvatarBackground(Color color) { - if (color == AppColors.blue500) return const Color(0xFFEEF4FF); - if (color == AppColors.violet600) return AppColors.surfaceInfoLight; - if (color == AppColors.blue600) return const Color(0xFFEDF5FF); - if (color == const Color(0xFF0EA5E9)) return const Color(0xFFF2F8FF); - if (color == AppColors.violet500) return const Color(0xFFF5F7FF); - return const Color(0xFFEEF4FF); - } - - Color _getAvatarBorder(Color color) { - if (color == AppColors.blue500) return const Color(0xFFDDE8FB); - if (color == AppColors.violet600) return const Color(0xFFE2EAFB); - if (color == AppColors.blue600) return const Color(0xFFDCE9FB); - if (color == const Color(0xFF0EA5E9)) return const Color(0xFFDFEAFA); - if (color == AppColors.violet500) return const Color(0xFFE4E8FA); - return const Color(0xFFDDE8FB); + Color _getAvatarBorder( + Color color, + ColorScheme colorScheme, + AppColorPalette palette, + ) { + final avatarIndex = palette.avatarColors.indexOf(color); + final opacities = [0.25, 0.20, 0.30, 0.15, 0.25]; + final opacity = avatarIndex >= 0 && avatarIndex < opacities.length + ? opacities[avatarIndex] + : 0.25; + final isTertiary = avatarIndex == 4; + return isTertiary + ? colorScheme.tertiary.withValues(alpha: opacity) + : colorScheme.primary.withValues(alpha: opacity); } Widget _buildDivider() { + final colorScheme = Theme.of(context).colorScheme; return Container( height: 1, margin: const EdgeInsets.symmetric(horizontal: 14), - color: const Color(0xFFEEF2F7), + color: colorScheme.outlineVariant, ); } - Widget _buildAvatar(String? avatarUrl, String userId, Color color) { + Widget _buildAvatar( + String? avatarUrl, + String userId, + ColorScheme colorScheme, + ) { + final palette = Theme.of(context).extension()!; + final avatarColor = palette + .avatarColors[userId.hashCode.abs() % palette.avatarColors.length]; + return Container( width: 42, height: 42, decoration: BoxDecoration( - color: _getAvatarBackground(color), + color: _getAvatarBackground(avatarColor, colorScheme, palette), borderRadius: BorderRadius.circular(21), - border: Border.all(color: _getAvatarBorder(color)), + border: Border.all( + color: _getAvatarBorder(avatarColor, colorScheme, palette), + ), ), child: avatarUrl != null ? ClipRRect( @@ -756,14 +789,11 @@ class _ContactsScreenState extends State { width: 42, height: 42, fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => Icon( - Icons.person, - size: 18, - color: _getAvatarColor(userId), - ), + errorBuilder: (context, error, stackTrace) => + Icon(Icons.person, size: 18, color: avatarColor), ), ) - : Icon(Icons.person, size: 18, color: _getAvatarColor(userId)), + : Icon(Icons.person, size: 18, color: avatarColor), ); } @@ -772,6 +802,7 @@ class _ContactsScreenState extends State { TextEditingController controller, BuildContext sheetContext, ) { + final colorScheme = Theme.of(context).colorScheme; return SizedBox( height: 44, child: ElevatedButton( @@ -786,18 +817,18 @@ class _ContactsScreenState extends State { ); }, style: ElevatedButton.styleFrom( - backgroundColor: AppColors.blue500, - foregroundColor: AppColors.white, + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.sm), ), ), child: _sendingRequestUserId == userId - ? const AppLoadingIndicator( + ? AppLoadingIndicator( size: 16, strokeWidth: 2, - color: AppColors.white, - trackColor: AppColors.blue300, + color: colorScheme.onPrimary, + trackColor: colorScheme.primary.withValues(alpha: 0.3), withContainer: false, ) : Text( diff --git a/apps/lib/features/home/presentation/screens/home_screen.dart b/apps/lib/features/home/presentation/screens/home_screen.dart index 998a76f..69d7ffb 100644 --- a/apps/lib/features/home/presentation/screens/home_screen.dart +++ b/apps/lib/features/home/presentation/screens/home_screen.dart @@ -5,15 +5,15 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:social_app/core/chat/agent_stage.dart'; import '../../../../core/network/api_exception.dart'; import '../../../../app/di/injection.dart'; import '../../../../app/router/app_route_observer.dart'; import '../../../../app/router/app_routes.dart'; import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; -import '../../../chat/presentation/bloc/agent_stage.dart'; +import '../../../../data/repositories/inbox_repository.dart'; import '../../../chat/presentation/bloc/chat_bloc.dart'; -import '../../../messages/data/inbox_api.dart'; import '../../data/voice_recorder.dart'; import '../controllers/home_keyboard_inset_calculator.dart'; import '../controllers/home_message_viewport_controller.dart'; @@ -47,9 +47,6 @@ const homeConversationStageKey = ValueKey('home_conversation_stage'); const homeBottomInputStackKey = ValueKey('home_bottom_input_stack'); const homeEmptyStateAmbientKey = ValueKey('home_empty_state_ambient'); -/// Color constants. -const _chatBgColor = AppColors.slate50; - class HomeScreen extends StatefulWidget { final VoiceRecorder? voiceRecorder; final Future Function(String filePath)? onTranscribeAudio; @@ -76,7 +73,7 @@ class _HomeScreenState extends State final ScrollController _scrollController = ScrollController(); late final ChatBloc _chatBloc; late final VoiceRecorder _voiceRecorder; - late final InboxApi _inboxApi; + late final InboxRepository _inboxRepository; late final Future Function(String filePath) _transcribeAudio; late final AnimationController _listeningAnimationController; bool _isRecording = false; @@ -113,7 +110,7 @@ class _HomeScreenState extends State _chatBloc = context.read(); } _voiceRecorder = widget.voiceRecorder ?? RecordVoiceRecorder(); - _inboxApi = sl(); + _inboxRepository = sl(); _transcribeAudio = widget.onTranscribeAudio ?? _chatBloc.transcribeAudioFile; _listeningAnimationController = AnimationController( @@ -132,7 +129,7 @@ class _HomeScreenState extends State Future _loadUnreadCount() async { try { - final messages = await _inboxApi.getMessages(isRead: false); + final messages = await _inboxRepository.getMessages(isRead: false); if (mounted) { setState(() => _unreadCount = messages.length); } @@ -220,8 +217,10 @@ class _HomeScreenState extends State _previousIsLoadingHistory = state.isLoadingHistory; }, builder: (context, state) { + final colorScheme = Theme.of(context).colorScheme; + return Scaffold( - backgroundColor: _chatBgColor, + backgroundColor: colorScheme.surface, resizeToAvoidBottomInset: false, body: SafeArea( maintainBottomViewPadding: true, @@ -688,6 +687,8 @@ class _HomeEmptyStateAmbient extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Center( child: IgnorePointer( child: Container( @@ -701,9 +702,9 @@ class _HomeEmptyStateAmbient extends StatelessWidget { begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ - AppColors.homeBackgroundGlowSoft.withValues(alpha: 0.12), - AppColors.homeBackgroundGlow.withValues(alpha: 0.08), - AppColors.homeBackgroundGlowSoft.withValues(alpha: 0.12), + colorScheme.primaryContainer.withValues(alpha: 0.12), + colorScheme.primary.withValues(alpha: 0.08), + colorScheme.primaryContainer.withValues(alpha: 0.12), ], ), ), diff --git a/apps/lib/features/home/presentation/screens/home_screen_interactions.dart b/apps/lib/features/home/presentation/screens/home_screen_interactions.dart index d16ac72..9d82ae1 100644 --- a/apps/lib/features/home/presentation/screens/home_screen_interactions.dart +++ b/apps/lib/features/home/presentation/screens/home_screen_interactions.dart @@ -212,7 +212,9 @@ extension _HomeScreenInteractions on _HomeScreenState { void _showBottomSheet(BuildContext context) { showModalBottomSheet( context: context, - backgroundColor: Colors.transparent, + backgroundColor: Theme.of( + context, + ).colorScheme.surface.withValues(alpha: 0), isScrollControlled: true, builder: (context) => HomeSheet( onImagesSelected: (images) { diff --git a/apps/lib/features/home/presentation/screens/home_sheet.dart b/apps/lib/features/home/presentation/screens/home_sheet.dart index 78d0f38..9b133e9 100644 --- a/apps/lib/features/home/presentation/screens/home_sheet.dart +++ b/apps/lib/features/home/presentation/screens/home_sheet.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../../../../core/l10n/l10n.dart'; -import '../../../../core/theme/design_tokens.dart'; class HomeSheet extends StatelessWidget { final Function(List) onImagesSelected; @@ -11,10 +10,12 @@ class HomeSheet extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return GestureDetector( onTap: () => Navigator.of(context).pop(), child: Container( - color: const Color(0x4D0F172A), + color: colorScheme.scrim.withValues(alpha: 0.3), child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -23,9 +24,11 @@ class HomeSheet extends StatelessWidget { child: Container( width: double.infinity, padding: const EdgeInsets.all(16), - decoration: const BoxDecoration( - color: AppColors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(28), + ), ), child: Column( children: [ @@ -33,7 +36,7 @@ class HomeSheet extends StatelessWidget { width: 36, height: 4, decoration: BoxDecoration( - color: AppColors.slate300, + color: colorScheme.outlineVariant, borderRadius: BorderRadius.circular(2), ), ), @@ -79,6 +82,8 @@ class HomeSheet extends StatelessWidget { required String label, required VoidCallback onTap, }) { + final colorScheme = Theme.of(context).colorScheme; + return GestureDetector( onTap: onTap, child: Column( @@ -88,18 +93,18 @@ class HomeSheet extends StatelessWidget { width: 72, height: 72, decoration: BoxDecoration( - color: AppColors.blue50, + color: colorScheme.primaryContainer, borderRadius: BorderRadius.circular(16), ), - child: Icon(icon, size: 32, color: AppColors.blue500), + child: Icon(icon, size: 32, color: colorScheme.primary), ), const SizedBox(height: 12), Text( label, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: AppColors.slate700, + color: colorScheme.onSurfaceVariant, ), ), ], diff --git a/apps/lib/features/home/presentation/widgets/home_attachment_strip.dart b/apps/lib/features/home/presentation/widgets/home_attachment_strip.dart index e8fa6d0..f29e0dd 100644 --- a/apps/lib/features/home/presentation/widgets/home_attachment_strip.dart +++ b/apps/lib/features/home/presentation/widgets/home_attachment_strip.dart @@ -24,13 +24,15 @@ class HomeAttachmentStrip extends StatelessWidget { return const SizedBox.shrink(); } + final colorScheme = Theme.of(context).colorScheme; + return Container( key: homeAttachmentStripKey, padding: const EdgeInsets.all(AppSpacing.sm), decoration: BoxDecoration( - color: AppColors.homeAttachmentSurface, + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.homeComposerBorder), + border: Border.all(color: colorScheme.outlineVariant), ), child: Wrap( spacing: AppSpacing.sm, @@ -55,6 +57,7 @@ class _AttachmentPreviewTile extends StatelessWidget { @override Widget build(BuildContext context) { const previewExtent = AppSpacing.xxl * 3 + AppSpacing.sm; + final colorScheme = Theme.of(context).colorScheme; return Stack( children: [ @@ -69,12 +72,12 @@ class _AttachmentPreviewTile extends StatelessWidget { return Container( width: previewExtent, height: previewExtent, - color: AppColors.white, + color: colorScheme.surface, alignment: Alignment.center, - child: const Icon( + child: Icon( LucideIcons.image, size: AppSpacing.xl, - color: AppColors.slate400, + color: colorScheme.onSurfaceVariant, ), ); }, @@ -88,14 +91,14 @@ class _AttachmentPreviewTile extends StatelessWidget { child: Container( width: AppSpacing.lg + AppSpacing.sm, height: AppSpacing.lg + AppSpacing.sm, - decoration: const BoxDecoration( - color: AppColors.red500, + decoration: BoxDecoration( + color: colorScheme.error, shape: BoxShape.circle, ), - child: const Icon( + child: Icon( LucideIcons.x, size: AppSpacing.md, - color: AppColors.white, + color: colorScheme.onError, ), ), ), diff --git a/apps/lib/features/home/presentation/widgets/home_background_field.dart b/apps/lib/features/home/presentation/widgets/home_background_field.dart index e04ecf7..d1f702b 100644 --- a/apps/lib/features/home/presentation/widgets/home_background_field.dart +++ b/apps/lib/features/home/presentation/widgets/home_background_field.dart @@ -11,13 +11,15 @@ class HomeBackgroundField extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return DecoratedBox( key: homeBackgroundFieldKey, - decoration: const BoxDecoration( + decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [AppColors.homeBackgroundTop, AppColors.homeBackgroundBottom], + colors: [colorScheme.surface, colorScheme.surfaceContainerLowest], ), ), child: const Stack(children: [_HomeTopGlow(), _HomeBottomGlow()]), @@ -30,6 +32,8 @@ class _HomeTopGlow extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Align( alignment: const Alignment(-0.25, -0.9), child: IgnorePointer( @@ -39,10 +43,10 @@ class _HomeTopGlow extends StatelessWidget { height: AppSpacing.xxl * 7, decoration: BoxDecoration( borderRadius: BorderRadius.circular(AppSpacing.xxl * 3), - color: AppColors.homeBackgroundGlowSoft.withValues(alpha: 0.28), + color: colorScheme.primaryContainer.withValues(alpha: 0.28), boxShadow: [ BoxShadow( - color: AppColors.homeBackgroundGlow.withValues(alpha: 0.28), + color: colorScheme.primary.withValues(alpha: 0.2), blurRadius: AppSpacing.xxl * 3, spreadRadius: AppSpacing.xl, ), @@ -59,6 +63,8 @@ class _HomeBottomGlow extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return IgnorePointer( child: Align( alignment: Alignment.bottomCenter, @@ -69,11 +75,11 @@ class _HomeBottomGlow extends StatelessWidget { width: AppSpacing.xxl * 12, height: AppSpacing.xxl * 3, decoration: BoxDecoration( - color: AppColors.homeBackgroundGlowSoft.withValues(alpha: 0.18), + color: colorScheme.primaryContainer.withValues(alpha: 0.18), borderRadius: BorderRadius.circular(AppSpacing.xxl * 2), boxShadow: [ BoxShadow( - color: AppColors.homeBackgroundGlow.withValues(alpha: 0.1), + color: colorScheme.primary.withValues(alpha: 0.1), blurRadius: AppSpacing.xxl, spreadRadius: AppSpacing.sm, ), diff --git a/apps/lib/features/home/presentation/widgets/home_chat_item_renderer.dart b/apps/lib/features/home/presentation/widgets/home_chat_item_renderer.dart index 83ce090..5eb3006 100644 --- a/apps/lib/features/home/presentation/widgets/home_chat_item_renderer.dart +++ b/apps/lib/features/home/presentation/widgets/home_chat_item_renderer.dart @@ -2,12 +2,12 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:lucide_icons/lucide_icons.dart'; +import 'package:social_app/core/chat/chat_list_item.dart'; import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../core/utils/tool_name_localizer.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; -import '../../../chat/data/models/chat_list_item.dart'; import '../../../ui_schema/presentation/widgets/ui_schema_renderer.dart'; const _messagePaddingH = 13.0; @@ -23,15 +23,16 @@ class HomeChatItemRenderer { static Widget build(BuildContext context, ChatListItem item) { switch (item.type) { case ChatItemType.message: - return _buildMessageItem(item as TextMessageItem); + return _buildMessageItem(context, item as TextMessageItem); case ChatItemType.toolCall: return _buildToolCallItem(context, item as ToolCallItem); case ChatItemType.toolResult: - return _buildToolResultItem(item as ToolResultItem); + return _buildToolResultItem(context, item as ToolResultItem); } } - static Widget _buildMessageItem(TextMessageItem item) { + static Widget _buildMessageItem(BuildContext context, TextMessageItem item) { + final colorScheme = Theme.of(context).colorScheme; final isUser = item.sender == MessageSender.user; final imageAttachments = _collectRenderableImageAttachments( item.attachments, @@ -55,20 +56,26 @@ class HomeChatItemRenderer { vertical: _messagePaddingV, ), decoration: BoxDecoration( - color: isUser ? AppColors.blue50 : AppColors.white, + color: isUser + ? colorScheme.primaryContainer + : colorScheme.surface, borderRadius: BorderRadius.only( topLeft: const Radius.circular(_cornerRadius), topRight: const Radius.circular(_cornerRadius), bottomLeft: Radius.circular(isUser ? _cornerRadius : 0), bottomRight: Radius.circular(isUser ? 0 : _cornerRadius), ), - border: isUser ? null : Border.all(color: AppColors.slate300), + border: isUser + ? null + : Border.all(color: colorScheme.outlineVariant), ), child: Text( item.content, - style: const TextStyle( + style: TextStyle( fontSize: 14, - color: AppColors.slate900, + color: isUser + ? colorScheme.onPrimaryContainer + : colorScheme.onSurface, ), ), ), @@ -79,6 +86,7 @@ class HomeChatItemRenderer { Padding( padding: const EdgeInsets.only(top: _attachmentPreviewGap), child: _buildHistoryAttachmentPreviews( + context, item.attachments, imageAttachments: imageAttachments, ), @@ -88,6 +96,7 @@ class HomeChatItemRenderer { } static Widget _buildHistoryAttachmentPreviews( + BuildContext context, List> attachments, { List>? imageAttachments, }) { @@ -100,7 +109,9 @@ class HomeChatItemRenderer { spacing: _attachmentPreviewGap, runSpacing: _attachmentPreviewGap, crossAxisAlignment: WrapCrossAlignment.start, - children: renderableAttachments.map(_buildHistoryAttachmentTile).toList(), + children: renderableAttachments + .map((attachment) => _buildHistoryAttachmentTile(context, attachment)) + .toList(), ); } @@ -122,7 +133,11 @@ class HomeChatItemRenderer { mimeType.startsWith('image/'); } - static Widget _buildHistoryAttachmentTile(Map attachment) { + static Widget _buildHistoryAttachmentTile( + BuildContext context, + Map attachment, + ) { + final colorScheme = Theme.of(context).colorScheme; final path = attachment['path']; final url = attachment['url']; final isUploading = attachment['uploading'] == true; @@ -143,11 +158,11 @@ class HomeChatItemRenderer { ); }, errorBuilder: (context, error, stackTrace) { - return const Center( + return Center( child: Icon( LucideIcons.imageOff, size: _iconSize, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), ); }, @@ -157,11 +172,11 @@ class HomeChatItemRenderer { File(path), fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { - return const Center( + return Center( child: Icon( LucideIcons.imageOff, size: _iconSize, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), ); }, @@ -175,21 +190,21 @@ class HomeChatItemRenderer { child: Container( width: _attachmentPreviewSize, height: _attachmentPreviewSize, - color: AppColors.slate100, + color: colorScheme.surfaceContainerHighest, child: Stack( fit: StackFit.expand, children: [ image, if (isUploading) ColoredBox( - color: AppColors.slate900.withValues(alpha: 0.2), - child: const Center( + color: colorScheme.scrim.withValues(alpha: 0.2), + child: Center( child: AppLoadingIndicator( variant: AppLoadingVariant.inline, size: 18, strokeWidth: 2, - color: AppColors.white, - trackColor: AppColors.slate200, + color: colorScheme.onPrimary, + trackColor: colorScheme.surfaceContainerHighest, ), ), ), @@ -201,25 +216,26 @@ class HomeChatItemRenderer { static Widget _buildToolCallItem(BuildContext context, ToolCallItem item) { final l10n = context.l10n; + final colorScheme = Theme.of(context).colorScheme; final (statusText, statusColor, statusIcon) = switch (item.status) { ToolCallStatus.pending => ( l10n.homeToolPreparing, - AppColors.slate500, + colorScheme.onSurfaceVariant, LucideIcons.clock, ), ToolCallStatus.executing => ( l10n.homeToolExecuting, - AppColors.blue600, + colorScheme.primary, LucideIcons.loader, ), ToolCallStatus.error => ( item.errorMessage ?? l10n.homeToolExecutionFailed, - AppColors.red600, + colorScheme.error, LucideIcons.alertCircle, ), ToolCallStatus.completed => ( l10n.homeToolCompleted, - AppColors.emerald600, + colorScheme.tertiary, LucideIcons.checkCircle, ), }; @@ -228,9 +244,9 @@ class HomeChatItemRenderer { width: double.infinity, padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.surfaceInfoLight, + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all(color: AppColors.borderTertiary), + border: Border.all(color: colorScheme.outlineVariant), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -239,9 +255,9 @@ class HomeChatItemRenderer { width: 28, height: 28, decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.full), - border: Border.all(color: AppColors.borderTertiary), + border: Border.all(color: colorScheme.outlineVariant), ), child: Icon(statusIcon, size: 14, color: statusColor), ), @@ -252,10 +268,10 @@ class HomeChatItemRenderer { children: [ Text( localizeToolName(item.toolName), - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, - color: AppColors.slate800, + color: colorScheme.onSurface, ), ), const SizedBox(height: AppSpacing.xs), @@ -275,21 +291,27 @@ class HomeChatItemRenderer { ); } - static Widget _buildToolResultItem(ToolResultItem item) { + static Widget _buildToolResultItem( + BuildContext context, + ToolResultItem item, + ) { + final colorScheme = Theme.of(context).colorScheme; final rootNode = item.uiSchema['root']; final appearance = rootNode is Map ? rootNode['appearance'] as String? : null; final needsOuterCard = appearance == null || appearance == 'plain'; - final schemaContent = UiSchemaRenderer.renderSchema(item.uiSchema); + final schemaContent = UiSchemaRenderer( + colorScheme, + ).renderSchema(item.uiSchema); final wrappedContent = needsOuterCard ? Container( width: double.infinity, padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all(color: AppColors.homeConversationBorder), + border: Border.all(color: colorScheme.outlineVariant), ), child: schemaContent, ) diff --git a/apps/lib/features/home/presentation/widgets/home_composer_stack.dart b/apps/lib/features/home/presentation/widgets/home_composer_stack.dart index e49f401..0504a29 100644 --- a/apps/lib/features/home/presentation/widgets/home_composer_stack.dart +++ b/apps/lib/features/home/presentation/widgets/home_composer_stack.dart @@ -115,6 +115,8 @@ class HomeComposerStack extends StatelessWidget { } Widget _buildTextInputContent(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + if (isTranscribing) { return _buildTranscribingIndicator(context); } @@ -126,10 +128,10 @@ class HomeComposerStack extends StatelessWidget { focusNode: messageFocusNode, minLines: 1, maxLines: 1, - style: const TextStyle( + style: TextStyle( fontSize: AppSpacing.lg, height: 1, - color: AppColors.slate900, + color: colorScheme.onSurface, ), textAlignVertical: TextAlignVertical.center, decoration: InputDecoration( @@ -137,7 +139,7 @@ class HomeComposerStack extends StatelessWidget { hintStyle: TextStyle( fontSize: AppSpacing.lg, height: 1, - color: AppColors.slate400, + color: colorScheme.onSurfaceVariant, ), border: InputBorder.none, enabledBorder: InputBorder.none, @@ -156,29 +158,31 @@ class HomeComposerStack extends StatelessWidget { } Widget _buildTranscribingIndicator(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( width: 18, height: 18, - child: const AppLoadingIndicator( + child: AppLoadingIndicator( variant: AppLoadingVariant.inline, size: 18, strokeWidth: 2, - color: AppColors.blue600, - trackColor: AppColors.blue100, + color: colorScheme.primary, + trackColor: colorScheme.primaryContainer, ), ), const SizedBox(width: AppSpacing.sm), - _buildWaveDots(), + _buildWaveDots(context), const SizedBox(width: AppSpacing.sm), Expanded( child: Text( context.l10n.homeTranscribing, - style: const TextStyle( + style: TextStyle( fontSize: 14, - color: AppColors.blue600, + color: colorScheme.primary, fontWeight: FontWeight.w600, ), overflow: TextOverflow.ellipsis, @@ -188,7 +192,9 @@ class HomeComposerStack extends StatelessWidget { ); } - Widget _buildWaveDots() { + Widget _buildWaveDots(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Row( crossAxisAlignment: CrossAxisAlignment.end, children: List.generate(3, (index) { @@ -197,7 +203,7 @@ class HomeComposerStack extends StatelessWidget { width: 3, height: 6 + index * 2, decoration: BoxDecoration( - color: AppColors.blue500, + color: colorScheme.primary, borderRadius: BorderRadius.circular(2), ), ); diff --git a/apps/lib/features/home/presentation/widgets/home_conversation_chrome.dart b/apps/lib/features/home/presentation/widgets/home_conversation_chrome.dart index 2b38853..45c541e 100644 --- a/apps/lib/features/home/presentation/widgets/home_conversation_chrome.dart +++ b/apps/lib/features/home/presentation/widgets/home_conversation_chrome.dart @@ -11,6 +11,8 @@ class HomeWaitingIndicator extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Padding( padding: const EdgeInsets.fromLTRB(20, 0, 20, 20), child: Row( @@ -19,18 +21,18 @@ class HomeWaitingIndicator extends StatelessWidget { SizedBox( width: 18, height: 18, - child: const AppLoadingIndicator( + child: AppLoadingIndicator( variant: AppLoadingVariant.inline, size: 18, strokeWidth: 2, - color: AppColors.blue600, - trackColor: AppColors.blue100, + color: colorScheme.primary, + trackColor: colorScheme.primaryContainer, ), ), SizedBox(width: AppSpacing.sm), Text( label, - style: const TextStyle(fontSize: 14, color: AppColors.slate500), + style: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant), ), ], ), @@ -45,6 +47,7 @@ class HomeDateDivider extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; final now = DateTime.now(); const weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; final weekday = weekdays[date.weekday - 1]; @@ -62,7 +65,7 @@ class HomeDateDivider extends StatelessWidget { alignment: Alignment.center, child: Text( label, - style: const TextStyle(fontSize: 12, color: AppColors.slate400), + style: TextStyle(fontSize: 12, color: colorScheme.onSurfaceVariant), ), ); } @@ -80,22 +83,27 @@ class HomeLoadMoreButton extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return GestureDetector( onTap: isLoading ? null : onTap, child: Container( padding: const EdgeInsets.symmetric(vertical: 8), alignment: Alignment.center, child: isLoading - ? const AppLoadingIndicator( + ? AppLoadingIndicator( variant: AppLoadingVariant.inline, size: 14, strokeWidth: 1.5, - color: AppColors.slate400, - trackColor: AppColors.slate200, + color: colorScheme.onSurfaceVariant, + trackColor: colorScheme.surfaceContainerHighest, ) : Text( context.l10n.homeViewHistory, - style: const TextStyle(fontSize: 12, color: AppColors.slate400), + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), ), ), ); diff --git a/apps/lib/features/home/presentation/widgets/home_floating_header.dart b/apps/lib/features/home/presentation/widgets/home_floating_header.dart index 12c7135..7e95d73 100644 --- a/apps/lib/features/home/presentation/widgets/home_floating_header.dart +++ b/apps/lib/features/home/presentation/widgets/home_floating_header.dart @@ -22,6 +22,8 @@ class HomeFloatingHeader extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Container( key: homeFloatingHeaderKey, padding: const EdgeInsets.fromLTRB( @@ -30,9 +32,9 @@ class HomeFloatingHeader extends StatelessWidget { AppSpacing.lg, AppSpacing.xs, ), - decoration: const BoxDecoration( - color: AppColors.homeToolbarSurface, - border: Border(bottom: BorderSide(color: AppColors.homeToolbarBorder)), + decoration: BoxDecoration( + color: colorScheme.surface.withValues(alpha: 0.95), + border: Border(bottom: BorderSide(color: colorScheme.outlineVariant)), ), child: Stack( alignment: Alignment.center, @@ -61,7 +63,7 @@ class HomeFloatingHeader extends StatelessWidget { ), ], ), - const IgnorePointer( + IgnorePointer( child: Padding( padding: EdgeInsets.symmetric(horizontal: AppSpacing.xl * 3), child: Text( @@ -72,7 +74,7 @@ class HomeFloatingHeader extends StatelessWidget { style: TextStyle( fontSize: AppSpacing.lg + (AppSpacing.xs / 2), fontWeight: FontWeight.w600, - color: AppColors.slate800, + color: colorScheme.onSurface, ), ), ), @@ -91,6 +93,8 @@ class _HeaderIconButton extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return IconButton( visualDensity: VisualDensity.compact, padding: const EdgeInsets.all(AppSpacing.xs), @@ -99,7 +103,7 @@ class _HeaderIconButton extends StatelessWidget { minHeight: AppSpacing.xxl + AppSpacing.lg, ), onPressed: onPressed, - icon: Icon(icon, size: AppSpacing.xxl, color: AppColors.slate900), + icon: Icon(icon, size: AppSpacing.xxl, color: colorScheme.onSurface), ); } } @@ -112,6 +116,8 @@ class _MessagesButton extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return IconButton( visualDensity: VisualDensity.compact, padding: const EdgeInsets.all(AppSpacing.xs), @@ -123,10 +129,10 @@ class _MessagesButton extends StatelessWidget { icon: Stack( clipBehavior: Clip.none, children: [ - const Icon( + Icon( LucideIcons.messageSquare, size: AppSpacing.xxl, - color: AppColors.slate900, + color: colorScheme.onSurface, ), if (unreadCount > 0) Positioned( @@ -138,7 +144,7 @@ class _MessagesButton extends StatelessWidget { vertical: AppSpacing.xs / 2, ), decoration: BoxDecoration( - color: AppColors.red500, + color: colorScheme.error, borderRadius: BorderRadius.circular(AppSpacing.sm), ), constraints: const BoxConstraints( @@ -148,10 +154,10 @@ class _MessagesButton extends StatelessWidget { child: Text( unreadCount > 99 ? '99+' : unreadCount.toString(), textAlign: TextAlign.center, - style: const TextStyle( + style: TextStyle( fontSize: AppSpacing.sm + (AppSpacing.xs / 2), fontWeight: FontWeight.w600, - color: AppColors.white, + color: colorScheme.onError, ), ), ), diff --git a/apps/lib/features/home/presentation/widgets/home_recording_overlay.dart b/apps/lib/features/home/presentation/widgets/home_recording_overlay.dart index bbc34d8..face5a6 100644 --- a/apps/lib/features/home/presentation/widgets/home_recording_overlay.dart +++ b/apps/lib/features/home/presentation/widgets/home_recording_overlay.dart @@ -3,13 +3,6 @@ import 'package:flutter/material.dart'; import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; -const _recordingCancelTopColor = AppColors.warningBackground; -const _recordingCancelBottomColor = AppColors.red400; -const _recordingCancelLabelColor = AppColors.red600; -const _recordingActiveTopColor = AppColors.blue50; -const _recordingActiveBottomColor = AppColors.blue400; -const _recordingActiveLabelColor = AppColors.white; - class HomeRecordingOverlay extends StatelessWidget { const HomeRecordingOverlay({ super.key, @@ -22,15 +15,15 @@ class HomeRecordingOverlay extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; final topColor = isCancel - ? _recordingCancelTopColor - : _recordingActiveTopColor; - final bottomColor = isCancel - ? _recordingCancelBottomColor - : _recordingActiveBottomColor; + ? colorScheme.errorContainer + : colorScheme.primaryContainer; + final bottomColor = isCancel ? colorScheme.error : colorScheme.primary; final labelColor = isCancel - ? _recordingCancelLabelColor - : _recordingActiveLabelColor; + ? colorScheme.onErrorContainer + : colorScheme.onPrimaryContainer; + final barColor = isCancel ? colorScheme.error : colorScheme.primary; final label = isCancel ? context.l10n.homeRecordingReleaseCancel : context.l10n.homeRecordingHintReleaseSend; @@ -73,7 +66,7 @@ class HomeRecordingOverlay extends StatelessWidget { const SizedBox(height: AppSpacing.md), _WaveDots( listeningAnimation: listeningAnimation, - barColor: isCancel ? AppColors.red500 : AppColors.blue500, + barColor: barColor, ), ], ), diff --git a/apps/lib/features/home/presentation/widgets/home_unread_badge.dart b/apps/lib/features/home/presentation/widgets/home_unread_badge.dart index 4b1356d..3bacddb 100644 --- a/apps/lib/features/home/presentation/widgets/home_unread_badge.dart +++ b/apps/lib/features/home/presentation/widgets/home_unread_badge.dart @@ -12,6 +12,8 @@ class HomeUnreadBadge extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return AppPressable( onTap: onTap, child: Container( @@ -20,11 +22,11 @@ class HomeUnreadBadge extends StatelessWidget { vertical: AppSpacing.sm, ), decoration: BoxDecoration( - color: AppColors.blue600, + color: colorScheme.primary, borderRadius: BorderRadius.circular(AppRadius.full), boxShadow: [ BoxShadow( - color: AppColors.slate900.withValues(alpha: 0.18), + color: colorScheme.shadow.withValues(alpha: 0.18), blurRadius: AppRadius.md, offset: const Offset(0, AppSpacing.xs), ), @@ -32,9 +34,9 @@ class HomeUnreadBadge extends StatelessWidget { ), child: Text( context.l10n.homeUnreadMessages(count), - style: const TextStyle( - color: AppColors.white, - fontSize: 12, + style: TextStyle( + color: colorScheme.onPrimary, + fontSize: AppSpacing.md, fontWeight: FontWeight.w600, ), ), diff --git a/apps/lib/features/messages/presentation/screens/message_invite_detail_screen.dart b/apps/lib/features/messages/presentation/screens/message_invite_detail_screen.dart index e4f43b9..7b020c3 100644 --- a/apps/lib/features/messages/presentation/screens/message_invite_detail_screen.dart +++ b/apps/lib/features/messages/presentation/screens/message_invite_detail_screen.dart @@ -3,15 +3,15 @@ import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import '../../../../app/di/injection.dart'; +import '../../../../data/repositories/calendar_event_repository.dart'; +import '../../../../data/repositories/models/inbox_message.dart'; +import '../../../../data/repositories/inbox_repository.dart'; +import '../../../../data/repositories/user_repository.dart'; import '../../../../core/l10n/l10n.dart'; -import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/page_header.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; -import '../../../calendar/data/calendar_api.dart'; -import '../../../contacts/data/users/users_api.dart'; -import '../../data/inbox_api.dart'; class MessageInviteDetailScreen extends StatefulWidget { final String inviteId; @@ -24,25 +24,27 @@ class MessageInviteDetailScreen extends StatefulWidget { } class _MessageInviteDetailScreenState extends State { - late final InboxApi _inboxApi; - late final CalendarApi _calendarApi; - late final UsersApi _usersApi; + late final InboxRepository _inboxRepository; + late final CalendarEventRepository _calendarRepository; + late final UserRepository _userRepository; - InboxMessageResponse? _message; + InboxMessage? _message; String? _calendarTitle; String? _senderName; bool _loading = true; bool _submitting = false; String? _error; + ColorScheme get _colorScheme => Theme.of(context).colorScheme; + bool get _isPending => _message?.status == InboxMessageStatus.pending; @override void initState() { super.initState(); - _inboxApi = sl(); - _calendarApi = sl(); - _usersApi = sl(); + _inboxRepository = sl(); + _calendarRepository = sl(); + _userRepository = sl(); _loadDetail(); } @@ -54,11 +56,11 @@ class _MessageInviteDetailScreenState extends State { try { final results = await Future.wait([ - _inboxApi.getMessages(isRead: false), - _inboxApi.getMessages(isRead: true), + _inboxRepository.getMessages(isRead: false), + _inboxRepository.getMessages(isRead: true), ]); final messages = [...results[0], ...results[1]]; - InboxMessageResponse? message; + InboxMessage? message; for (final item in messages) { if (item.id == widget.inviteId) { message = item; @@ -72,7 +74,9 @@ class _MessageInviteDetailScreenState extends State { String? calendarTitle; if (message.scheduleItemId != null) { try { - final event = await _calendarApi.getById(message.scheduleItemId!); + final event = await _calendarRepository.getById( + message.scheduleItemId!, + ); calendarTitle = event.title; } catch (_) { calendarTitle = null; @@ -82,7 +86,7 @@ class _MessageInviteDetailScreenState extends State { String? senderName; if (message.senderId != null) { try { - final sender = await _usersApi.getById(message.senderId!); + final sender = await _userRepository.getById(message.senderId!); senderName = sender.username; } catch (_) { senderName = null; @@ -118,8 +122,8 @@ class _MessageInviteDetailScreenState extends State { setState(() => _submitting = true); try { - await _calendarApi.acceptSubscription(itemId); - await _inboxApi.markAsRead(message.id); + await _calendarRepository.acceptSubscription(itemId); + await _inboxRepository.markAsRead(message.id); if (!mounted) { return; } @@ -154,8 +158,8 @@ class _MessageInviteDetailScreenState extends State { setState(() => _submitting = true); try { - await _calendarApi.rejectSubscription(itemId); - await _inboxApi.markAsRead(message.id); + await _calendarRepository.rejectSubscription(itemId); + await _inboxRepository.markAsRead(message.id); if (!mounted) { return; } @@ -190,7 +194,7 @@ class _MessageInviteDetailScreenState extends State { } return Scaffold( - backgroundColor: AppColors.messageBg, + backgroundColor: _colorScheme.surface, body: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -211,9 +215,9 @@ class _MessageInviteDetailScreenState extends State { const SizedBox(height: 14), Text( _error!, - style: const TextStyle( + style: TextStyle( fontSize: 12, - color: AppColors.feedbackErrorText, + color: _colorScheme.error, ), ), ], @@ -250,9 +254,9 @@ class _MessageInviteDetailScreenState extends State { width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration( - color: AppColors.messageCardBg, + color: _colorScheme.surface, borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColors.messageCardBorder), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -262,7 +266,7 @@ class _MessageInviteDetailScreenState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: AppColors.slate900, + color: _colorScheme.onSurface, ), ), const SizedBox(height: 10), @@ -270,10 +274,10 @@ class _MessageInviteDetailScreenState extends State { context.l10n.messagesInviteEvent( _calendarTitle ?? context.l10n.messagesInviteUnnamedEvent, ), - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: AppColors.slate700, + color: _colorScheme.onSurface, ), ), const SizedBox(height: 10), @@ -281,37 +285,37 @@ class _MessageInviteDetailScreenState extends State { context.l10n.messagesInviteSender( _senderName ?? context.l10n.messagesInviteUnknownUser, ), - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.normal, - color: AppColors.slate500, + color: _colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 10), Text( context.l10n.messagesInviteTime(createdAtText), - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.normal, - color: AppColors.slate500, + color: _colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 10), Text( context.l10n.messagesInviteStatus(statusText), - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.normal, - color: AppColors.slate500, + color: _colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 10), Text( context.l10n.messagesInviteId(widget.inviteId), - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.normal, - color: AppColors.slate500, + color: _colorScheme.onSurfaceVariant, ), ), ], @@ -324,21 +328,25 @@ class _MessageInviteDetailScreenState extends State { width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( - color: AppColors.messageTipBg, + color: _colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.messageTipBorder), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Row( children: [ - const Icon(Icons.info_outline, size: 14, color: AppColors.slate500), + Icon( + Icons.info_outline, + size: 14, + color: _colorScheme.onSurfaceVariant, + ), const SizedBox(width: 8), Expanded( child: Text( context.l10n.messagesInviteTip, - style: const TextStyle( + style: TextStyle( fontSize: 12, fontWeight: FontWeight.normal, - color: AppColors.slate500, + color: _colorScheme.onSurfaceVariant, ), ), ), @@ -353,16 +361,16 @@ class _MessageInviteDetailScreenState extends State { width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), decoration: BoxDecoration( - color: AppColors.messageCardBg, + color: _colorScheme.surface, borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.messageCardBorder), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Text( context.l10n.messagesInviteAlreadyHandled, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, - color: AppColors.slate600, + color: _colorScheme.onSurfaceVariant, ), textAlign: TextAlign.center, ), @@ -378,28 +386,21 @@ class _MessageInviteDetailScreenState extends State { onTap: _submitting ? null : _rejectInvite, child: Container( decoration: BoxDecoration( - color: AppColors.messageCardBg, + color: _colorScheme.surface, borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.messageRejectBorder), + border: Border.all(color: _colorScheme.error), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text( - '×', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w700, - color: AppColors.red400, - ), - ), + Icon(Icons.close, size: 15, color: _colorScheme.error), const SizedBox(width: 6), Text( context.l10n.messagesReject, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: AppColors.red400, + color: _colorScheme.error, ), ), ], @@ -413,28 +414,21 @@ class _MessageInviteDetailScreenState extends State { onTap: _submitting ? null : _acceptInvite, child: Container( decoration: BoxDecoration( - color: AppColors.messageTagBg, + color: _colorScheme.primaryContainer, borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.messageAcceptBorder), + border: Border.all(color: _colorScheme.primary), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text( - '√', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w700, - color: AppColors.blue600, - ), - ), + Icon(Icons.check, size: 15, color: _colorScheme.primary), const SizedBox(width: 6), Text( context.l10n.messagesAccept, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: AppColors.blue600, + color: _colorScheme.primary, ), ), ], diff --git a/apps/lib/features/messages/presentation/screens/message_invite_list_screen.dart b/apps/lib/features/messages/presentation/screens/message_invite_list_screen.dart index 4195a68..01e2458 100644 --- a/apps/lib/features/messages/presentation/screens/message_invite_list_screen.dart +++ b/apps/lib/features/messages/presentation/screens/message_invite_list_screen.dart @@ -3,20 +3,21 @@ import 'package:go_router/go_router.dart'; import '../../../../app/di/injection.dart'; import '../../../../app/router/app_routes.dart'; +import '../../../../data/repositories/friend_repository.dart'; +import '../../../../data/repositories/inbox_repository.dart'; +import '../../../../data/repositories/models/friend_request.dart'; +import '../../../../data/repositories/models/inbox_message.dart'; import '../../../../core/l10n/l10n.dart'; -import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/app_pull_refresh_feedback.dart'; import '../../../../shared/widgets/page_header.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; -import '../../../contacts/data/friends_api.dart'; -import '../../data/inbox_api.dart'; import '../../presentation/widgets/message_action_sheet.dart'; class MessageWithFriend { - final InboxMessageResponse message; - final FriendRequestResponse? friendRequest; + final InboxMessage message; + final FriendRequest? friendRequest; const MessageWithFriend({required this.message, this.friendRequest}); } @@ -30,8 +31,8 @@ class MessageInviteListScreen extends StatefulWidget { } class _MessageInviteListScreenState extends State { - late final InboxApi _inboxApi; - late final FriendsApi _friendsApi; + late final InboxRepository _inboxRepository; + late final FriendRepository _friendRepository; List _unreadMessages = []; List _readMessages = []; @@ -39,11 +40,13 @@ class _MessageInviteListScreenState extends State { bool _isPullRefreshing = false; int _activeTabIndex = 0; + ColorScheme get _colorScheme => Theme.of(context).colorScheme; + @override void initState() { super.initState(); - _inboxApi = sl(); - _friendsApi = sl(); + _inboxRepository = sl(); + _friendRepository = sl(); _loadMessages(); } @@ -59,8 +62,8 @@ class _MessageInviteListScreenState extends State { } try { final results = await Future.wait([ - _inboxApi.getMessages(isRead: false), - _inboxApi.getMessages(isRead: true), + _inboxRepository.getMessages(isRead: false), + _inboxRepository.getMessages(isRead: true), ]); final unreadRaw = results[0]; final readRaw = results[1]; @@ -76,22 +79,9 @@ class _MessageInviteListScreenState extends State { .toSet() .toList(); - final requestMap = {}; - if (friendshipIds.isNotEmpty) { - final fetched = await Future.wait( - friendshipIds.map((id) async { - try { - final req = await _friendsApi.getRequestById(id); - return (id, req as FriendRequestResponse?); - } catch (_) { - return (id, null as FriendRequestResponse?); - } - }), - ); - for (final pair in fetched) { - requestMap[pair.$1] = pair.$2; - } - } + final requestMap = await _friendRepository.getRequestsByIds( + friendshipIds, + ); final unread = _mapMessagesWithFriend(unreadRaw, requestMap); final read = _mapMessagesWithFriend(readRaw, requestMap); @@ -122,8 +112,8 @@ class _MessageInviteListScreenState extends State { } List _mapMessagesWithFriend( - List messages, - Map requestMap, + List messages, + Map requestMap, ) { return messages.map((message) { final friendRequest = message.friendshipId == null @@ -185,16 +175,16 @@ class _MessageInviteListScreenState extends State { ); final description = message.content?['message'] as String?; final statusText = isReadOnly - ? (friendRequest.status == 'accepted' + ? (friendRequest.status == FriendRequestStatus.accepted ? context.l10n.messagesInviteStatusAccepted - : friendRequest.status == 'rejected' + : friendRequest.status == FriendRequestStatus.rejected ? context.l10n.messagesInviteStatusRejected : context.l10n.messagesInviteStatusHandled) : null; showModalBottomSheet( context: context, - backgroundColor: Colors.transparent, + backgroundColor: _colorScheme.surface.withValues(alpha: 0), isScrollControlled: true, builder: (ctx) => MessageActionSheet( title: title, @@ -202,7 +192,7 @@ class _MessageInviteListScreenState extends State { statusText: statusText, isReadOnly: isReadOnly, icon: Icons.person_add_outlined, - iconColor: AppColors.emerald500, + iconColor: _colorScheme.tertiary, onAccept: isReadOnly ? null : () async { @@ -234,7 +224,7 @@ class _MessageInviteListScreenState extends State { try { if (accept) { - await _friendsApi.acceptRequest(friendshipId); + await _friendRepository.acceptRequest(friendshipId); if (mounted) { Toast.show( context, @@ -243,7 +233,7 @@ class _MessageInviteListScreenState extends State { ); } } else { - await _friendsApi.declineRequest(friendshipId); + await _friendRepository.declineRequest(friendshipId); if (mounted) { Toast.show( context, @@ -252,7 +242,7 @@ class _MessageInviteListScreenState extends State { ); } } - await _inboxApi.markAsRead(message.id); + await _inboxRepository.markAsRead(message.id); await _loadMessages(); } catch (e) { if (mounted) { @@ -268,7 +258,7 @@ class _MessageInviteListScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: AppColors.background, + backgroundColor: _colorScheme.surface, body: SafeArea( child: Column( children: [ @@ -278,8 +268,6 @@ class _MessageInviteListScreenState extends State { ? const Center( child: AppLoadingIndicator( size: 22, - color: AppColors.blue500, - trackColor: AppColors.blue100, withContainer: false, ), ) @@ -311,7 +299,7 @@ class _MessageInviteListScreenState extends State { return Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( - color: AppColors.slate100, + color: _colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(10), ), child: Row( @@ -347,7 +335,9 @@ class _MessageInviteListScreenState extends State { child: Container( padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( - color: isSelected ? AppColors.white : Colors.transparent, + color: isSelected + ? _colorScheme.surface + : _colorScheme.surface.withValues(alpha: 0), borderRadius: BorderRadius.circular(8), ), child: Row( @@ -356,7 +346,9 @@ class _MessageInviteListScreenState extends State { Icon( icon, size: 16, - color: isSelected ? AppColors.slate900 : AppColors.slate500, + color: isSelected + ? _colorScheme.onSurface + : _colorScheme.onSurfaceVariant, ), const SizedBox(width: 6), Text( @@ -364,7 +356,9 @@ class _MessageInviteListScreenState extends State { style: TextStyle( fontSize: 14, fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, - color: isSelected ? AppColors.slate900 : AppColors.slate500, + color: isSelected + ? _colorScheme.onSurface + : _colorScheme.onSurfaceVariant, ), ), if (index == 0 && _unreadMessages.isNotEmpty) ...[ @@ -372,17 +366,17 @@ class _MessageInviteListScreenState extends State { Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), decoration: BoxDecoration( - color: AppColors.red500, + color: _colorScheme.error, borderRadius: BorderRadius.circular(8), ), child: Text( _unreadMessages.length > 99 ? '99+' : _unreadMessages.length.toString(), - style: const TextStyle( + style: TextStyle( fontSize: 10, fontWeight: FontWeight.w600, - color: AppColors.white, + color: _colorScheme.onError, ), ), ), @@ -440,13 +434,13 @@ class _MessageInviteListScreenState extends State { width: 80, height: 80, decoration: BoxDecoration( - color: AppColors.slate100, + color: _colorScheme.surfaceContainerHigh, shape: BoxShape.circle, ), child: Icon( isUnread ? Icons.notifications_none : Icons.inbox_outlined, size: 36, - color: AppColors.slate400, + color: _colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 16), @@ -454,10 +448,10 @@ class _MessageInviteListScreenState extends State { isUnread ? context.l10n.messagesEmptyUnreadTitle : context.l10n.messagesEmptyReadTitle, - style: const TextStyle( + style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, - color: AppColors.slate500, + color: _colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 8), @@ -465,7 +459,10 @@ class _MessageInviteListScreenState extends State { isUnread ? context.l10n.messagesEmptyUnreadDesc : context.l10n.messagesEmptyReadDesc, - style: const TextStyle(fontSize: 13, color: AppColors.slate400), + style: TextStyle( + fontSize: 13, + color: _colorScheme.onSurfaceVariant, + ), ), ], ), @@ -479,22 +476,24 @@ class _MessageCard extends StatelessWidget { const _MessageCard({required this.item, required this.onTap}); - InboxMessageResponse get message => item.message; - FriendRequestResponse? get friendRequest => item.friendRequest; + InboxMessage get message => item.message; + FriendRequest? get friendRequest => item.friendRequest; @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(16), border: Border.all( color: message.isRead - ? AppColors.borderSecondary - : AppColors.blue100, + ? colorScheme.outlineVariant + : colorScheme.primary, width: message.isRead ? 1 : 1.5, ), ), @@ -505,13 +504,13 @@ class _MessageCard extends StatelessWidget { width: 48, height: 48, decoration: BoxDecoration( - color: AppColors.emerald500.withValues(alpha: 0.1), + color: colorScheme.tertiaryContainer, borderRadius: BorderRadius.circular(14), ), - child: const Icon( + child: Icon( Icons.person_add_outlined, size: 22, - color: AppColors.emerald500, + color: colorScheme.tertiary, ), ), const SizedBox(width: 14), @@ -521,18 +520,18 @@ class _MessageCard extends StatelessWidget { children: [ Text( _title(), - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, - color: AppColors.slate900, + color: colorScheme.onSurface, ), ), const SizedBox(height: 6), Text( _content(), - style: const TextStyle( + style: TextStyle( fontSize: 13, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), maxLines: 2, overflow: TextOverflow.ellipsis, @@ -572,12 +571,12 @@ class _MessageCard extends StatelessWidget { final type = data['type'] as String?; if (type == 'invite') { - final status = message.status.value; - if (status == 'pending') { + final status = message.status; + if (status == InboxMessageStatus.pending) { return L10n.current.messagesInviteJoinCalendar; - } else if (status == 'accepted') { + } else if (status == InboxMessageStatus.accepted) { return L10n.current.messagesInviteAccepted; - } else if (status == 'rejected') { + } else if (status == InboxMessageStatus.rejected) { return L10n.current.messagesInviteRejected; } } else if (type == 'update') { diff --git a/apps/lib/features/messages/presentation/widgets/calendar_message_card.dart b/apps/lib/features/messages/presentation/widgets/calendar_message_card.dart index 6cddaa4..4b5eb80 100644 --- a/apps/lib/features/messages/presentation/widgets/calendar_message_card.dart +++ b/apps/lib/features/messages/presentation/widgets/calendar_message_card.dart @@ -25,6 +25,7 @@ class CalendarInviteCard extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; + final colorScheme = Theme.of(context).colorScheme; return Container( margin: const EdgeInsets.symmetric( @@ -33,9 +34,9 @@ class CalendarInviteCard extends StatelessWidget { ), padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.border), + border: Border.all(color: colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -45,12 +46,12 @@ class CalendarInviteCard extends StatelessWidget { Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: AppColors.blue100, + color: colorScheme.primaryContainer, borderRadius: BorderRadius.circular(AppRadius.sm), ), - child: const Icon( + child: Icon( Icons.calendar_today, - color: AppColors.blue600, + color: colorScheme.primary, size: 20, ), ), @@ -71,7 +72,7 @@ class CalendarInviteCard extends StatelessWidget { eventTitle != null ? l10n.messagesCalendarCardInviteWithTitle(eventTitle!) : l10n.messagesCalendarCardInviteWithoutTitle, - style: const TextStyle(fontSize: 14, color: AppColors.slate700), + style: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant), ), const SizedBox(height: AppSpacing.md), Row( @@ -112,6 +113,7 @@ class CalendarUpdateCard extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; + final colorScheme = Theme.of(context).colorScheme; return GestureDetector( onTap: onTap, @@ -122,21 +124,21 @@ class CalendarUpdateCard extends StatelessWidget { ), padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.border), + border: Border.all(color: colorScheme.outlineVariant), ), child: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: AppColors.blue100, + color: colorScheme.primaryContainer, borderRadius: BorderRadius.circular(AppRadius.sm), ), - child: const Icon( + child: Icon( Icons.calendar_today, - color: AppColors.blue600, + color: colorScheme.primary, size: 20, ), ), @@ -157,15 +159,15 @@ class CalendarUpdateCard extends StatelessWidget { const SizedBox(height: 2), Text( _formatTime(context, message.createdAt), - style: const TextStyle( + style: TextStyle( fontSize: 12, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), ), ], ), ), - const Icon(Icons.chevron_right, color: AppColors.slate400), + Icon(Icons.chevron_right, color: colorScheme.outline), ], ), ), @@ -201,6 +203,7 @@ class CalendarDeleteCard extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; + final colorScheme = Theme.of(context).colorScheme; return Container( margin: const EdgeInsets.symmetric( @@ -209,21 +212,21 @@ class CalendarDeleteCard extends StatelessWidget { ), padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.slate50, + color: colorScheme.surfaceContainerLowest, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.slate200), + border: Border.all(color: colorScheme.outlineVariant), ), child: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: AppColors.slate200, + color: colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(AppRadius.sm), ), - child: const Icon( + child: Icon( Icons.calendar_today, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, size: 20, ), ), @@ -233,7 +236,10 @@ class CalendarDeleteCard extends StatelessWidget { eventTitle != null ? l10n.messagesCalendarCardDeletedWithTitle(eventTitle!) : l10n.messagesCalendarCardDeletedWithoutTitle, - style: const TextStyle(fontSize: 14, color: AppColors.slate500), + style: TextStyle( + fontSize: 14, + color: colorScheme.onSurfaceVariant, + ), ), ), ], diff --git a/apps/lib/features/messages/presentation/widgets/message_action_sheet.dart b/apps/lib/features/messages/presentation/widgets/message_action_sheet.dart index 6a3a6ef..4bfbe71 100644 --- a/apps/lib/features/messages/presentation/widgets/message_action_sheet.dart +++ b/apps/lib/features/messages/presentation/widgets/message_action_sheet.dart @@ -27,12 +27,15 @@ class MessageActionSheet extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final resolvedIconColor = iconColor ?? colorScheme.primary; + return Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(24, 20, 24, 0), - decoration: const BoxDecoration( - color: AppColors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), ), child: Column( mainAxisSize: MainAxisSize.min, @@ -41,7 +44,7 @@ class MessageActionSheet extends StatelessWidget { width: 40, height: 4, decoration: BoxDecoration( - color: AppColors.slate300, + color: colorScheme.outlineVariant, borderRadius: BorderRadius.circular(2), ), ), @@ -51,23 +54,19 @@ class MessageActionSheet extends StatelessWidget { width: 72, height: 72, decoration: BoxDecoration( - color: (iconColor ?? AppColors.blue500).withValues(alpha: 0.1), + color: resolvedIconColor.withValues(alpha: 0.1), shape: BoxShape.circle, ), - child: Icon( - icon, - size: 32, - color: iconColor ?? AppColors.blue500, - ), + child: Icon(icon, size: 32, color: resolvedIconColor), ), const SizedBox(height: 16), ], Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 20, fontWeight: FontWeight.w600, - color: AppColors.slate900, + color: colorScheme.onSurface, ), textAlign: TextAlign.center, ), @@ -75,7 +74,10 @@ class MessageActionSheet extends StatelessWidget { const SizedBox(height: 8), Text( description!, - style: const TextStyle(fontSize: 14, color: AppColors.slate500), + style: TextStyle( + fontSize: 14, + color: colorScheme.onSurfaceVariant, + ), textAlign: TextAlign.center, ), ], @@ -84,12 +86,15 @@ class MessageActionSheet extends StatelessWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( - color: AppColors.slate100, + color: colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(16), ), child: Text( statusText!, - style: const TextStyle(fontSize: 14, color: AppColors.slate600), + style: TextStyle( + fontSize: 14, + color: colorScheme.onSurfaceVariant, + ), ), ), ], diff --git a/apps/lib/features/notification/data/services/ios_notification_payload_bridge.dart b/apps/lib/features/notification/data/services/ios_notification_payload_bridge.dart deleted file mode 100644 index d598381..0000000 --- a/apps/lib/features/notification/data/services/ios_notification_payload_bridge.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'dart:convert'; -import 'package:shared_preferences/shared_preferences.dart'; -import '../../domain/models/reminder_payload.dart'; - -class IOSNotificationPayloadBridge { - static const String _key = 'pending_notification_payload'; - final SharedPreferences _prefs; - - IOSNotificationPayloadBridge(this._prefs); - - Future getPendingPayload() async { - final raw = _prefs.getString(_key); - if (raw == null || raw.isEmpty) { - return null; - } - try { - final json = Map.from(jsonDecode(raw) as Map); - return ReminderPayload.fromJson(json); - } catch (_) { - return null; - } - } - - Future setPendingPayload(ReminderPayload payload) async { - await _prefs.setString(_key, jsonEncode(payload.toJson())); - } - - Future clearPendingPayload() async { - await _prefs.remove(_key); - } -} diff --git a/apps/lib/features/notification/domain/services/reminder_action_executor.dart b/apps/lib/features/notification/domain/services/reminder_action_executor.dart index f34f6cb..95f4ac8 100644 --- a/apps/lib/features/notification/domain/services/reminder_action_executor.dart +++ b/apps/lib/features/notification/domain/services/reminder_action_executor.dart @@ -1,7 +1,8 @@ -import '../../../calendar/data/services/calendar_service.dart'; -import '../../data/services/local_notification_service.dart'; +import '../../../../data/services/calendar_service.dart'; +import '../../../../data/models/reminder_payload.dart'; +import '../../../../data/repositories/models/schedule_item_model.dart'; +import '../../../../data/services/local_notification_service.dart'; import '../models/reminder_action.dart'; -import '../models/reminder_payload.dart'; class ReminderActionExecutor { final CalendarService _calendarService; @@ -39,8 +40,11 @@ class ReminderActionExecutor { } Future _snoozeEvent(String eventId) async { - final event = await _calendarService.getEventById(eventId); - if (event == null) { + late final ScheduleItemModel event; + try { + event = await _calendarService.getEventById(eventId); + } catch (_) { + await _notificationService.cancelEventReminder(eventId); return; } final now = DateTime.now(); @@ -62,6 +66,10 @@ class ReminderActionExecutor { } Future _archiveEvent(String eventId) async { - await _calendarService.archiveEvent(eventId); + try { + await _calendarService.archiveEvent(eventId); + } catch (_) { + await _notificationService.cancelEventReminder(eventId); + } } } diff --git a/apps/lib/features/notification/domain/services/reminder_queue_manager.dart b/apps/lib/features/notification/domain/services/reminder_queue_manager.dart index f7df247..8edfd5e 100644 --- a/apps/lib/features/notification/domain/services/reminder_queue_manager.dart +++ b/apps/lib/features/notification/domain/services/reminder_queue_manager.dart @@ -1,4 +1,4 @@ -import '../models/reminder_payload.dart'; +import '../../../../data/models/reminder_payload.dart'; class ReminderQueueManager { ReminderPayload? _currentPayload; diff --git a/apps/lib/features/notification/presentation/widgets/reminder_overlay.dart b/apps/lib/features/notification/presentation/widgets/reminder_overlay.dart index ffccbd3..818ad69 100644 --- a/apps/lib/features/notification/presentation/widgets/reminder_overlay.dart +++ b/apps/lib/features/notification/presentation/widgets/reminder_overlay.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; +import '../../../../data/models/reminder_payload.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../domain/services/reminder_queue_manager.dart'; -import '../../domain/models/reminder_payload.dart'; class ReminderOverlay extends StatefulWidget { const ReminderOverlay({ @@ -53,36 +53,41 @@ class _ReminderOverlayState extends State { left: button.dx, top: button.dy + box.size.height + 4, width: 120, - child: Material( - elevation: 4, - borderRadius: BorderRadius.circular(8), - child: Container( - decoration: BoxDecoration( - color: AppColors.white, + child: Builder( + builder: (context) { + final colorScheme = Theme.of(context).colorScheme; + return Material( + elevation: 4, borderRadius: BorderRadius.circular(8), - border: Border.all(color: AppColors.borderSecondary), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _SnoozeOption( - label: context.l10n.notificationSnoozeMinutes(5), - onTap: () { - _hideSnoozeOptions(); - _handleSnooze(5); - }, + child: Container( + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: colorScheme.outlineVariant), ), - const Divider(height: 1, color: AppColors.borderSecondary), - _SnoozeOption( - label: context.l10n.notificationSnoozeMinutes(15), - onTap: () { - _hideSnoozeOptions(); - _handleSnooze(15); - }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _SnoozeOption( + label: context.l10n.notificationSnoozeMinutes(5), + onTap: () { + _hideSnoozeOptions(); + _handleSnooze(5); + }, + ), + Divider(height: 1, color: colorScheme.outlineVariant), + _SnoozeOption( + label: context.l10n.notificationSnoozeMinutes(15), + onTap: () { + _hideSnoozeOptions(); + _handleSnooze(15); + }, + ), + ], ), - ], - ), - ), + ), + ); + }, ), ), ); @@ -104,13 +109,14 @@ class _ReminderOverlayState extends State { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; final payload = _currentPayload; if (payload == null) { return const SizedBox.shrink(); } return Container( - color: AppColors.white, + color: colorScheme.surface, child: SafeArea( child: Padding( padding: const EdgeInsets.all(AppSpacing.lg), @@ -121,7 +127,7 @@ class _ReminderOverlayState extends State { Text( payload.title, style: Theme.of(context).textTheme.headlineSmall?.copyWith( - color: AppColors.slate900, + color: colorScheme.onSurface, fontWeight: FontWeight.w600, ), textAlign: TextAlign.center, @@ -129,9 +135,9 @@ class _ReminderOverlayState extends State { const SizedBox(height: AppSpacing.sm), Text( DateFormat('HH:mm').format(DateTime.now()), - style: Theme.of( - context, - ).textTheme.titleLarge?.copyWith(color: AppColors.slate500), + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: colorScheme.onSurfaceVariant, + ), textAlign: TextAlign.center, ), const SizedBox(height: AppSpacing.xl), @@ -169,6 +175,7 @@ class _SnoozeOption extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return InkWell( onTap: onTap, child: Padding( @@ -180,7 +187,7 @@ class _SnoozeOption extends StatelessWidget { label, style: Theme.of( context, - ).textTheme.bodyMedium?.copyWith(color: AppColors.slate900), + ).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurface), ), ), ); diff --git a/apps/lib/features/settings/data/services/settings_user_cache.dart b/apps/lib/features/settings/data/services/settings_user_cache.dart deleted file mode 100644 index 0ea05c3..0000000 --- a/apps/lib/features/settings/data/services/settings_user_cache.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'dart:async'; - -import '../../../contacts/data/users/models/user_response.dart'; -import 'user_profile_cache_repository.dart'; - -class SettingsUserCache { - final UserProfileCacheRepository _repository; - - SettingsUserCache(this._repository); - - UserResponse? _cachedUser; - - UserResponse? get cachedUser => _cachedUser; - - Future getProfile({bool forceRefresh = false}) async { - final user = await _repository.getProfile(forceRefresh: forceRefresh); - _cachedUser = user; - return user; - } - - void set(UserResponse user) { - _cachedUser = user; - unawaited(_repository.setCached(user)); - } - - void invalidate() { - _cachedUser = null; - unawaited(_repository.invalidate()); - } -} diff --git a/apps/lib/features/settings/data/services/user_profile_cache_repository.dart b/apps/lib/features/settings/data/services/user_profile_cache_repository.dart index 4dfb873..52d650f 100644 --- a/apps/lib/features/settings/data/services/user_profile_cache_repository.dart +++ b/apps/lib/features/settings/data/services/user_profile_cache_repository.dart @@ -1,83 +1,67 @@ -import 'dart:async'; +import '../../../../data/cache/cache_policy.dart'; +import '../../../../data/cache/cached_repository.dart'; +import '../../../../data/models/user_profile.dart'; -import '../../../../core/cache/cache_entry.dart'; -import '../../../../core/cache/cache_policy.dart'; -import '../../../../core/cache/hybrid_cache_store.dart'; -import '../../../contacts/data/users/models/user_response.dart'; - -class UserProfileCacheRepository { +class UserProfileCacheRepository extends CachedRepository { static const String cacheKey = 'settings:user_profile'; - final HybridCacheStore store; - final CachePolicy policy; - final DateTime Function() now; - final Future Function() remoteLoader; - - Future? _refreshInFlight; + final Future Function() remoteLoader; + UserProfile? _cachedUser; + int _generation = 0; UserProfileCacheRepository({ - required this.store, + required super.store, required this.remoteLoader, CachePolicy? policy, - DateTime Function()? now, - }) : policy = - policy ?? - const CachePolicy( - softTtl: Duration(minutes: 2), - hardTtl: Duration(minutes: 30), - minRefreshInterval: Duration(minutes: 1), - ), - now = now ?? DateTime.now; + super.now, + }) : super( + policy: + policy ?? + const CachePolicy( + softTtl: Duration(minutes: 2), + hardTtl: Duration(minutes: 30), + minRefreshInterval: Duration(minutes: 1), + ), + ); - Future getProfile({bool forceRefresh = false}) async { - if (forceRefresh) { - return _refreshAndRead(); - } + UserProfile? get cachedUser => _cachedUser; - final cached = await store.read>(cacheKey); - if (cached == null) { - return _refreshAndRead(); - } - - final decision = policy.evaluate(now: now(), fetchedAt: cached.fetchedAt); - if (decision.shouldRefreshInBackground) { - _refreshInBackground(); - } - if (decision.mustBlockForNetwork || !decision.canUseCached) { - return _refreshAndRead(); - } - return cached.value; - } - - Future setCached(UserResponse user) { - return store.write>( - cacheKey, - CacheEntry(value: user, fetchedAt: now()), + Future getProfile({bool forceRefresh = false}) async { + final generation = _generation; + final user = await getOrLoad( + key: cacheKey, + forceRefresh: forceRefresh, + loadFromRemote: _loadAndRemember, + shouldWriteLoaded: (_) => generation == _generation, ); - } - - Future invalidate() => store.remove(cacheKey); - - void _refreshInBackground() { - final running = _refreshInFlight; - if (running != null) { - return; + if (generation == _generation) { + _cachedUser = user; } - final task = _refreshAndWrite().whenComplete(() { - _refreshInFlight = null; - }); - _refreshInFlight = task; - unawaited(task); + return user; } - Future _refreshAndRead() async { - await _refreshAndWrite(); - final cached = await store.read>(cacheKey); - return cached!.value; + Future setCached(UserProfile user) async { + final generation = _generation; + _cachedUser = user; + await writeCacheEntry(cacheKey, user); + if (generation != _generation) { + _cachedUser = null; + await removeCacheKey(cacheKey); + } } - Future _refreshAndWrite() async { + Future invalidate() async { + _generation += 1; + _cachedUser = null; + await removeCacheKey(cacheKey); + } + + Future _loadAndRemember() async { + final generation = _generation; final remote = await remoteLoader(); - await setCached(remote); + if (generation == _generation) { + _cachedUser = remote; + } + return remote; } } diff --git a/apps/lib/features/settings/data/services/user_profile_service.dart b/apps/lib/features/settings/data/services/user_profile_service.dart new file mode 100644 index 0000000..c9fc920 --- /dev/null +++ b/apps/lib/features/settings/data/services/user_profile_service.dart @@ -0,0 +1,53 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; + +import '../../../../core/network/i_api_client.dart'; +import '../../../../data/models/user_profile.dart'; + +class UserProfileService { + static const _prefix = '/api/v1/users'; + + final IApiClient _client; + + UserProfileService(this._client); + + Future getMe() async { + final response = await _client.get>('$_prefix/me'); + final data = response.data; + if (data == null) { + throw StateError('Invalid getMe response: empty payload'); + } + return UserProfile.fromJson(data); + } + + Future updateMe(UserUpdateRequest request) async { + final response = await _client.patch>( + '$_prefix/me', + data: request.toJson(), + ); + final data = response.data; + if (data == null) { + throw StateError('Invalid updateMe response: empty payload'); + } + return UserProfile.fromJson(data); + } + + Future uploadAvatar(File file) async { + final formData = FormData.fromMap({ + 'file': await MultipartFile.fromFile( + file.path, + filename: file.path.split('/').last, + ), + }); + final response = await _client.post>( + '$_prefix/me/avatar', + data: formData, + ); + final data = response.data; + if (data == null || data['url'] is! String) { + throw StateError('Invalid uploadAvatar response: missing url'); + } + return data['url'] as String; + } +} diff --git a/apps/lib/features/settings/presentation/screens/edit_profile_screen.dart b/apps/lib/features/settings/presentation/screens/edit_profile_screen.dart index 6e5e073..92b5d8c 100644 --- a/apps/lib/features/settings/presentation/screens/edit_profile_screen.dart +++ b/apps/lib/features/settings/presentation/screens/edit_profile_screen.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -9,9 +10,9 @@ import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; -import '../../data/services/settings_user_cache.dart'; -import '../../../contacts/data/users/models/user_response.dart'; -import '../../../contacts/data/users/users_api.dart'; +import '../../../../data/models/user_profile.dart'; +import '../../data/services/user_profile_cache_repository.dart'; +import '../../data/services/user_profile_service.dart'; import '../widgets/account_section_card.dart'; import '../widgets/settings_page_scaffold.dart'; @@ -25,11 +26,11 @@ class EditProfileScreen extends StatefulWidget { class _EditProfileScreenState extends State { final _usernameController = TextEditingController(); final _bioController = TextEditingController(); - final _usersApi = sl(); - final _userCache = sl(); + final _userProfileService = sl(); + final _userCache = sl(); final _imagePicker = ImagePicker(); - UserResponse? _user; + UserProfile? _user; File? _selectedAvatar; bool _isLoading = true; bool _isSaving = false; @@ -55,9 +56,9 @@ class _EditProfileScreenState extends State { } try { - final user = await _usersApi.getMe(); + final user = await _userProfileService.getMe(); if (mounted) { - _userCache.set(user); + unawaited(_userCache.setCached(user)); setState(() { _user = user; _usernameController.text = user.username; @@ -115,7 +116,7 @@ class _EditProfileScreenState extends State { }); try { - await _usersApi.uploadAvatar(_selectedAvatar!); + await _userProfileService.uploadAvatar(_selectedAvatar!); if (mounted) { Toast.show( context, @@ -183,8 +184,8 @@ class _EditProfileScreenState extends State { username: usernameChanged ? newUsername : null, bio: bioChanged ? (newBio.isEmpty ? null : newBio) : null, ); - final updatedUser = await _usersApi.updateMe(request); - _userCache.set(updatedUser); + final updatedUser = await _userProfileService.updateMe(request); + unawaited(_userCache.setCached(updatedUser)); } if (mounted) { @@ -254,11 +255,10 @@ class _EditProfileScreenState extends State { Widget _buildBasicInfoSection() { final l10n = context.l10n; + final colorScheme = Theme.of(context).colorScheme; return AccountSectionCard( title: l10n.settingsEditProfileBasicInfo, - backgroundColor: AppColors.white, - borderColor: AppColors.borderSecondary, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -269,14 +269,14 @@ class _EditProfileScreenState extends State { style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, - color: AppColors.slate700, + color: colorScheme.onSurface, ), ), const SizedBox(height: AppSpacing.sm), TextField( controller: _usernameController, onChanged: (_) => _onFieldChanged(), - style: const TextStyle(fontSize: 15, color: AppColors.slate900), + style: TextStyle(fontSize: 15, color: colorScheme.onSurface), decoration: _buildInputDecoration( l10n.settingsEditProfileUsernameHint, ), @@ -287,6 +287,7 @@ class _EditProfileScreenState extends State { } Widget _buildAvatarSection() { + final colorScheme = Theme.of(context).colorScheme; final avatarUrl = _user?.avatarUrl; final hasSelectedAvatar = _selectedAvatar != null; @@ -300,8 +301,8 @@ class _EditProfileScreenState extends State { height: 80, decoration: BoxDecoration( shape: BoxShape.circle, - color: AppColors.surfaceSecondary, - border: Border.all(color: AppColors.borderTertiary, width: 2), + color: colorScheme.surfaceContainerLow, + border: Border.all(color: colorScheme.outlineVariant, width: 2), image: hasSelectedAvatar ? DecorationImage( image: FileImage(_selectedAvatar!), @@ -315,10 +316,10 @@ class _EditProfileScreenState extends State { : null, ), child: !hasSelectedAvatar && avatarUrl == null - ? const Icon( + ? Icon( Icons.person, size: 40, - color: AppColors.slate400, + color: colorScheme.onSurfaceVariant, ) : null, ), @@ -327,16 +328,16 @@ class _EditProfileScreenState extends State { child: Container( decoration: BoxDecoration( shape: BoxShape.circle, - color: Colors.black.withValues(alpha: 0.4), + color: colorScheme.scrim.withValues(alpha: 0.4), ), - child: const Center( + child: Center( child: SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( - AppColors.white, + colorScheme.onPrimary, ), ), ), @@ -351,13 +352,13 @@ class _EditProfileScreenState extends State { height: 28, decoration: BoxDecoration( shape: BoxShape.circle, - color: AppColors.blue500, - border: Border.all(color: AppColors.white, width: 2), + color: colorScheme.primary, + border: Border.all(color: colorScheme.surface, width: 2), ), - child: const Icon( + child: Icon( Icons.camera_alt, size: 14, - color: AppColors.white, + color: colorScheme.onPrimary, ), ), ), @@ -369,11 +370,10 @@ class _EditProfileScreenState extends State { Widget _buildBioSection() { final l10n = context.l10n; + final colorScheme = Theme.of(context).colorScheme; return AccountSectionCard( title: l10n.settingsEditProfileBio, - backgroundColor: AppColors.white, - borderColor: AppColors.borderSecondary, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -382,7 +382,7 @@ class _EditProfileScreenState extends State { style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, - color: AppColors.slate700, + color: colorScheme.onSurface, ), ), const SizedBox(height: AppSpacing.sm), @@ -391,7 +391,7 @@ class _EditProfileScreenState extends State { onChanged: (_) => _onFieldChanged(), maxLines: 4, maxLength: 200, - style: const TextStyle(fontSize: 15, color: AppColors.slate900), + style: TextStyle(fontSize: 15, color: colorScheme.onSurface), decoration: _buildInputDecoration( l10n.settingsEditProfileBioHint, ).copyWith(contentPadding: const EdgeInsets.all(AppSpacing.lg)), @@ -402,11 +402,13 @@ class _EditProfileScreenState extends State { } InputDecoration _buildInputDecoration(String hintText) { + final colorScheme = Theme.of(context).colorScheme; + return InputDecoration( hintText: hintText, - hintStyle: const TextStyle(fontSize: 14, color: AppColors.slate400), + hintStyle: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant), filled: true, - fillColor: AppColors.surfaceSecondary, + fillColor: colorScheme.surfaceContainerLow, contentPadding: const EdgeInsets.symmetric( horizontal: AppSpacing.lg, vertical: AppSpacing.lg, @@ -417,11 +419,11 @@ class _EditProfileScreenState extends State { ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.lg), - borderSide: const BorderSide(color: AppColors.borderTertiary), + borderSide: BorderSide(color: colorScheme.outlineVariant), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppRadius.lg), - borderSide: const BorderSide(color: AppColors.blue500), + borderSide: BorderSide(color: colorScheme.primary), ), ); } diff --git a/apps/lib/features/settings/presentation/screens/features_screen.dart b/apps/lib/features/settings/presentation/screens/features_screen.dart index 7e39097..38be834 100644 --- a/apps/lib/features/settings/presentation/screens/features_screen.dart +++ b/apps/lib/features/settings/presentation/screens/features_screen.dart @@ -49,7 +49,9 @@ class _FeaturesScreenState extends State { body: BlocBuilder( builder: (context, state) { if (state.isLoading) { - return const Center(child: AppLoadingIndicator()); + return const Center( + child: AppLoadingIndicator(variant: AppLoadingVariant.surface), + ); } if (state.error != null) { return Center(child: Text(state.error!)); @@ -90,19 +92,21 @@ class _FeaturesScreenState extends State { } Widget _buildEmptyHint(String text) { + final colorScheme = Theme.of(context).colorScheme; + return Container( width: double.infinity, margin: const EdgeInsets.only(bottom: AppSpacing.sm), padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: colorScheme.outlineVariant), ), child: Text( text, - style: const TextStyle( - color: AppColors.slate500, + style: TextStyle( + color: colorScheme.onSurfaceVariant, fontSize: 13, fontWeight: FontWeight.w500, ), @@ -111,17 +115,21 @@ class _FeaturesScreenState extends State { } Widget _buildSectionTitle(String title) { + final colorScheme = Theme.of(context).colorScheme; + return Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), ); } Widget _buildJobCard(AutomationJobModel job) { + final colorScheme = Theme.of(context).colorScheme; + return AppPressable( onTap: () async { await context.push(AppRoutes.settingsJobDetail(job.id)); @@ -135,9 +143,9 @@ class _FeaturesScreenState extends State { margin: const EdgeInsets.only(bottom: AppSpacing.sm), padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: colorScheme.outlineVariant), ), child: Row( children: [ @@ -145,13 +153,13 @@ class _FeaturesScreenState extends State { width: AppSpacing.xxl + AppSpacing.lg, height: AppSpacing.xxl + AppSpacing.lg, decoration: BoxDecoration( - color: AppColors.surfaceTertiary, + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.md), ), - child: const Icon( + child: Icon( Icons.auto_awesome, size: AppSpacing.lg, - color: AppColors.blue500, + color: colorScheme.primary, ), ), const SizedBox(width: AppSpacing.md), @@ -161,17 +169,18 @@ class _FeaturesScreenState extends State { children: [ Text( job.title, - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, + color: colorScheme.onSurface, ), ), const SizedBox(height: AppSpacing.xs), Text( _buildSubtitle(job), - style: const TextStyle( + style: TextStyle( fontSize: 12, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), ), ], diff --git a/apps/lib/features/settings/presentation/screens/job_detail_screen.dart b/apps/lib/features/settings/presentation/screens/job_detail_screen.dart index 76802eb..a9adc1e 100644 --- a/apps/lib/features/settings/presentation/screens/job_detail_screen.dart +++ b/apps/lib/features/settings/presentation/screens/job_detail_screen.dart @@ -47,6 +47,8 @@ class _JobDetailScreenState extends State { int _contextWindowCount = 2; final Set _selectedTools = {'memory.write', 'memory.forget'}; + ColorScheme get _colorScheme => Theme.of(context).colorScheme; + @override void initState() { super.initState(); @@ -117,8 +119,8 @@ class _JobDetailScreenState extends State { const SizedBox(height: AppSpacing.sm), Text( error, - style: const TextStyle( - color: AppColors.error, + style: TextStyle( + color: _colorScheme.error, fontSize: 13, fontWeight: FontWeight.w500, ), @@ -245,23 +247,23 @@ class _JobDetailScreenState extends State { width: double.infinity, padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [AppColors.white, AppColors.surfaceInfoLight], + gradient: LinearGradient( + colors: [_colorScheme.surface, _colorScheme.surfaceContainerLow], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.borderTertiary), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( job.title, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, - color: AppColors.slate900, + color: _colorScheme.onSurface, ), ), const SizedBox(height: AppSpacing.sm), @@ -294,14 +296,14 @@ class _JobDetailScreenState extends State { vertical: AppSpacing.xs, ), decoration: BoxDecoration( - color: AppColors.white, + color: _colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.full), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Text( text, - style: const TextStyle( - color: AppColors.slate600, + style: TextStyle( + color: _colorScheme.onSurfaceVariant, fontSize: 12, fontWeight: FontWeight.w600, ), @@ -453,9 +455,9 @@ class _JobDetailScreenState extends State { vertical: AppSpacing.md, ), decoration: BoxDecoration( - color: AppColors.white, + color: _colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Row( children: [ @@ -465,8 +467,8 @@ class _JobDetailScreenState extends State { children: [ Text( label, - style: const TextStyle( - color: AppColors.slate500, + style: TextStyle( + color: _colorScheme.onSurfaceVariant, fontSize: 12, fontWeight: FontWeight.w500, ), @@ -474,8 +476,8 @@ class _JobDetailScreenState extends State { const SizedBox(height: AppSpacing.xs), Text( value, - style: const TextStyle( - color: AppColors.slate800, + style: TextStyle( + color: _colorScheme.onSurface, fontSize: 14, fontWeight: FontWeight.w600, ), @@ -483,7 +485,10 @@ class _JobDetailScreenState extends State { ], ), ), - const Icon(Icons.keyboard_arrow_down, color: AppColors.slate400), + Icon( + Icons.keyboard_arrow_down, + color: _colorScheme.onSurfaceVariant, + ), ], ), ), @@ -502,17 +507,17 @@ class _JobDetailScreenState extends State { vertical: AppSpacing.md, ), decoration: BoxDecoration( - color: AppColors.white, + color: _colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Row( children: [ Expanded( child: Text( context.l10n.settingsJobCounterValue(label, value), - style: const TextStyle( - color: AppColors.slate800, + style: TextStyle( + color: _colorScheme.onSurface, fontSize: 14, fontWeight: FontWeight.w600, ), @@ -537,14 +542,18 @@ class _JobDetailScreenState extends State { width: AppSpacing.xxl + AppSpacing.md, height: AppSpacing.xxl + AppSpacing.md, decoration: BoxDecoration( - color: onTap == null ? AppColors.slate100 : AppColors.surfaceTertiary, + color: onTap == null + ? _colorScheme.surfaceContainerHighest + : _colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.full), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Icon( icon, size: AppSpacing.lg, - color: onTap == null ? AppColors.slate300 : AppColors.blue500, + color: onTap == null + ? _colorScheme.onSurfaceVariant + : _colorScheme.primary, ), ), ); @@ -573,16 +582,22 @@ class _JobDetailScreenState extends State { vertical: AppSpacing.sm, ), decoration: BoxDecoration( - color: selected ? AppColors.blue50 : AppColors.white, + color: selected + ? _colorScheme.primaryContainer + : _colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.full), border: Border.all( - color: selected ? AppColors.blue300 : AppColors.borderSecondary, + color: selected + ? _colorScheme.primary + : _colorScheme.outlineVariant, ), ), child: Text( localizeToolName(toolName), style: TextStyle( - color: selected ? AppColors.blue600 : AppColors.slate600, + color: selected + ? _colorScheme.primary + : _colorScheme.onSurfaceVariant, fontSize: 12, fontWeight: FontWeight.w600, ), @@ -608,9 +623,9 @@ class _JobDetailScreenState extends State { return Container( padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.white, + color: _colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -618,7 +633,7 @@ class _JobDetailScreenState extends State { Text( l10n.settingsJobRunDays, style: TextStyle( - color: AppColors.slate500, + color: _colorScheme.onSurfaceVariant, fontSize: 12, fontWeight: FontWeight.w500, ), @@ -648,18 +663,22 @@ class _JobDetailScreenState extends State { vertical: AppSpacing.sm, ), decoration: BoxDecoration( - color: selected ? AppColors.blue50 : AppColors.white, + color: selected + ? _colorScheme.primaryContainer + : _colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.full), border: Border.all( color: selected - ? AppColors.blue300 - : AppColors.borderSecondary, + ? _colorScheme.primary + : _colorScheme.outlineVariant, ), ), child: Text( entry.value, style: TextStyle( - color: selected ? AppColors.blue600 : AppColors.slate600, + color: selected + ? _colorScheme.primary + : _colorScheme.onSurfaceVariant, fontSize: 12, fontWeight: FontWeight.w600, ), @@ -691,14 +710,14 @@ class _JobDetailScreenState extends State { width: double.infinity, padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.white, + color: _colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Text( text, - style: const TextStyle( - color: AppColors.slate700, + style: TextStyle( + color: _colorScheme.onSurface, fontSize: 13, fontWeight: FontWeight.w500, height: 1.5, @@ -710,10 +729,10 @@ class _JobDetailScreenState extends State { Widget _buildSectionTitle(String title) { return Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, - color: AppColors.slate500, + color: _colorScheme.onSurfaceVariant, ), ); } @@ -722,9 +741,9 @@ class _JobDetailScreenState extends State { return Container( padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.white, + color: _colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -742,8 +761,8 @@ class _JobDetailScreenState extends State { children: [ Text( label, - style: const TextStyle( - color: AppColors.slate500, + style: TextStyle( + color: _colorScheme.onSurfaceVariant, fontSize: 13, fontWeight: FontWeight.w500, ), @@ -753,8 +772,8 @@ class _JobDetailScreenState extends State { child: Text( value, textAlign: TextAlign.right, - style: const TextStyle( - color: AppColors.slate800, + style: TextStyle( + color: _colorScheme.onSurface, fontSize: 13, fontWeight: FontWeight.w600, ), diff --git a/apps/lib/features/settings/presentation/screens/memory_screen.dart b/apps/lib/features/settings/presentation/screens/memory_screen.dart index d0fc4f1..c6a62ac 100644 --- a/apps/lib/features/settings/presentation/screens/memory_screen.dart +++ b/apps/lib/features/settings/presentation/screens/memory_screen.dart @@ -23,6 +23,8 @@ class _MemoryScreenState extends State { bool _isLoading = true; String? _error; + ColorScheme get _colorScheme => Theme.of(context).colorScheme; + @override void initState() { super.initState(); @@ -81,16 +83,16 @@ class _MemoryScreenState extends State { return Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( - gradient: const LinearGradient( + gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [AppColors.white, AppColors.surfaceInfoLight], + colors: [_colorScheme.surface, _colorScheme.surfaceContainerLow], ), borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.borderTertiary), + border: Border.all(color: _colorScheme.outlineVariant), boxShadow: [ BoxShadow( - color: AppColors.blue100.withValues(alpha: 0.35), + color: _colorScheme.shadow.withValues(alpha: 0.2), blurRadius: 14, offset: const Offset(0, 4), ), @@ -103,24 +105,27 @@ class _MemoryScreenState extends State { width: 44, height: 44, decoration: BoxDecoration( - gradient: const LinearGradient( + gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [AppColors.blue100, AppColors.blue50], + colors: [ + _colorScheme.primaryContainer, + _colorScheme.surfaceContainerLow, + ], ), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: AppColors.blue200.withValues(alpha: 0.45), + color: _colorScheme.primary.withValues(alpha: 0.2), blurRadius: 8, offset: const Offset(0, 2), ), ], ), - child: const Icon( + child: Icon( Icons.auto_awesome, size: 22, - color: AppColors.blue600, + color: _colorScheme.primary, ), ), const SizedBox(width: AppSpacing.md), @@ -133,7 +138,7 @@ class _MemoryScreenState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: AppColors.slate900, + color: _colorScheme.onSurface, ), ), const SizedBox(height: 2), @@ -142,7 +147,7 @@ class _MemoryScreenState extends State { style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, - color: AppColors.slate500, + color: _colorScheme.onSurfaceVariant, ), ), ], @@ -167,11 +172,18 @@ class _MemoryScreenState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.error_outline, size: 48, color: AppColors.slate300), + Icon( + Icons.error_outline, + size: 48, + color: _colorScheme.onSurfaceVariant, + ), const SizedBox(height: AppSpacing.md), Text( _error ?? context.l10n.memoryLoadFailedRetry, - style: TextStyle(fontSize: 14, color: AppColors.slate500), + style: TextStyle( + fontSize: 14, + color: _colorScheme.onSurfaceVariant, + ), ), const SizedBox(height: AppSpacing.lg), AppPressable( @@ -183,16 +195,16 @@ class _MemoryScreenState extends State { vertical: AppSpacing.sm, ), decoration: BoxDecoration( - color: AppColors.blue50, + color: _colorScheme.primaryContainer, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.blue100), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Text( context.l10n.memoryReload, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: AppColors.blue600, + color: _colorScheme.primary, ), ), ), @@ -222,10 +234,10 @@ class _MemoryScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xs), child: Text( label, - style: const TextStyle( + style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, - color: AppColors.slate500, + color: _colorScheme.onSurfaceVariant, ), ), ); @@ -241,16 +253,16 @@ class _MemoryScreenState extends State { child: Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( - gradient: const LinearGradient( + gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [AppColors.white, AppColors.surfaceInfoLight], + colors: [_colorScheme.surface, _colorScheme.surfaceContainerLow], ), borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.borderTertiary), + border: Border.all(color: _colorScheme.outlineVariant), boxShadow: [ BoxShadow( - color: AppColors.slate200.withValues(alpha: 0.45), + color: _colorScheme.shadow.withValues(alpha: 0.15), blurRadius: 8, offset: const Offset(0, 2), ), @@ -269,16 +281,16 @@ class _MemoryScreenState extends State { begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - AppColors.blue100.withValues(alpha: 0.8), - AppColors.blue50.withValues(alpha: 0.8), + _colorScheme.primaryContainer.withValues(alpha: 0.8), + _colorScheme.surfaceContainerLow.withValues(alpha: 0.8), ], ), borderRadius: BorderRadius.circular(10), ), - child: const Icon( + child: Icon( Icons.person, size: 20, - color: AppColors.blue600, + color: _colorScheme.primary, ), ), const SizedBox(width: AppSpacing.md), @@ -291,7 +303,7 @@ class _MemoryScreenState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: AppColors.slate900, + color: _colorScheme.onSurface, ), ), const SizedBox(height: 2), @@ -302,7 +314,7 @@ class _MemoryScreenState extends State { style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, - color: AppColors.slate500, + color: _colorScheme.onSurfaceVariant, ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -310,7 +322,11 @@ class _MemoryScreenState extends State { ], ), ), - Icon(Icons.chevron_right, size: 20, color: AppColors.slate400), + Icon( + Icons.chevron_right, + size: 20, + color: _colorScheme.onSurfaceVariant, + ), ], ), if (hasData) ...[ @@ -352,16 +368,16 @@ class _MemoryScreenState extends State { child: Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( - gradient: const LinearGradient( + gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [AppColors.white, AppColors.surfaceTertiary], + colors: [_colorScheme.surface, _colorScheme.surfaceContainerLow], ), borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: _colorScheme.outlineVariant), boxShadow: [ BoxShadow( - color: AppColors.slate200.withValues(alpha: 0.4), + color: _colorScheme.shadow.withValues(alpha: 0.15), blurRadius: 8, offset: const Offset(0, 2), ), @@ -380,16 +396,16 @@ class _MemoryScreenState extends State { begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - AppColors.violet500.withValues(alpha: 0.15), - AppColors.violet500.withValues(alpha: 0.05), + _colorScheme.tertiary.withValues(alpha: 0.15), + _colorScheme.tertiary.withValues(alpha: 0.05), ], ), borderRadius: BorderRadius.circular(10), ), - child: const Icon( + child: Icon( Icons.work_outline, size: 20, - color: AppColors.violet600, + color: _colorScheme.tertiary, ), ), const SizedBox(width: AppSpacing.md), @@ -402,7 +418,7 @@ class _MemoryScreenState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: AppColors.slate900, + color: _colorScheme.onSurface, ), ), const SizedBox(height: 2), @@ -413,7 +429,7 @@ class _MemoryScreenState extends State { style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, - color: AppColors.slate500, + color: _colorScheme.onSurfaceVariant, ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -421,7 +437,11 @@ class _MemoryScreenState extends State { ], ), ), - Icon(Icons.chevron_right, size: 20, color: AppColors.slate400), + Icon( + Icons.chevron_right, + size: 20, + color: _colorScheme.onSurfaceVariant, + ), ], ), if (hasData) ...[ @@ -467,19 +487,23 @@ class _MemoryScreenState extends State { right: index < icons.length - 1 ? AppSpacing.sm : 0, ), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: _colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.md), ), child: Column( children: [ - Icon(icons[index], size: 16, color: AppColors.slate400), + Icon( + icons[index], + size: 16, + color: _colorScheme.onSurfaceVariant, + ), const SizedBox(height: 4), Text( values[index], - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: AppColors.slate700, + color: _colorScheme.onSurface, ), ), const SizedBox(height: 2), @@ -488,7 +512,7 @@ class _MemoryScreenState extends State { style: TextStyle( fontSize: 10, fontWeight: FontWeight.w500, - color: AppColors.slate400, + color: _colorScheme.onSurfaceVariant, ), ), ], diff --git a/apps/lib/features/settings/presentation/screens/settings_screen.dart b/apps/lib/features/settings/presentation/screens/settings_screen.dart index 0d59cd2..bdb88d2 100644 --- a/apps/lib/features/settings/presentation/screens/settings_screen.dart +++ b/apps/lib/features/settings/presentation/screens/settings_screen.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:social_app/core/constants/app_constants.dart'; +import 'package:social_app/core/config/env.dart'; import 'package:social_app/app/di/injection.dart'; import 'package:social_app/app/router/app_routes.dart'; import 'package:social_app/core/l10n/l10n.dart'; import 'package:social_app/core/theme/design_tokens.dart'; +import 'package:social_app/core/auth/session_controller.dart'; +import 'package:social_app/data/models/user_profile.dart'; +import 'package:social_app/data/repositories/friend_repository.dart'; import 'package:social_app/shared/widgets/app_button.dart'; import 'package:social_app/shared/widgets/app_loading_indicator.dart'; import 'package:social_app/shared/widgets/app_pressable.dart'; @@ -13,15 +15,10 @@ import 'package:social_app/shared/widgets/destructive_action_sheet.dart'; import 'package:social_app/shared/widgets/toast/toast.dart'; import 'package:social_app/shared/widgets/toast/toast_type.dart'; import 'package:social_app/core/utils/phone_display_formatter.dart'; -import 'package:social_app/features/auth/presentation/bloc/auth_bloc.dart'; -import 'package:social_app/features/auth/presentation/bloc/auth_event.dart'; -import 'package:social_app/features/auth/presentation/bloc/auth_state.dart'; -import 'package:social_app/features/contacts/data/friends_api.dart'; import 'package:social_app/features/settings/data/settings_api.dart'; import 'package:social_app/features/settings/data/services/automation_jobs_api.dart'; -import 'package:social_app/features/settings/data/services/settings_user_cache.dart'; -import 'package:social_app/features/contacts/data/users/models/user_response.dart'; -import 'package:social_app/features/home/presentation/navigation/home_return_policy.dart'; +import 'package:social_app/features/settings/data/services/user_profile_cache_repository.dart'; +import 'package:social_app/app/router/home_return_policy.dart'; import '../widgets/settings_page_scaffold.dart'; const settingsProfileEditButtonKey = ValueKey('settings_profile_edit_button'); @@ -35,11 +32,13 @@ class SettingsScreen extends StatefulWidget { } class _SettingsScreenState extends State { - final FriendsApi _friendsApi = sl(); + final FriendRepository _friendRepository = sl(); + final SessionController _sessionController = sl(); final AutomationJobsApi _automationJobsApi = sl(); - final SettingsUserCache _userCache = sl(); + final UserProfileCacheRepository _userCache = + sl(); - UserResponse? _user; + UserProfile? _user; bool _isLoading = true; int _friendsCount = 0; String? _firstFriendName; @@ -75,14 +74,12 @@ class _SettingsScreenState extends State { } try { - final friends = await _friendsApi.getFriends(); + final friends = await _friendRepository.getFriends(); if (mounted) { setState(() { _friendsCount = friends.length; - _firstFriendName = friends.isNotEmpty - ? friends.first.friend.username - : null; + _firstFriendName = friends.isNotEmpty ? friends.first.username : null; }); } } catch (e) { @@ -128,13 +125,15 @@ class _SettingsScreenState extends State { } Widget _buildProfileHero() { + final colorScheme = Theme.of(context).colorScheme; + if (_isLoading) { return Container( width: double.infinity, height: 120, padding: const EdgeInsets.all(AppSpacing.xl), decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.xxl), ), child: const Center(child: AppLoadingIndicator(size: 22)), @@ -151,16 +150,16 @@ class _SettingsScreenState extends State { width: double.infinity, padding: const EdgeInsets.all(AppSpacing.xl), decoration: BoxDecoration( - gradient: const LinearGradient( + gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [AppColors.white, AppColors.surfaceInfoLight], + colors: [colorScheme.surface, colorScheme.surfaceContainerLow], ), borderRadius: BorderRadius.circular(AppRadius.xxl), - border: Border.all(color: AppColors.borderTertiary), + border: Border.all(color: colorScheme.outlineVariant), boxShadow: [ BoxShadow( - color: AppColors.blue100.withValues(alpha: 0.35), + color: colorScheme.shadow.withValues(alpha: 0.2), blurRadius: 14, offset: const Offset(0, 4), ), @@ -179,12 +178,7 @@ class _SettingsScreenState extends State { borderRadius: BorderRadius.circular(32), boxShadow: [ BoxShadow( - color: Color.fromRGBO( - AppColors.blue400.r.toInt(), - AppColors.blue400.g.toInt(), - AppColors.blue400.b.toInt(), - 0.2, - ), + color: colorScheme.primary.withValues(alpha: 0.2), blurRadius: 12, offset: const Offset(0, 4), ), @@ -201,10 +195,10 @@ class _SettingsScreenState extends State { children: [ Text( username, - style: const TextStyle( + style: TextStyle( fontSize: 20, fontWeight: FontWeight.w700, - color: AppColors.slate900, + color: colorScheme.onSurface, ), overflow: TextOverflow.ellipsis, ), @@ -214,7 +208,7 @@ class _SettingsScreenState extends State { style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), ), ], @@ -235,17 +229,17 @@ class _SettingsScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Icon( + Icon( Icons.edit, size: 14, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), const SizedBox(height: 3), Container( width: 12, height: 1.5, decoration: BoxDecoration( - color: AppColors.slate400, + color: colorScheme.onSurfaceVariant, borderRadius: BorderRadius.circular( AppRadius.full, ), @@ -267,37 +261,47 @@ class _SettingsScreenState extends State { } Widget _buildFreeBadge() { + final colorScheme = Theme.of(context).colorScheme; + return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( gradient: LinearGradient( - colors: [AppColors.blue50, AppColors.surfaceInfoLight], + colors: [ + colorScheme.primaryContainer, + colorScheme.surfaceContainerLow, + ], ), borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.borderQuaternary), + border: Border.all(color: colorScheme.outlineVariant), ), child: Text( context.l10n.settingsFreeBadge, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, - color: AppColors.blue600, + color: colorScheme.primary, ), ), ); } Widget _buildAvatarImage(String? avatarUrl) { + final colorScheme = Theme.of(context).colorScheme; + if (avatarUrl == null) { return Container( - decoration: const BoxDecoration( + decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [AppColors.blue100, AppColors.blue50], + colors: [ + colorScheme.primaryContainer, + colorScheme.surfaceContainerLow, + ], ), ), - child: const Icon(Icons.person, size: 28, color: AppColors.blue600), + child: Icon(Icons.person, size: 28, color: colorScheme.primary), ); } return Image.network( @@ -307,33 +311,39 @@ class _SettingsScreenState extends State { height: 64, errorBuilder: (context, error, stackTrace) { return Container( - decoration: const BoxDecoration( + decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [AppColors.blue100, AppColors.blue50], + colors: [ + colorScheme.primaryContainer, + colorScheme.surfaceContainerLow, + ], ), ), - child: const Icon(Icons.person, size: 28, color: AppColors.blue600), + child: Icon(Icons.person, size: 28, color: colorScheme.primary), ); }, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Container( - decoration: const BoxDecoration( + decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [AppColors.blue100, AppColors.blue50], + colors: [ + colorScheme.primaryContainer, + colorScheme.surfaceContainerLow, + ], ), ), - child: const Center( + child: Center( child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(AppColors.blue600), + valueColor: AlwaysStoppedAnimation(colorScheme.primary), ), ), ), @@ -374,7 +384,6 @@ class _SettingsScreenState extends State { Expanded( child: _buildActionCard( icon: Icons.people, - iconColor: AppColors.blue500, title: context.l10n.contactsTitle, subtitle: _buildFriendsSubtitle(), onTap: () => context.push(AppRoutes.contactsList), @@ -384,7 +393,6 @@ class _SettingsScreenState extends State { Expanded( child: _buildActionCard( icon: Icons.auto_awesome, - iconColor: AppColors.blue500, title: context.l10n.settingsFeaturesTitle, subtitle: _buildAutomationSubtitle(), onTap: () => context.push(AppRoutes.settingsFeatures), @@ -396,11 +404,12 @@ class _SettingsScreenState extends State { Widget _buildActionCard({ required IconData icon, - required Color iconColor, required String title, required String subtitle, required VoidCallback onTap, }) { + final colorScheme = Theme.of(context).colorScheme; + return AppPressable( onTap: onTap, borderRadius: BorderRadius.circular(AppRadius.xl), @@ -408,12 +417,12 @@ class _SettingsScreenState extends State { constraints: const BoxConstraints(minHeight: 136), padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: colorScheme.outlineVariant), boxShadow: [ BoxShadow( - color: AppColors.slate200.withValues(alpha: 0.45), + color: colorScheme.shadow.withValues(alpha: 0.15), blurRadius: 8, offset: const Offset(0, 2), ), @@ -428,22 +437,26 @@ class _SettingsScreenState extends State { width: 36, height: 36, decoration: BoxDecoration( - color: AppColors.surfaceTertiary, + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(10), ), - child: Icon(icon, size: 18, color: iconColor), + child: Icon(icon, size: 18, color: colorScheme.primary), ), const Spacer(), - Icon(Icons.chevron_right, size: 16, color: AppColors.slate300), + Icon( + Icons.chevron_right, + size: 16, + color: colorScheme.onSurfaceVariant, + ), ], ), const SizedBox(height: AppSpacing.md), Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: AppColors.slate900, + color: colorScheme.onSurface, ), ), const SizedBox(height: 2), @@ -452,7 +465,7 @@ class _SettingsScreenState extends State { style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -464,19 +477,21 @@ class _SettingsScreenState extends State { } Widget _buildSubscriptionCard() { + final colorScheme = Theme.of(context).colorScheme; + return Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [AppColors.white, AppColors.surfaceInfoLight], + colors: [colorScheme.surface, colorScheme.surfaceContainerLow], ), borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.borderTertiary), + border: Border.all(color: colorScheme.outlineVariant), boxShadow: [ BoxShadow( - color: AppColors.slate200.withValues(alpha: 0.4), + color: colorScheme.shadow.withValues(alpha: 0.15), blurRadius: 8, offset: const Offset(0, 2), ), @@ -491,21 +506,24 @@ class _SettingsScreenState extends State { gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [AppColors.blue100, AppColors.blue50], + colors: [ + colorScheme.primaryContainer, + colorScheme.surfaceContainerLow, + ], ), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: AppColors.blue200.withValues(alpha: 0.45), + color: colorScheme.primary.withValues(alpha: 0.2), blurRadius: 6, offset: const Offset(0, 1), ), ], ), - child: const Icon( + child: Icon( Icons.workspace_premium, size: 22, - color: AppColors.blue600, + color: colorScheme.primary, ), ), const SizedBox(width: AppSpacing.md), @@ -518,7 +536,7 @@ class _SettingsScreenState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: AppColors.slate900, + color: colorScheme.onSurface, ), ), const SizedBox(height: 2), @@ -527,7 +545,7 @@ class _SettingsScreenState extends State { style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), ), ], @@ -536,13 +554,16 @@ class _SettingsScreenState extends State { Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [AppColors.blue500, AppColors.blue600], + gradient: LinearGradient( + colors: [ + colorScheme.primary, + colorScheme.primary.withValues(alpha: 0.85), + ], ), borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: const Color(0x4D60A5FA), + color: colorScheme.primary.withValues(alpha: 0.3), blurRadius: 8, offset: const Offset(0, 2), ), @@ -553,7 +574,7 @@ class _SettingsScreenState extends State { style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, - color: AppColors.white, + color: colorScheme.onPrimary, ), ), ), @@ -563,11 +584,13 @@ class _SettingsScreenState extends State { } Widget _buildMenuCard(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Container( decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: colorScheme.outlineVariant), ), child: Column( children: [ @@ -586,7 +609,7 @@ class _SettingsScreenState extends State { _buildMenuItem( icon: Icons.system_update, title: context.l10n.settingsMenuCheckUpdates, - trailing: 'v${AppConstants.version}', + trailing: 'v${Env.version}', onTap: _checkForUpdates, ), ], @@ -600,6 +623,8 @@ class _SettingsScreenState extends State { String? trailing, required VoidCallback onTap, }) { + final colorScheme = Theme.of(context).colorScheme; + return AppPressable( onTap: onTap, borderRadius: BorderRadius.circular(AppRadius.md), @@ -611,14 +636,14 @@ class _SettingsScreenState extends State { children: [ Row( children: [ - Icon(icon, size: 20, color: AppColors.slate500), + Icon(icon, size: 20, color: colorScheme.onSurfaceVariant), const SizedBox(width: 10), Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, - color: AppColors.slate900, + color: colorScheme.onSurface, ), ), ], @@ -628,17 +653,17 @@ class _SettingsScreenState extends State { if (trailing != null) ...[ Text( trailing, - style: const TextStyle( + style: TextStyle( fontSize: 14, - color: AppColors.slate400, + color: colorScheme.onSurfaceVariant, ), ), const SizedBox(width: 6), ], - const Icon( + Icon( Icons.chevron_right, size: 18, - color: AppColors.slate400, + color: colorScheme.onSurfaceVariant, ), ], ), @@ -649,10 +674,12 @@ class _SettingsScreenState extends State { } Widget _buildDivider() { + final colorScheme = Theme.of(context).colorScheme; + return Container( height: 1, margin: const EdgeInsets.symmetric(horizontal: 14), - color: AppColors.slate100, + color: colorScheme.outlineVariant, ); } @@ -679,13 +706,12 @@ class _SettingsScreenState extends State { return; } - _userCache.invalidate(); - final authBloc = context.read(); - authBloc.add(AuthLoggedOut()); + await _userCache.invalidate(); + if (!mounted) { + return; + } try { - await authBloc.stream - .firstWhere((state) => state is AuthUnauthenticated) - .timeout(const Duration(seconds: 5)); + await _sessionController.logoutAndWaitUnauthenticated(); } catch (_) { if (!mounted) return; Toast.show( @@ -703,8 +729,8 @@ class _SettingsScreenState extends State { try { final settingsApi = sl(); final result = await settingsApi.checkUpdates( - currentVersionCode: AppConstants.build, - currentVersionName: AppConstants.version, + currentVersionCode: Env.build, + currentVersionName: Env.version, platform: 'android', ); diff --git a/apps/lib/features/settings/presentation/screens/user_memory_detail_screen.dart b/apps/lib/features/settings/presentation/screens/user_memory_detail_screen.dart index cf5de0a..958d216 100644 --- a/apps/lib/features/settings/presentation/screens/user_memory_detail_screen.dart +++ b/apps/lib/features/settings/presentation/screens/user_memory_detail_screen.dart @@ -26,6 +26,8 @@ class _UserMemoryDetailScreenState extends State { String? _error; bool _hasChanges = false; + ColorScheme get _colorScheme => Theme.of(context).colorScheme; + @override void initState() { super.initState(); @@ -130,11 +132,15 @@ class _UserMemoryDetailScreenState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.error_outline, size: 48, color: AppColors.slate300), + Icon( + Icons.error_outline, + size: 48, + color: _colorScheme.onSurfaceVariant, + ), const SizedBox(height: AppSpacing.md), Text( _error ?? context.l10n.memoryLoadFailedRetry, - style: TextStyle(color: AppColors.slate500), + style: TextStyle(color: _colorScheme.onSurfaceVariant), ), const SizedBox(height: AppSpacing.lg), AppPressable( @@ -146,16 +152,16 @@ class _UserMemoryDetailScreenState extends State { vertical: AppSpacing.sm, ), decoration: BoxDecoration( - color: AppColors.blue50, + color: _colorScheme.primaryContainer, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.blue100), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Text( context.l10n.memoryReload, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: AppColors.blue600, + color: _colorScheme.primary, ), ), ), @@ -170,11 +176,15 @@ class _UserMemoryDetailScreenState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.person_off_outlined, size: 48, color: AppColors.slate300), + Icon( + Icons.person_off_outlined, + size: 48, + color: _colorScheme.onSurfaceVariant, + ), const SizedBox(height: AppSpacing.md), Text( context.l10n.settingsUserMemoryEmptyProfile, - style: TextStyle(color: AppColors.slate500), + style: TextStyle(color: _colorScheme.onSurfaceVariant), ), ], ), @@ -191,13 +201,13 @@ class _UserMemoryDetailScreenState extends State { child: Container( height: 52, decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [AppColors.blue500, AppColors.blue600], + gradient: LinearGradient( + colors: [_colorScheme.primary, _colorScheme.primary], ), borderRadius: BorderRadius.circular(AppRadius.lg), boxShadow: [ BoxShadow( - color: AppColors.blue500.withValues(alpha: 0.3), + color: _colorScheme.primary.withValues(alpha: 0.3), blurRadius: 8, offset: const Offset(0, 2), ), @@ -211,7 +221,7 @@ class _UserMemoryDetailScreenState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: AppColors.white, + color: _colorScheme.onPrimary, ), ), ), @@ -298,9 +308,9 @@ class _UserMemoryDetailScreenState extends State { margin: const EdgeInsets.only(bottom: AppSpacing.sm), padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: _colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -327,7 +337,11 @@ class _UserMemoryDetailScreenState extends State { borderRadius: BorderRadius.circular(AppRadius.sm), child: Container( padding: const EdgeInsets.all(AppSpacing.xs), - child: Icon(Icons.close, size: 18, color: AppColors.slate400), + child: Icon( + Icons.close, + size: 18, + color: _colorScheme.onSurfaceVariant, + ), ), ), ], @@ -416,9 +430,9 @@ class _UserMemoryDetailScreenState extends State { margin: const EdgeInsets.only(bottom: AppSpacing.sm), padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: _colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -445,7 +459,11 @@ class _UserMemoryDetailScreenState extends State { borderRadius: BorderRadius.circular(AppRadius.sm), child: Container( padding: const EdgeInsets.all(AppSpacing.xs), - child: Icon(Icons.close, size: 18, color: AppColors.slate400), + child: Icon( + Icons.close, + size: 18, + color: _colorScheme.onSurfaceVariant, + ), ), ), ], @@ -613,9 +631,9 @@ class _UserMemoryDetailScreenState extends State { margin: const EdgeInsets.only(bottom: AppSpacing.sm), padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: _colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -649,7 +667,11 @@ class _UserMemoryDetailScreenState extends State { borderRadius: BorderRadius.circular(AppRadius.sm), child: Container( padding: const EdgeInsets.all(AppSpacing.xs), - child: Icon(Icons.close, size: 18, color: AppColors.slate400), + child: Icon( + Icons.close, + size: 18, + color: _colorScheme.onSurfaceVariant, + ), ), ), ], @@ -706,14 +728,14 @@ class _UserMemoryDetailScreenState extends State { children: [ Row( children: [ - Icon(icon, size: 18, color: AppColors.blue500), + Icon(icon, size: 18, color: _colorScheme.primary), const SizedBox(width: AppSpacing.sm), Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, - color: AppColors.slate800, + color: _colorScheme.onSurface, ), ), if (count != null) ...[ @@ -724,15 +746,15 @@ class _UserMemoryDetailScreenState extends State { vertical: 2, ), decoration: BoxDecoration( - color: AppColors.blue50, + color: _colorScheme.primaryContainer, borderRadius: BorderRadius.circular(AppRadius.full), ), child: Text( '$count', - style: const TextStyle( + style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, - color: AppColors.blue600, + color: _colorScheme.primary, ), ), ), @@ -758,20 +780,20 @@ class _UserMemoryDetailScreenState extends State { style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, - color: AppColors.slate500, + color: _colorScheme.onSurfaceVariant, ), ), const SizedBox(height: AppSpacing.xs), Container( decoration: BoxDecoration( - color: AppColors.white, + color: _colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: _colorScheme.outlineVariant), ), child: TextFormField( initialValue: value, onChanged: onChanged, - style: const TextStyle(fontSize: 14, color: AppColors.slate800), + style: TextStyle(fontSize: 14, color: _colorScheme.onSurface), decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, @@ -779,7 +801,10 @@ class _UserMemoryDetailScreenState extends State { ), border: InputBorder.none, hintText: context.l10n.settingsMemoryInputHint(label), - hintStyle: TextStyle(color: AppColors.slate400, fontSize: 14), + hintStyle: TextStyle( + color: _colorScheme.onSurfaceVariant, + fontSize: 14, + ), ), ), ), @@ -791,14 +816,14 @@ class _UserMemoryDetailScreenState extends State { return Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: _colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Center( child: Text( message, - style: TextStyle(fontSize: 14, color: AppColors.slate400), + style: TextStyle(fontSize: 14, color: _colorScheme.onSurfaceVariant), ), ), ); @@ -823,18 +848,18 @@ class _UserMemoryDetailScreenState extends State { vertical: AppSpacing.sm, ), decoration: BoxDecoration( - color: AppColors.blue50, + color: _colorScheme.primaryContainer, borderRadius: BorderRadius.circular(AppRadius.full), - border: Border.all(color: AppColors.blue100), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( entry.value, - style: const TextStyle( + style: TextStyle( fontSize: 13, - color: AppColors.blue600, + color: _colorScheme.primary, ), ), const SizedBox(width: AppSpacing.xs), @@ -844,7 +869,7 @@ class _UserMemoryDetailScreenState extends State { child: Icon( Icons.close, size: 14, - color: AppColors.blue400, + color: _colorScheme.primary, ), ), ], @@ -860,21 +885,28 @@ class _UserMemoryDetailScreenState extends State { vertical: AppSpacing.sm, ), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: _colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.full), border: Border.all( - color: AppColors.borderSecondary, + color: _colorScheme.outlineVariant, style: BorderStyle.solid, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.add, size: 14, color: AppColors.slate500), + Icon( + Icons.add, + size: 14, + color: _colorScheme.onSurfaceVariant, + ), const SizedBox(width: AppSpacing.xs), Text( context.l10n.contactsAdd, - style: TextStyle(fontSize: 13, color: AppColors.slate500), + style: TextStyle( + fontSize: 13, + color: _colorScheme.onSurfaceVariant, + ), ), ], ), @@ -925,24 +957,24 @@ class _UserMemoryDetailScreenState extends State { child: Container( padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: _colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all( - color: AppColors.borderSecondary, + color: _colorScheme.outlineVariant, style: BorderStyle.solid, ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.add, size: 18, color: AppColors.blue500), + Icon(Icons.add, size: 18, color: _colorScheme.primary), const SizedBox(width: AppSpacing.xs), Text( text, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: AppColors.blue600, + color: _colorScheme.primary, ), ), ], diff --git a/apps/lib/features/settings/presentation/screens/user_memory_view_screen.dart b/apps/lib/features/settings/presentation/screens/user_memory_view_screen.dart index 40af1ef..0b198cc 100644 --- a/apps/lib/features/settings/presentation/screens/user_memory_view_screen.dart +++ b/apps/lib/features/settings/presentation/screens/user_memory_view_screen.dart @@ -94,16 +94,22 @@ class _UserMemoryViewScreenState extends State { } Widget _buildErrorState() { + final colorScheme = Theme.of(context).colorScheme; + return Center( child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.error_outline, size: 48, color: AppColors.slate300), + Icon( + Icons.error_outline, + size: 48, + color: colorScheme.onSurfaceVariant, + ), const SizedBox(height: AppSpacing.md), Text( _error ?? context.l10n.memoryLoadFailedRetry, - style: TextStyle(color: AppColors.slate500), + style: TextStyle(color: colorScheme.onSurfaceVariant), ), const SizedBox(height: AppSpacing.lg), AppPressable( @@ -115,16 +121,16 @@ class _UserMemoryViewScreenState extends State { vertical: AppSpacing.sm, ), decoration: BoxDecoration( - color: AppColors.blue50, + color: colorScheme.primaryContainer, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.blue100), + border: Border.all(color: colorScheme.outlineVariant), ), child: Text( context.l10n.memoryReload, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: AppColors.blue600, + color: colorScheme.primary, ), ), ), @@ -269,12 +275,14 @@ class _UserMemoryViewScreenState extends State { required IconData icon, required List children, }) { + final colorScheme = Theme.of(context).colorScheme; + return Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -286,18 +294,18 @@ class _UserMemoryViewScreenState extends State { width: 28, height: 28, decoration: BoxDecoration( - color: AppColors.blue50, + color: colorScheme.primaryContainer, borderRadius: BorderRadius.circular(AppRadius.md), ), - child: Icon(icon, size: 16, color: AppColors.blue600), + child: Icon(icon, size: 16, color: colorScheme.primary), ), const SizedBox(width: AppSpacing.sm), Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, - color: AppColors.slate900, + color: colorScheme.onSurface, ), ), ], @@ -315,11 +323,13 @@ class _UserMemoryViewScreenState extends State { String value, { bool multiline = false, }) { + final colorScheme = Theme.of(context).colorScheme; + return Container( margin: const EdgeInsets.only(bottom: AppSpacing.sm), padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.md), ), child: Row( @@ -327,7 +337,7 @@ class _UserMemoryViewScreenState extends State { ? CrossAxisAlignment.start : CrossAxisAlignment.center, children: [ - Icon(icon, size: 16, color: AppColors.slate500), + Icon(icon, size: 16, color: colorScheme.onSurfaceVariant), const SizedBox(width: AppSpacing.sm), Expanded( child: Column( @@ -338,16 +348,16 @@ class _UserMemoryViewScreenState extends State { style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 2), Text( value, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: AppColors.slate800, + color: colorScheme.onSurface, ), ), ], @@ -359,6 +369,8 @@ class _UserMemoryViewScreenState extends State { } Widget _buildPeople(List people) { + final colorScheme = Theme.of(context).colorScheme; + if (people.isEmpty) { return _buildEmptyTip(context.l10n.settingsUserMemoryEmptyContacts); } @@ -369,9 +381,9 @@ class _UserMemoryViewScreenState extends State { margin: const EdgeInsets.only(bottom: AppSpacing.sm), padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.borderTertiary), + border: Border.all(color: colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -410,6 +422,8 @@ class _UserMemoryViewScreenState extends State { } Widget _buildPlaces(List places) { + final colorScheme = Theme.of(context).colorScheme; + if (places.isEmpty) { return _buildEmptyTip(context.l10n.settingsUserMemoryEmptyPlaces); } @@ -420,9 +434,9 @@ class _UserMemoryViewScreenState extends State { margin: const EdgeInsets.only(bottom: AppSpacing.sm), padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.borderTertiary), + border: Border.all(color: colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -455,6 +469,8 @@ class _UserMemoryViewScreenState extends State { } Widget _buildRoutines(List routines) { + final colorScheme = Theme.of(context).colorScheme; + if (routines.isEmpty) { return _buildEmptyTip(context.l10n.settingsUserMemoryEmptyRoutines); } @@ -465,9 +481,9 @@ class _UserMemoryViewScreenState extends State { margin: const EdgeInsets.only(bottom: AppSpacing.sm), padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.borderTertiary), + border: Border.all(color: colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -496,6 +512,8 @@ class _UserMemoryViewScreenState extends State { } Widget _buildTags(List tags) { + final colorScheme = Theme.of(context).colorScheme; + if (tags.isEmpty) { return _buildEmptyTip(context.l10n.memoryNoInfo); } @@ -509,22 +527,22 @@ class _UserMemoryViewScreenState extends State { vertical: AppSpacing.sm, ), decoration: BoxDecoration( - color: AppColors.blue50, + color: colorScheme.primaryContainer, borderRadius: BorderRadius.circular(AppRadius.full), - border: Border.all(color: AppColors.blue100), + border: Border.all(color: colorScheme.outlineVariant), ), child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Icon(Icons.label_outline, size: 14, color: AppColors.blue500), + Icon(Icons.label_outline, size: 14, color: colorScheme.primary), const SizedBox(width: AppSpacing.xs), Text( tag, - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, - color: AppColors.blue600, + color: colorScheme.onPrimaryContainer, ), ), ], @@ -535,24 +553,30 @@ class _UserMemoryViewScreenState extends State { } Widget _buildEmptyTip(String text) { + final colorScheme = Theme.of(context).colorScheme; + return Container( padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.borderTertiary), + border: Border.all(color: colorScheme.outlineVariant), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Icon(Icons.inbox_outlined, size: 16, color: AppColors.slate400), + Icon( + Icons.inbox_outlined, + size: 16, + color: colorScheme.onSurfaceVariant, + ), const SizedBox(width: AppSpacing.sm), Text( text, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), ), ], diff --git a/apps/lib/features/settings/presentation/screens/work_memory_detail_screen.dart b/apps/lib/features/settings/presentation/screens/work_memory_detail_screen.dart index 0b010fe..0b8904d 100644 --- a/apps/lib/features/settings/presentation/screens/work_memory_detail_screen.dart +++ b/apps/lib/features/settings/presentation/screens/work_memory_detail_screen.dart @@ -26,6 +26,8 @@ class _WorkMemoryDetailScreenState extends State { String? _error; bool _hasChanges = false; + ColorScheme get _colorScheme => Theme.of(context).colorScheme; + @override void initState() { super.initState(); @@ -130,11 +132,15 @@ class _WorkMemoryDetailScreenState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.error_outline, size: 48, color: AppColors.slate300), + Icon( + Icons.error_outline, + size: 48, + color: _colorScheme.onSurfaceVariant, + ), const SizedBox(height: AppSpacing.md), Text( _error ?? context.l10n.memoryLoadFailedRetry, - style: TextStyle(color: AppColors.slate500), + style: TextStyle(color: _colorScheme.onSurfaceVariant), ), const SizedBox(height: AppSpacing.lg), AppPressable( @@ -146,16 +152,16 @@ class _WorkMemoryDetailScreenState extends State { vertical: AppSpacing.sm, ), decoration: BoxDecoration( - color: AppColors.blue50, + color: _colorScheme.primaryContainer, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.blue100), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Text( context.l10n.memoryReload, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: AppColors.blue600, + color: _colorScheme.primary, ), ), ), @@ -170,11 +176,15 @@ class _WorkMemoryDetailScreenState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.work_off_outlined, size: 48, color: AppColors.slate300), + Icon( + Icons.work_off_outlined, + size: 48, + color: _colorScheme.onSurfaceVariant, + ), const SizedBox(height: AppSpacing.md), Text( context.l10n.settingsWorkMemoryEmptyProfile, - style: TextStyle(color: AppColors.slate500), + style: TextStyle(color: _colorScheme.onSurfaceVariant), ), ], ), @@ -191,13 +201,13 @@ class _WorkMemoryDetailScreenState extends State { child: Container( height: 52, decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [AppColors.blue500, AppColors.blue600], + gradient: LinearGradient( + colors: [_colorScheme.primary, _colorScheme.primary], ), borderRadius: BorderRadius.circular(AppRadius.lg), boxShadow: [ BoxShadow( - color: AppColors.blue500.withValues(alpha: 0.3), + color: _colorScheme.primary.withValues(alpha: 0.3), blurRadius: 8, offset: const Offset(0, 2), ), @@ -211,7 +221,7 @@ class _WorkMemoryDetailScreenState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - color: AppColors.white, + color: _colorScheme.onPrimary, ), ), ), @@ -338,9 +348,9 @@ class _WorkMemoryDetailScreenState extends State { margin: const EdgeInsets.only(bottom: AppSpacing.sm), padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: _colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -374,7 +384,11 @@ class _WorkMemoryDetailScreenState extends State { borderRadius: BorderRadius.circular(AppRadius.sm), child: Container( padding: const EdgeInsets.all(AppSpacing.xs), - child: Icon(Icons.close, size: 18, color: AppColors.slate400), + child: Icon( + Icons.close, + size: 18, + color: _colorScheme.onSurfaceVariant, + ), ), ), ], @@ -475,9 +489,9 @@ class _WorkMemoryDetailScreenState extends State { margin: const EdgeInsets.only(bottom: AppSpacing.sm), padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: _colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -506,7 +520,11 @@ class _WorkMemoryDetailScreenState extends State { borderRadius: BorderRadius.circular(AppRadius.sm), child: Container( padding: const EdgeInsets.all(AppSpacing.xs), - child: Icon(Icons.close, size: 18, color: AppColors.slate400), + child: Icon( + Icons.close, + size: 18, + color: _colorScheme.onSurfaceVariant, + ), ), ), ], @@ -649,14 +667,14 @@ class _WorkMemoryDetailScreenState extends State { children: [ Row( children: [ - Icon(icon, size: 18, color: AppColors.violet500), + Icon(icon, size: 18, color: _colorScheme.tertiary), const SizedBox(width: AppSpacing.sm), Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, - color: AppColors.slate800, + color: _colorScheme.onSurface, ), ), if (count != null) ...[ @@ -667,7 +685,7 @@ class _WorkMemoryDetailScreenState extends State { vertical: 2, ), decoration: BoxDecoration( - color: AppColors.violet500.withValues(alpha: 0.1), + color: _colorScheme.tertiaryContainer, borderRadius: BorderRadius.circular(AppRadius.full), ), child: Text( @@ -675,7 +693,7 @@ class _WorkMemoryDetailScreenState extends State { style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, - color: AppColors.violet600, + color: _colorScheme.tertiary, ), ), ), @@ -701,20 +719,20 @@ class _WorkMemoryDetailScreenState extends State { style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, - color: AppColors.slate500, + color: _colorScheme.onSurfaceVariant, ), ), const SizedBox(height: AppSpacing.xs), Container( decoration: BoxDecoration( - color: AppColors.white, + color: _colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: _colorScheme.outlineVariant), ), child: TextFormField( initialValue: value, onChanged: onChanged, - style: const TextStyle(fontSize: 14, color: AppColors.slate800), + style: TextStyle(fontSize: 14, color: _colorScheme.onSurface), decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, @@ -722,7 +740,10 @@ class _WorkMemoryDetailScreenState extends State { ), border: InputBorder.none, hintText: context.l10n.settingsMemoryInputHint(label), - hintStyle: TextStyle(color: AppColors.slate400, fontSize: 14), + hintStyle: TextStyle( + color: _colorScheme.onSurfaceVariant, + fontSize: 14, + ), ), ), ), @@ -734,14 +755,14 @@ class _WorkMemoryDetailScreenState extends State { return Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: _colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Center( child: Text( message, - style: TextStyle(fontSize: 14, color: AppColors.slate400), + style: TextStyle(fontSize: 14, color: _colorScheme.onSurfaceVariant), ), ), ); @@ -766,10 +787,10 @@ class _WorkMemoryDetailScreenState extends State { vertical: AppSpacing.sm, ), decoration: BoxDecoration( - color: AppColors.violet500.withValues(alpha: 0.1), + color: _colorScheme.tertiaryContainer, borderRadius: BorderRadius.circular(AppRadius.full), border: Border.all( - color: AppColors.violet500.withValues(alpha: 0.3), + color: _colorScheme.tertiary.withValues(alpha: 0.3), ), ), child: Row( @@ -779,7 +800,7 @@ class _WorkMemoryDetailScreenState extends State { entry.value, style: TextStyle( fontSize: 13, - color: AppColors.violet600, + color: _colorScheme.tertiary, ), ), const SizedBox(width: AppSpacing.xs), @@ -789,7 +810,7 @@ class _WorkMemoryDetailScreenState extends State { child: Icon( Icons.close, size: 14, - color: AppColors.violet500, + color: _colorScheme.tertiary, ), ), ], @@ -805,21 +826,28 @@ class _WorkMemoryDetailScreenState extends State { vertical: AppSpacing.sm, ), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: _colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.full), border: Border.all( - color: AppColors.borderSecondary, + color: _colorScheme.outlineVariant, style: BorderStyle.solid, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.add, size: 14, color: AppColors.slate500), + Icon( + Icons.add, + size: 14, + color: _colorScheme.onSurfaceVariant, + ), const SizedBox(width: AppSpacing.xs), Text( context.l10n.contactsAdd, - style: TextStyle(fontSize: 13, color: AppColors.slate500), + style: TextStyle( + fontSize: 13, + color: _colorScheme.onSurfaceVariant, + ), ), ], ), @@ -870,24 +898,24 @@ class _WorkMemoryDetailScreenState extends State { child: Container( padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: _colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all( - color: AppColors.borderSecondary, + color: _colorScheme.outlineVariant, style: BorderStyle.solid, ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.add, size: 18, color: AppColors.violet500), + Icon(Icons.add, size: 18, color: _colorScheme.tertiary), const SizedBox(width: AppSpacing.xs), Text( text, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: AppColors.violet600, + color: _colorScheme.tertiary, ), ), ], diff --git a/apps/lib/features/settings/presentation/screens/work_memory_view_screen.dart b/apps/lib/features/settings/presentation/screens/work_memory_view_screen.dart index ce45b6a..8cc541f 100644 --- a/apps/lib/features/settings/presentation/screens/work_memory_view_screen.dart +++ b/apps/lib/features/settings/presentation/screens/work_memory_view_screen.dart @@ -94,16 +94,22 @@ class _WorkMemoryViewScreenState extends State { } Widget _buildErrorState() { + final colorScheme = Theme.of(context).colorScheme; + return Center( child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.error_outline, size: 48, color: AppColors.slate300), + Icon( + Icons.error_outline, + size: 48, + color: colorScheme.onSurfaceVariant, + ), const SizedBox(height: AppSpacing.md), Text( _error ?? context.l10n.memoryLoadFailedRetry, - style: TextStyle(color: AppColors.slate500), + style: TextStyle(color: colorScheme.onSurfaceVariant), ), const SizedBox(height: AppSpacing.lg), AppPressable( @@ -115,16 +121,16 @@ class _WorkMemoryViewScreenState extends State { vertical: AppSpacing.sm, ), decoration: BoxDecoration( - color: AppColors.blue50, + color: colorScheme.primaryContainer, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.blue100), + border: Border.all(color: colorScheme.outlineVariant), ), child: Text( context.l10n.memoryReload, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: AppColors.blue600, + color: colorScheme.primary, ), ), ), @@ -249,12 +255,14 @@ class _WorkMemoryViewScreenState extends State { required IconData icon, required List children, }) { + final colorScheme = Theme.of(context).colorScheme; + return Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -266,18 +274,18 @@ class _WorkMemoryViewScreenState extends State { width: 28, height: 28, decoration: BoxDecoration( - color: AppColors.violet500.withValues(alpha: 0.1), + color: colorScheme.tertiaryContainer, borderRadius: BorderRadius.circular(AppRadius.md), ), - child: Icon(icon, size: 16, color: AppColors.violet600), + child: Icon(icon, size: 16, color: colorScheme.tertiary), ), const SizedBox(width: AppSpacing.sm), Text( title, - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, - color: AppColors.slate900, + color: colorScheme.onSurface, ), ), ], @@ -295,11 +303,13 @@ class _WorkMemoryViewScreenState extends State { String value, { bool multiline = false, }) { + final colorScheme = Theme.of(context).colorScheme; + return Container( margin: const EdgeInsets.only(bottom: AppSpacing.sm), padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.md), ), child: Row( @@ -307,7 +317,7 @@ class _WorkMemoryViewScreenState extends State { ? CrossAxisAlignment.start : CrossAxisAlignment.center, children: [ - Icon(icon, size: 16, color: AppColors.slate500), + Icon(icon, size: 16, color: colorScheme.onSurfaceVariant), const SizedBox(width: AppSpacing.sm), Expanded( child: Column( @@ -318,16 +328,16 @@ class _WorkMemoryViewScreenState extends State { style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 2), Text( value, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: AppColors.slate800, + color: colorScheme.onSurface, ), ), ], @@ -339,6 +349,8 @@ class _WorkMemoryViewScreenState extends State { } Widget _buildProjects(List projects) { + final colorScheme = Theme.of(context).colorScheme; + if (projects.isEmpty) { return _buildEmptyTip(context.l10n.settingsWorkMemoryEmptyProjects); } @@ -349,9 +361,9 @@ class _WorkMemoryViewScreenState extends State { margin: const EdgeInsets.only(bottom: AppSpacing.sm), padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.borderTertiary), + border: Border.all(color: colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -411,6 +423,8 @@ class _WorkMemoryViewScreenState extends State { } Widget _buildTeamMembers(List members) { + final colorScheme = Theme.of(context).colorScheme; + if (members.isEmpty) { return _buildEmptyTip(context.l10n.settingsWorkMemoryEmptyTeamMembers); } @@ -421,9 +435,9 @@ class _WorkMemoryViewScreenState extends State { margin: const EdgeInsets.only(bottom: AppSpacing.sm), padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.borderTertiary), + border: Border.all(color: colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -461,6 +475,8 @@ class _WorkMemoryViewScreenState extends State { } Widget _buildTags(List tags) { + final colorScheme = Theme.of(context).colorScheme; + if (tags.isEmpty) { return _buildEmptyTip(context.l10n.memoryNoInfo); } @@ -474,24 +490,24 @@ class _WorkMemoryViewScreenState extends State { vertical: AppSpacing.sm, ), decoration: BoxDecoration( - color: AppColors.violet500.withValues(alpha: 0.1), + color: colorScheme.tertiaryContainer, borderRadius: BorderRadius.circular(AppRadius.full), border: Border.all( - color: AppColors.violet500.withValues(alpha: 0.25), + color: colorScheme.tertiary.withValues(alpha: 0.25), ), ), child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Icon(Icons.label_outline, size: 14, color: AppColors.violet500), + Icon(Icons.label_outline, size: 14, color: colorScheme.tertiary), const SizedBox(width: AppSpacing.xs), Text( tag, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, - color: AppColors.violet600, + color: colorScheme.onTertiaryContainer, ), ), ], @@ -502,24 +518,30 @@ class _WorkMemoryViewScreenState extends State { } Widget _buildEmptyTip(String text) { + final colorScheme = Theme.of(context).colorScheme; + return Container( padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.borderTertiary), + border: Border.all(color: colorScheme.outlineVariant), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Icon(Icons.inbox_outlined, size: 16, color: AppColors.slate400), + Icon( + Icons.inbox_outlined, + size: 16, + color: colorScheme.onSurfaceVariant, + ), const SizedBox(width: AppSpacing.sm), Text( text, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), ), ], diff --git a/apps/lib/features/settings/presentation/widgets/account_section_card.dart b/apps/lib/features/settings/presentation/widgets/account_section_card.dart index 9ab7e2f..a6bba9e 100644 --- a/apps/lib/features/settings/presentation/widgets/account_section_card.dart +++ b/apps/lib/features/settings/presentation/widgets/account_section_card.dart @@ -8,27 +8,29 @@ class AccountSectionCard extends StatelessWidget { this.title, this.description, required this.child, - this.backgroundColor = AppColors.white, - this.borderColor = AppColors.borderSecondary, + this.backgroundColor, + this.borderColor, this.contentPadding = const EdgeInsets.all(AppSpacing.lg), }); final String? title; final String? description; final Widget child; - final Color backgroundColor; - final Color borderColor; + final Color? backgroundColor; + final Color? borderColor; final EdgeInsetsGeometry contentPadding; @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Container( width: double.infinity, padding: contentPadding, decoration: BoxDecoration( - color: backgroundColor, + color: backgroundColor ?? colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: borderColor), + border: Border.all(color: borderColor ?? colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -36,10 +38,10 @@ class AccountSectionCard extends StatelessWidget { if (title != null) ...[ Text( title!, - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, - color: AppColors.slate900, + color: colorScheme.onSurface, ), ), ], @@ -47,10 +49,10 @@ class AccountSectionCard extends StatelessWidget { const SizedBox(height: AppSpacing.xs), Text( description!, - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), ), ], diff --git a/apps/lib/features/settings/presentation/widgets/settings_page_scaffold.dart b/apps/lib/features/settings/presentation/widgets/settings_page_scaffold.dart index ec74760..6341d40 100644 --- a/apps/lib/features/settings/presentation/widgets/settings_page_scaffold.dart +++ b/apps/lib/features/settings/presentation/widgets/settings_page_scaffold.dart @@ -25,8 +25,10 @@ class SettingsPageScaffold extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Scaffold( - backgroundColor: AppColors.surfaceSecondary, + backgroundColor: colorScheme.surfaceContainerLow, resizeToAvoidBottomInset: resizeOnKeyboard, body: SafeArea( maintainBottomViewPadding: maintainBottomViewPadding, diff --git a/apps/lib/features/todo/data/todo_repository.dart b/apps/lib/features/todo/data/todo_repository.dart index cebdf6d..ce95e1c 100644 --- a/apps/lib/features/todo/data/todo_repository.dart +++ b/apps/lib/features/todo/data/todo_repository.dart @@ -1,57 +1,49 @@ import 'dart:async'; -import '../../../core/cache/cache_entry.dart'; -import '../../../core/cache/cache_invalidator.dart'; -import '../../../core/cache/hybrid_cache_store.dart'; +import '../../../data/cache/cache_entry.dart'; +import '../../../data/cache/cache_invalidator.dart'; +import '../../../data/cache/cache_policy.dart'; +import '../../../data/cache/cached_repository.dart'; import 'todo_api.dart'; -class TodoRepository { +class TodoRepository extends CachedRepository> { static const String pendingListKey = 'todo:list:pending'; final TodoApi api; - final HybridCacheStore store; final CacheInvalidator invalidator; - final DateTime Function() now; TodoRepository({ required this.api, - required this.store, + required super.store, required this.invalidator, - DateTime Function()? now, - }) : now = now ?? DateTime.now; + super.now, + }) : super( + policy: const CachePolicy( + softTtl: Duration(days: 3650), + hardTtl: Duration(days: 3650), + minRefreshInterval: Duration(days: 3650), + ), + ); Future> getPendingTodos({ bool forceRefresh = false, }) async { - if (!forceRefresh) { - final cached = await store.read>>( - pendingListKey, - ); - if (cached != null) { - return cached.value; - } - } - - final remote = await api.getPendingTodos(); - await store.write>>( - pendingListKey, - CacheEntry(value: remote, fetchedAt: now()), + return getOrLoad( + key: pendingListKey, + forceRefresh: forceRefresh, + loadFromRemote: api.getPendingTodos, ); - return remote; } Future completeTodo(String id) async { - final cached = await store.read>>( + final CacheEntry>? cached = await readCacheEntry( pendingListKey, ); if (cached != null) { final next = cached.value .where((todo) => todo.id != id) .toList(growable: false); - await store.write>>( - pendingListKey, - CacheEntry(value: next, fetchedAt: now()), - ); + await writeCacheEntry(pendingListKey, next); } try { diff --git a/apps/lib/features/todo/presentation/screens/todo_detail_screen.dart b/apps/lib/features/todo/presentation/screens/todo_detail_screen.dart index 23b337b..d6cd3fe 100644 --- a/apps/lib/features/todo/presentation/screens/todo_detail_screen.dart +++ b/apps/lib/features/todo/presentation/screens/todo_detail_screen.dart @@ -5,7 +5,6 @@ import 'package:lucide_icons/lucide_icons.dart'; import '../../../../app/di/injection.dart'; import '../../../../app/router/app_routes.dart'; import '../../../../core/l10n/l10n.dart'; -import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/back_title_page_header.dart'; import '../../../../shared/widgets/detail_header_action_menu.dart'; import '../../../../shared/widgets/destructive_action_sheet.dart'; @@ -84,30 +83,31 @@ class _TodoDetailScreenState extends State { } } - Color _getPriorityColor(int priority) { + Color _getPriorityColor(int priority, ColorScheme colorScheme) { switch (priority) { case 1: - return AppColors.g1Text; + return colorScheme.error; case 2: - return AppColors.g3Text; + return colorScheme.tertiary; case 3: - return AppColors.g2Text; + return colorScheme.primary; default: - return AppColors.slate500; + return colorScheme.onSurfaceVariant; } } @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Scaffold( - backgroundColor: AppColors.todoBg, + backgroundColor: colorScheme.surface, body: SafeArea( child: Container( - decoration: const BoxDecoration( + decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [AppColors.homeBackgroundTop, AppColors.todoBg], + colors: [colorScheme.surfaceContainerHigh, colorScheme.surface], ), ), child: Column( @@ -164,6 +164,7 @@ class _TodoDetailScreenState extends State { } Widget _buildContent() { + final colorScheme = Theme.of(context).colorScheme; if (_isLoading) { return const FullScreenLoading(); } @@ -192,7 +193,7 @@ class _TodoDetailScreenState extends State { fontFamily: 'Inter', fontSize: 12, fontWeight: FontWeight.w500, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 10), @@ -201,7 +202,7 @@ class _TodoDetailScreenState extends State { id: item.id, title: item.title, time: _formatEventTime(item.startAt, item.endAt), - borderColor: AppColors.todoEventBorder1, + borderColor: colorScheme.outlineVariant, onTap: () => context.push(AppRoutes.calendarEventDetail(item.id)), ), @@ -225,60 +226,61 @@ class _TodoDetailScreenState extends State { } Widget _buildMainCard() { + final colorScheme = Theme.of(context).colorScheme; return Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: AppColors.todoCardBg, + color: colorScheme.surface, borderRadius: BorderRadius.circular(14), - border: Border.all(color: AppColors.todoDetailCardBorder, width: 1), + border: Border.all(color: colorScheme.outlineVariant, width: 1), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _todo!.title, - style: const TextStyle( + style: TextStyle( fontFamily: 'Inter', fontSize: 18, fontWeight: FontWeight.w700, - color: AppColors.slate900, + color: colorScheme.onSurface, ), ), const SizedBox(height: 4), Text( _buildSubtitle(), - style: const TextStyle( + style: TextStyle( fontFamily: 'Inter', fontSize: 12, fontWeight: FontWeight.w500, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), ), if (_todo!.description != null && _todo!.description!.isNotEmpty) ...[ const SizedBox(height: 8), Text( _todo!.description!, - style: const TextStyle( + style: TextStyle( fontFamily: 'Inter', fontSize: 13, - color: AppColors.slate600, + color: colorScheme.onSurfaceVariant, ), ), ], const SizedBox(height: 8), - Container(height: 1, color: AppColors.border), + Container(height: 1, color: colorScheme.outlineVariant), const SizedBox(height: 8), _buildInfoRow( label: context.l10n.todoPriorityQuadrant, value: _getPriorityLabel(_todo!.priority), - valueColor: _getPriorityColor(_todo!.priority), + valueColor: _getPriorityColor(_todo!.priority, colorScheme), ), const SizedBox(height: 8), _buildInfoRow( label: context.l10n.todoLinkedCalendarEvents, value: context.l10n.todoItemCount(_todo!.scheduleItems.length), - valueColor: AppColors.g3Text, + valueColor: colorScheme.tertiary, ), const SizedBox(height: 8), _buildInfoRow( @@ -287,8 +289,8 @@ class _TodoDetailScreenState extends State { ? context.l10n.todoStatusDone : context.l10n.todoStatusInProgress, valueColor: _todo!.status == 'done' - ? AppColors.success - : AppColors.blue600, + ? colorScheme.tertiary + : colorScheme.primary, ), ], ), @@ -311,16 +313,17 @@ class _TodoDetailScreenState extends State { required String value, required Color valueColor, }) { + final colorScheme = Theme.of(context).colorScheme; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, - style: const TextStyle( + style: TextStyle( fontFamily: 'Inter', fontSize: 12, fontWeight: FontWeight.w600, - color: AppColors.slate400, + color: colorScheme.outline, ), ), Text( @@ -343,6 +346,7 @@ class _TodoDetailScreenState extends State { required Color borderColor, VoidCallback? onTap, }) { + final colorScheme = Theme.of(context).colorScheme; return GestureDetector( onTap: onTap, child: Container( @@ -350,7 +354,7 @@ class _TodoDetailScreenState extends State { padding: const EdgeInsets.all(10), margin: const EdgeInsets.only(bottom: 10), decoration: BoxDecoration( - color: AppColors.todoCardBg, + color: colorScheme.surface, borderRadius: BorderRadius.circular(14), border: Border.all(color: borderColor, width: 1), ), @@ -359,21 +363,21 @@ class _TodoDetailScreenState extends State { children: [ Text( title, - style: const TextStyle( + style: TextStyle( fontFamily: 'Inter', fontSize: 13, fontWeight: FontWeight.w600, - color: AppColors.slate700, + color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 8), Text( time, - style: const TextStyle( + style: TextStyle( fontFamily: 'Inter', fontSize: 12, fontWeight: FontWeight.w500, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), ), ], diff --git a/apps/lib/features/todo/presentation/screens/todo_edit_screen.dart b/apps/lib/features/todo/presentation/screens/todo_edit_screen.dart index af888d5..33a84d6 100644 --- a/apps/lib/features/todo/presentation/screens/todo_edit_screen.dart +++ b/apps/lib/features/todo/presentation/screens/todo_edit_screen.dart @@ -3,6 +3,8 @@ import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import '../../../../app/di/injection.dart'; +import '../../../../data/repositories/calendar_event_repository.dart'; +import '../../../../data/repositories/models/calendar_event.dart'; import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_button.dart'; @@ -13,8 +15,6 @@ import '../../../../shared/widgets/error_retry_surface.dart'; import '../../../../shared/widgets/full_screen_loading.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; -import '../../../calendar/data/calendar_api.dart'; -import '../../../calendar/data/models/schedule_item_model.dart'; import '../../data/todo_api.dart'; class TodoEditScreen extends StatefulWidget { @@ -32,7 +32,8 @@ class TodoEditScreen extends StatefulWidget { class _TodoEditScreenState extends State { final TodoApi _todoApi = sl(); - final CalendarApi _calendarApi = sl(); + final CalendarEventRepository _calendarRepository = + sl(); final TextEditingController _titleController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController(); @@ -46,6 +47,8 @@ class _TodoEditScreenState extends State { final Set _selectedScheduleItemIds = {}; List<_ScheduleItemSimple> _scheduleItems = const <_ScheduleItemSimple>[]; + ColorScheme get _colorScheme => Theme.of(context).colorScheme; + @override void initState() { super.initState(); @@ -69,7 +72,7 @@ class _TodoEditScreenState extends State { final now = DateTime.now(); final start = now.subtract(const Duration(days: 30)); final end = now.add(const Duration(days: 90)); - final scheduleItems = await _calendarApi.listByRange( + final scheduleItems = await _calendarRepository.listByRange( startAt: start, endAt: end, ); @@ -91,7 +94,7 @@ class _TodoEditScreenState extends State { ..clear() ..addAll(todo?.scheduleItems.map((item) => item.id) ?? const []); _scheduleItems = scheduleItems - .where((item) => item.status == ScheduleStatus.active) + .where((item) => item.status == CalendarEventStatus.active) .map( (item) => _ScheduleItemSimple( id: item.id, @@ -118,18 +121,21 @@ class _TodoEditScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: AppColors.todoBg, + backgroundColor: _colorScheme.surface, resizeToAvoidBottomInset: false, body: SafeArea( child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () => FocusScope.of(context).unfocus(), child: Container( - decoration: const BoxDecoration( + decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [AppColors.homeBackgroundTop, AppColors.todoBg], + colors: [ + _colorScheme.surfaceContainerLow, + _colorScheme.surface, + ], ), ), child: Column( @@ -184,15 +190,21 @@ class _TodoEditScreenState extends State { } Widget _buildHeaderCard() { + final headerDesc = widget.isCreateMode + ? context.l10n.todoInfoDescCreate + : _todo?.status == 'done' + ? context.l10n.todoInfoDescDone + : context.l10n.todoInfoDescDefault; + return Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( - color: AppColors.white, + color: _colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.borderTertiary), + border: Border.all(color: _colorScheme.outlineVariant), boxShadow: [ BoxShadow( - color: AppColors.slate200.withValues(alpha: 0.34), + color: _colorScheme.shadow.withValues(alpha: 0.18), blurRadius: AppRadius.lg, offset: const Offset(0, AppSpacing.xs), ), @@ -206,17 +218,16 @@ class _TodoEditScreenState extends State { style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, - color: AppColors.slate900, + color: _colorScheme.onSurface, ), ), const SizedBox(height: AppSpacing.xs), Text( - widget.isCreateMode - ? context.l10n.todoInfoDescCreate - : _todo?.status == 'done' - ? context.l10n.todoInfoDescDone - : context.l10n.todoInfoDescDefault, - style: const TextStyle(fontSize: 13, color: AppColors.slate500), + headerDesc, + style: TextStyle( + fontSize: 13, + color: _colorScheme.onSurfaceVariant, + ), ), ], ), @@ -227,9 +238,9 @@ class _TodoEditScreenState extends State { return Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( - color: AppColors.white, + color: _colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -252,7 +263,7 @@ class _TodoEditScreenState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: AppColors.slate700, + color: _colorScheme.onSurface, ), ), const SizedBox(height: AppSpacing.sm), @@ -263,22 +274,22 @@ class _TodoEditScreenState extends State { _PriorityPill( label: context.l10n.todoQuadrantImportantUrgent, selected: _priority == 1, - borderColor: AppColors.g1Border, - activeColor: AppColors.g1Text, + borderColor: _colorScheme.error, + activeColor: _colorScheme.error, onTap: () => setState(() => _priority = 1), ), _PriorityPill( label: context.l10n.todoQuadrantUrgentNotImportant, selected: _priority == 3, - borderColor: AppColors.g2Border, - activeColor: AppColors.g2Text, + borderColor: _colorScheme.primary, + activeColor: _colorScheme.primary, onTap: () => setState(() => _priority = 3), ), _PriorityPill( label: context.l10n.todoQuadrantImportantNotUrgent, selected: _priority == 2, - borderColor: AppColors.g3Border, - activeColor: AppColors.g3Text, + borderColor: _colorScheme.tertiary, + activeColor: _colorScheme.tertiary, onTap: () => setState(() => _priority = 2), ), ], @@ -292,9 +303,9 @@ class _TodoEditScreenState extends State { return Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: _colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: _colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -307,16 +318,16 @@ class _TodoEditScreenState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: AppColors.slate700, + color: _colorScheme.onSurface, ), ), const Spacer(), Text( context.l10n.todoItemCount(_selectedScheduleItemIds.length), - style: const TextStyle( + style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, - color: AppColors.slate500, + color: _colorScheme.onSurfaceVariant, ), ), ], @@ -328,7 +339,7 @@ class _TodoEditScreenState extends State { child: Center( child: Text( context.l10n.todoNoSelectableCalendarEvents, - style: const TextStyle(color: AppColors.slate500), + style: TextStyle(color: _colorScheme.onSurfaceVariant), ), ), ) @@ -375,8 +386,8 @@ class _TodoEditScreenState extends State { AppSpacing.lg, ), decoration: BoxDecoration( - color: AppColors.white.withValues(alpha: 0.9), - border: const Border(top: BorderSide(color: AppColors.borderSecondary)), + color: _colorScheme.surface.withValues(alpha: 0.9), + border: Border(top: BorderSide(color: _colorScheme.outlineVariant)), ), child: AppButton( text: _saving @@ -479,6 +490,8 @@ class _PriorityPill extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return AppPressable( onTap: onTap, borderRadius: BorderRadius.circular(AppRadius.full), @@ -492,10 +505,10 @@ class _PriorityPill extends StatelessWidget { decoration: BoxDecoration( color: selected ? borderColor.withValues(alpha: 0.28) - : AppColors.white, + : colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.full), border: Border.all( - color: selected ? borderColor : AppColors.slate300, + color: selected ? borderColor : colorScheme.outlineVariant, width: selected ? 1.5 : 1, ), ), @@ -504,7 +517,7 @@ class _PriorityPill extends StatelessWidget { style: TextStyle( fontSize: 12, fontWeight: selected ? FontWeight.w600 : FontWeight.w500, - color: selected ? activeColor : AppColors.slate600, + color: selected ? activeColor : colorScheme.onSurfaceVariant, ), ), ), @@ -527,6 +540,8 @@ class _ScheduleSelectableTile extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return AppPressable( onTap: onTap, borderRadius: BorderRadius.circular(AppRadius.lg), @@ -535,10 +550,10 @@ class _ScheduleSelectableTile extends StatelessWidget { curve: Curves.easeOut, padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: selected ? AppColors.surfaceInfoLight : AppColors.white, + color: selected ? colorScheme.primaryContainer : colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.lg), border: Border.all( - color: selected ? AppColors.borderQuaternary : AppColors.border, + color: selected ? colorScheme.primary : colorScheme.outlineVariant, ), ), child: Row( @@ -552,18 +567,18 @@ class _ScheduleSelectableTile extends StatelessWidget { title, maxLines: 1, overflow: TextOverflow.ellipsis, - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, - color: AppColors.slate800, + color: colorScheme.onSurface, ), ), const SizedBox(height: AppSpacing.xs), Text( subtitle, - style: const TextStyle( + style: TextStyle( fontSize: 12, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), ), ], @@ -575,14 +590,16 @@ class _ScheduleSelectableTile extends StatelessWidget { width: AppSpacing.lg, height: AppSpacing.lg, decoration: BoxDecoration( - color: selected ? AppColors.blue600 : AppColors.white, + color: selected ? colorScheme.primary : colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.full), border: Border.all( - color: selected ? AppColors.blue600 : AppColors.slate300, + color: selected + ? colorScheme.primary + : colorScheme.outlineVariant, ), ), child: selected - ? const Icon(Icons.check, size: 12, color: AppColors.white) + ? Icon(Icons.check, size: 12, color: colorScheme.onPrimary) : null, ), ], diff --git a/apps/lib/features/todo/presentation/screens/todo_quadrants_screen.dart b/apps/lib/features/todo/presentation/screens/todo_quadrants_screen.dart index 921a0e7..0ccc9fb 100644 --- a/apps/lib/features/todo/presentation/screens/todo_quadrants_screen.dart +++ b/apps/lib/features/todo/presentation/screens/todo_quadrants_screen.dart @@ -4,18 +4,18 @@ import 'package:go_router/go_router.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../../../../app/di/injection.dart'; import '../../../../app/router/app_routes.dart'; +import '../../../../app/router/home_return_policy.dart'; import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; -import '../../../home/presentation/navigation/home_return_policy.dart'; import '../../../../shared/widgets/app_pull_refresh_feedback.dart'; import '../../../../shared/widgets/app_pressable.dart'; import '../../../../shared/widgets/back_title_page_header.dart'; import '../../../../shared/widgets/error_retry_surface.dart'; import '../../../../shared/widgets/full_screen_loading.dart'; +import '../../../../shared/widgets/bottom_dock.dart'; +import '../../../../shared/state/calendar_state_manager.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; -import '../../../calendar/presentation/calendar_state_manager.dart'; -import '../../../calendar/presentation/widgets/bottom_dock.dart'; import '../../data/todo_api.dart'; import '../../data/todo_repository.dart'; @@ -305,8 +305,9 @@ class _TodoQuadrantsScreenState extends State { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Scaffold( - backgroundColor: AppColors.todoBg, + backgroundColor: colorScheme.surface, body: PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) { @@ -328,6 +329,7 @@ class _TodoQuadrantsScreenState extends State { } Widget _buildHeader() { + final colorScheme = Theme.of(context).colorScheme; return BackTitlePageHeader( title: context.l10n.todoScreenTitle, showBackButton: false, @@ -342,20 +344,20 @@ class _TodoQuadrantsScreenState extends State { width: 36, height: 36, decoration: BoxDecoration( - color: AppColors.blue600, + color: colorScheme.primary, borderRadius: BorderRadius.circular(AppRadius.full), boxShadow: [ BoxShadow( - color: AppColors.blue300.withValues(alpha: 0.28), + color: colorScheme.primary.withValues(alpha: 0.28), blurRadius: AppRadius.lg, offset: const Offset(0, AppSpacing.xs), ), ], ), - child: const Icon( + child: Icon( LucideIcons.plus, size: 18, - color: AppColors.white, + color: colorScheme.onPrimary, ), ), ), @@ -394,29 +396,31 @@ class _TodoQuadrantsScreenState extends State { } Widget _buildDragBoard() { + final colorScheme = Theme.of(context).colorScheme; + final palette = Theme.of(context).extension()!; final quadrants = [ _QuadrantMeta( value: 1, title: context.l10n.todoQuadrantImportantUrgent, - textColor: AppColors.g1Text, - dividerColor: AppColors.g1Divider, - borderColor: AppColors.g1Border, + textColor: palette.g1Text, + dividerColor: palette.g1Divider, + borderColor: palette.g1Border, items: _importantUrgent, ), _QuadrantMeta( value: 3, title: context.l10n.todoQuadrantUrgentNotImportant, - textColor: AppColors.g2Text, - dividerColor: AppColors.g2Divider, - borderColor: AppColors.g2Border, + textColor: palette.g3Text, + dividerColor: palette.g3Divider, + borderColor: palette.g3Border, items: _urgentNotImportant, ), _QuadrantMeta( value: 2, title: context.l10n.todoQuadrantImportantNotUrgent, - textColor: AppColors.g3Text, - dividerColor: AppColors.g3Divider, - borderColor: AppColors.g3Border, + textColor: palette.g2Text, + dividerColor: palette.g2Divider, + borderColor: palette.g2Border, items: _importantNotUrgent, ), ]; @@ -429,7 +433,7 @@ class _TodoQuadrantsScreenState extends State { contentsWhenEmpty: _buildEmptyQuadrant(), lastTarget: const SizedBox(height: AppSpacing.lg), decoration: BoxDecoration( - color: AppColors.todoCardBg, + color: colorScheme.surface, borderRadius: BorderRadius.circular(14), border: Border.all(color: meta.borderColor), ), @@ -468,16 +472,16 @@ class _TodoQuadrantsScreenState extends State { listDivider: const SizedBox(height: AppSpacing.md), itemDivider: Padding( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), - child: Container(height: 1, color: AppColors.slate100), + child: Container(height: 1, color: colorScheme.surfaceContainerHigh), ), listPadding: EdgeInsets.zero, itemDecorationWhileDragging: BoxDecoration( - color: AppColors.todoCardBg, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: colorScheme.outlineVariant), boxShadow: [ BoxShadow( - color: AppColors.slate200.withValues(alpha: 0.6), + color: colorScheme.shadow.withValues(alpha: 0.16), blurRadius: AppRadius.md, offset: const Offset(0, AppSpacing.xs), ), @@ -530,6 +534,7 @@ class _TodoQuadrantsScreenState extends State { } Widget _buildEmptyQuadrant() { + final colorScheme = Theme.of(context).colorScheme; return SizedBox( height: 60, child: Center( @@ -538,7 +543,7 @@ class _TodoQuadrantsScreenState extends State { style: TextStyle( fontFamily: 'Inter', fontSize: 13, - color: AppColors.slate400, + color: colorScheme.outline, ), ), ), @@ -645,6 +650,7 @@ class _TodoItemWidgetState extends State<_TodoItemWidget> @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return GestureDetector( onTap: widget.onTap, child: SizedBox( @@ -656,11 +662,11 @@ class _TodoItemWidgetState extends State<_TodoItemWidget> Expanded( child: Text( widget.item.title, - style: const TextStyle( + style: TextStyle( fontFamily: 'Inter', fontSize: 13, fontWeight: FontWeight.w600, - color: AppColors.slate700, + color: colorScheme.onSurfaceVariant, ), ), ), @@ -673,11 +679,13 @@ class _TodoItemWidgetState extends State<_TodoItemWidget> width: 20, height: 20, decoration: BoxDecoration( - color: _isChecked ? AppColors.blue600 : Colors.white, + color: _isChecked + ? colorScheme.primary + : colorScheme.surface, border: Border.all( color: _isChecked - ? AppColors.blue600 - : AppColors.slate300, + ? colorScheme.primary + : colorScheme.outlineVariant, width: 1.5, ), borderRadius: BorderRadius.circular(4), @@ -685,10 +693,10 @@ class _TodoItemWidgetState extends State<_TodoItemWidget> child: _isChecked ? Transform.scale( scale: _scaleAnimation.value, - child: const Icon( + child: Icon( Icons.check, size: 14, - color: Colors.white, + color: colorScheme.onPrimary, ), ) : null, diff --git a/apps/lib/features/todo/presentation/widgets/todo_drag_item.dart b/apps/lib/features/todo/presentation/widgets/todo_drag_item.dart index c51fed4..75b435f 100644 --- a/apps/lib/features/todo/presentation/widgets/todo_drag_item.dart +++ b/apps/lib/features/todo/presentation/widgets/todo_drag_item.dart @@ -30,7 +30,7 @@ class TodoDragItem extends StatelessWidget { borderRadius: BorderRadius.circular(AppRadius.md), child: Transform.scale( scale: 1.03, - child: SizedBox(width: 280, child: _buildDragFeedback()), + child: SizedBox(width: 280, child: _buildDragFeedback(context)), ), ), childWhenDragging: AnimatedOpacity( @@ -44,18 +44,19 @@ class TodoDragItem extends StatelessWidget { ); } - Widget _buildDragFeedback() { + Widget _buildDragFeedback(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Container( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: AppSpacing.sm, ), decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.md), boxShadow: [ BoxShadow( - color: AppColors.slate400.withValues(alpha: 0.3), + color: colorScheme.shadow.withValues(alpha: 0.2), blurRadius: 12, offset: const Offset(0, 4), ), @@ -65,10 +66,10 @@ class TodoDragItem extends StatelessWidget { todo.title, maxLines: 1, overflow: TextOverflow.ellipsis, - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, - color: AppColors.slate700, + color: colorScheme.onSurfaceVariant, ), ), ); diff --git a/apps/lib/features/ui_schema/presentation/widgets/ui_schema_renderer.dart b/apps/lib/features/ui_schema/presentation/widgets/ui_schema_renderer.dart index ac5ee86..afde8dc 100644 --- a/apps/lib/features/ui_schema/presentation/widgets/ui_schema_renderer.dart +++ b/apps/lib/features/ui_schema/presentation/widgets/ui_schema_renderer.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; import 'package:social_app/core/l10n/l10n.dart'; import 'package:social_app/core/theme/design_tokens.dart'; -import 'package:social_app/shared/widgets/toast/toast.dart'; -import 'package:social_app/shared/widgets/toast/toast_type.dart'; -import '../navigation/ui_schema_navigation.dart'; class UiSchemaRenderer { - static Widget renderSchema(Map? schema) { + final ColorScheme colorScheme; + + UiSchemaRenderer(this.colorScheme); + + Widget renderSchema(Map? schema) { if (schema == null || schema.isEmpty) { return const SizedBox.shrink(); } @@ -19,7 +19,7 @@ class UiSchemaRenderer { return _renderLayoutNode(root); } - static Widget _renderLayoutNode(Map node) { + Widget _renderLayoutNode(Map node) { final type = _asString(node['type']); return switch (type) { 'stack' => _renderStack(node), @@ -28,7 +28,7 @@ class UiSchemaRenderer { }; } - static Widget _renderNode(Map node) { + Widget _renderNode(Map node) { final type = _asString(node['type']); if (node['visible'] == false) { return const SizedBox.shrink(); @@ -46,7 +46,7 @@ class UiSchemaRenderer { }; } - static Widget _renderStack(Map node) { + Widget _renderStack(Map node) { final children = _asList( node['children'], ).whereType>().map(_renderNode).toList(); @@ -71,7 +71,7 @@ class UiSchemaRenderer { return _wrapSurface(node, content); } - static Widget _renderGrid(Map node) { + Widget _renderGrid(Map node) { final children = _asList( node['children'], ).whereType>().map(_renderNode).toList(); @@ -91,34 +91,34 @@ class UiSchemaRenderer { ); } - static Widget _renderText(Map node) { + Widget _renderText(Map node) { final role = _asString(node['role'], fallback: 'body'); final status = _asString(node['status']); final style = switch (role) { - 'title' => const TextStyle( + 'title' => TextStyle( fontSize: 18, fontWeight: FontWeight.w700, - color: AppColors.slate900, + color: colorScheme.onSurface, height: 1.2, ), - 'subtitle' => const TextStyle( + 'subtitle' => TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: AppColors.slate800, + color: colorScheme.onSurface, ), - 'caption' => const TextStyle( + 'caption' => TextStyle( fontSize: 11, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, height: 1.4, ), - 'code' => const TextStyle( + 'code' => TextStyle( fontSize: 12, - color: AppColors.slate700, + color: colorScheme.onSurfaceVariant, fontFamily: 'monospace', ), - _ => const TextStyle( + _ => TextStyle( fontSize: 13, - color: AppColors.slate700, + color: colorScheme.onSurfaceVariant, height: 1.35, ), }; @@ -130,7 +130,7 @@ class UiSchemaRenderer { ); } - static Widget _renderIcon(Map node) { + Widget _renderIcon(Map node) { final value = _asString(node['value']); if (_asString(node['source']) == 'emoji' && value.isNotEmpty) { return Text(value, style: const TextStyle(fontSize: 18)); @@ -138,10 +138,11 @@ class UiSchemaRenderer { return Icon(Icons.bubble_chart_rounded, color: _statusTextColor('', null)); } - static Widget _renderBadge(Map node) { + Widget _renderBadge(Map node) { final status = _asString(node['status']); final fg = - _statusTextColor(status, AppColors.slate700) ?? AppColors.slate700; + _statusTextColor(status, colorScheme.onSurfaceVariant) ?? + colorScheme.onSurfaceVariant; final bg = _statusBackground(status); return Container( padding: const EdgeInsets.symmetric( @@ -160,117 +161,45 @@ class UiSchemaRenderer { ); } - static Widget _renderButton(Map node) { + Widget _renderButton(Map node) { final style = _asString(node['style'], fallback: 'secondary'); final action = _asMap(node['action']); final disabled = node['disabled'] == true; - return Builder( - builder: (context) { - return ElevatedButton( - onPressed: disabled - ? null - : () { - _handleAction(context, action); - }, - style: ElevatedButton.styleFrom( - elevation: 0, - padding: const EdgeInsets.symmetric( - horizontal: AppSpacing.lg, - vertical: AppSpacing.sm, - ), - backgroundColor: style == 'primary' - ? AppColors.authPrimaryButton - : AppColors.surfaceInfoLight, - foregroundColor: style == 'primary' - ? AppColors.white - : AppColors.slate700, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.full), - side: style == 'primary' - ? BorderSide.none - : const BorderSide(color: AppColors.borderTertiary), - ), - ), - child: Text( - _asString( - node['label'], - fallback: L10n.current.uiSchemaActionFallback, - ), - style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600), - ), - ); - }, + return ElevatedButton( + onPressed: disabled + ? null + : () { + _handleAction(action); + }, + style: ElevatedButton.styleFrom( + elevation: 0, + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.lg, + vertical: AppSpacing.sm, + ), + backgroundColor: style == 'primary' + ? colorScheme.primary + : colorScheme.surfaceContainerHighest, + foregroundColor: style == 'primary' + ? colorScheme.onPrimary + : colorScheme.onSurfaceVariant, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadius.full), + side: style == 'primary' + ? BorderSide.none + : BorderSide(color: colorScheme.outlineVariant), + ), + ), + child: Text( + _asString(node['label'], fallback: L10n.current.uiSchemaActionFallback), + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600), + ), ); } - static void _handleAction( - BuildContext context, - Map? action, - ) { - final actionType = _asString(action?['type']); - switch (actionType) { - case 'copy': - Toast.show( - context, - L10n.current.commonCopySuccess, - type: ToastType.success, - ); - return; - case 'navigation': - _handleNavigationAction(context, action); - return; - default: - Toast.show( - context, - L10n.current.uiSchemaActionNotImplemented, - type: ToastType.info, - ); - return; - } - } + void _handleAction(Map? action) {} - static void _handleNavigationAction( - BuildContext context, - Map? action, - ) { - if (action == null) { - Toast.show( - context, - L10n.current.uiSchemaNavigationInvalidParams, - type: ToastType.warning, - ); - return; - } - - final path = _asString(action['path']).trim(); - if (!isValidInternalNavigationPath(path)) { - Toast.show( - context, - L10n.current.uiSchemaNavigationInvalidPath, - type: ToastType.warning, - ); - return; - } - - final params = _asMap(action['params']); - final shouldReplace = action['replace'] == true; - try { - final target = buildUiSchemaNavigationTarget(path: path, params: params); - if (shouldReplace) { - context.replace(target); - return; - } - context.push(target); - } on FormatException { - Toast.show( - context, - L10n.current.uiSchemaNavigationInvalidPath, - type: ToastType.warning, - ); - } - } - - static Widget _renderKv(Map node) { + Widget _renderKv(Map node) { final items = _asList( node['items'], ).whereType>().toList(); @@ -293,7 +222,7 @@ class UiSchemaRenderer { vertical: AppSpacing.xs, ), decoration: BoxDecoration( - color: AppColors.surfaceSecondary, + color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(AppRadius.md), ), child: Row( @@ -303,9 +232,9 @@ class UiSchemaRenderer { flex: 3, child: Text( label, - style: const TextStyle( + style: TextStyle( fontSize: 11, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), ), ), @@ -314,9 +243,9 @@ class UiSchemaRenderer { flex: 5, child: Text( value, - style: const TextStyle( + style: TextStyle( fontSize: 12, - color: AppColors.slate800, + color: colorScheme.onSurface, fontWeight: FontWeight.w600, ), ), @@ -330,30 +259,30 @@ class UiSchemaRenderer { ); } - static Widget _renderDivider(Map node) { + Widget _renderDivider(Map node) { final inset = _asDouble(node['inset'], fallback: 0); return Padding( padding: EdgeInsets.symmetric(horizontal: inset), - child: const Divider(height: 1, color: AppColors.slate200), + child: Divider(height: 1, color: colorScheme.outlineVariant), ); } - static Widget _wrapSurface(Map node, Widget child) { + Widget _wrapSurface(Map node, Widget child) { final appearance = _asString(node['appearance'], fallback: 'plain'); final status = _asString(node['status']); if (appearance == 'plain') { return child; } final bg = switch (appearance) { - 'section' => AppColors.surfaceSecondary, - 'card' => AppColors.white, + 'section' => colorScheme.surfaceContainerHighest, + 'card' => colorScheme.surface, _ => _statusBackground(status), }; final borderColor = switch (status) { - 'success' => AppColors.feedbackSuccessBorder, - 'warning' => AppColors.feedbackWarningBorder, - 'error' => AppColors.feedbackErrorBorder, - _ => AppColors.homeConversationBorder, + 'success' => colorScheme.tertiary, + 'warning' => colorScheme.secondary, + 'error' => colorScheme.error, + _ => colorScheme.outlineVariant, }; return Container( width: double.infinity, @@ -364,7 +293,7 @@ class UiSchemaRenderer { border: Border.all(color: borderColor), boxShadow: [ BoxShadow( - color: AppColors.slate200.withValues(alpha: 0.35), + color: colorScheme.shadow.withValues(alpha: 0.08), blurRadius: 12, offset: const Offset(0, 6), ), @@ -374,25 +303,22 @@ class UiSchemaRenderer { ); } - static Widget _fallback(String text) { + Widget _fallback(String text) { return Container( padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( - color: AppColors.feedbackWarningSurface, - border: Border.all(color: AppColors.feedbackWarningBorder), + color: colorScheme.secondaryContainer, + border: Border.all(color: colorScheme.secondary), borderRadius: BorderRadius.circular(AppRadius.md), ), child: Text( text, - style: const TextStyle( - fontSize: 12, - color: AppColors.feedbackWarningText, - ), + style: TextStyle(fontSize: 12, color: colorScheme.onSecondaryContainer), ), ); } - static List _withGap(List widgets, double gap) { + List _withGap(List widgets, double gap) { if (widgets.isEmpty) return const []; return [ widgets.first, @@ -403,32 +329,32 @@ class UiSchemaRenderer { ]; } - static Color _statusBackground(String status) { + Color _statusBackground(String status) { return switch (status) { - 'success' => AppColors.feedbackSuccessSurface, - 'warning' => AppColors.feedbackWarningSurface, - 'error' => AppColors.feedbackErrorSurface, - 'pending' => AppColors.feedbackInfoSurface, - _ => AppColors.surfaceSecondary, + 'success' => colorScheme.tertiaryContainer, + 'warning' => colorScheme.secondaryContainer, + 'error' => colorScheme.errorContainer, + 'pending' => colorScheme.primaryContainer, + _ => colorScheme.surfaceContainerHighest, }; } - static Color _statusBorder(String status) { + Color _statusBorder(String status) { return switch (status) { - 'success' => AppColors.feedbackSuccessBorder, - 'warning' => AppColors.feedbackWarningBorder, - 'error' => AppColors.feedbackErrorBorder, - 'pending' => AppColors.feedbackInfoBorder, - _ => AppColors.borderTertiary, + 'success' => colorScheme.tertiary, + 'warning' => colorScheme.secondary, + 'error' => colorScheme.error, + 'pending' => colorScheme.primary, + _ => colorScheme.outlineVariant, }; } - static Color? _statusTextColor(String status, Color? fallback) { + Color? _statusTextColor(String status, Color? fallback) { return switch (status) { - 'success' => AppColors.feedbackSuccessText, - 'warning' => AppColors.feedbackWarningText, - 'error' => AppColors.feedbackErrorText, - 'pending' => AppColors.feedbackInfoText, + 'success' => colorScheme.onTertiaryContainer, + 'warning' => colorScheme.onSecondaryContainer, + 'error' => colorScheme.onErrorContainer, + 'pending' => colorScheme.onPrimaryContainer, _ => fallback, }; } diff --git a/apps/lib/main.dart b/apps/lib/main.dart index c94041e..82399c6 100644 --- a/apps/lib/main.dart +++ b/apps/lib/main.dart @@ -1,17 +1,12 @@ import 'package:flutter/material.dart'; -import 'core/constants/app_constants.dart'; +import 'core/config/env.dart'; import 'app/di/injection.dart'; -import 'features/auth/presentation/bloc/auth_bloc.dart'; -import 'features/auth/presentation/bloc/auth_event.dart'; import 'app/app.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await configureDependencies(); - await AppConstants.init(); + await Env.init(); - final authBloc = sl(); - authBloc.add(AuthStarted()); - - runApp(LinksyApp(authBloc: authBloc)); + runApp(const LinksyApp()); } diff --git a/apps/lib/features/calendar/presentation/calendar_state_manager.dart b/apps/lib/shared/state/calendar_state_manager.dart similarity index 100% rename from apps/lib/features/calendar/presentation/calendar_state_manager.dart rename to apps/lib/shared/state/calendar_state_manager.dart diff --git a/apps/lib/shared/widgets/app_button.dart b/apps/lib/shared/widgets/app_button.dart index 5ac74c0..2ed616a 100644 --- a/apps/lib/shared/widgets/app_button.dart +++ b/apps/lib/shared/widgets/app_button.dart @@ -21,6 +21,7 @@ class AppButton extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; final isDisabled = onPressed == null || isLoading; if (isOutlined) { @@ -30,17 +31,15 @@ class AppButton extends StatelessWidget { onPressed: isLoading ? null : onPressed, style: OutlinedButton.styleFrom( backgroundColor: isDisabled - ? AppColors.authSecondaryButtonBackground.withValues( - alpha: 0.55, - ) - : AppColors.authSecondaryButtonBackground, + ? colorScheme.secondaryContainer.withValues(alpha: 0.55) + : colorScheme.secondaryContainer, foregroundColor: isDisabled - ? AppColors.authLinkMuted - : AppColors.authSecondaryButtonText, + ? colorScheme.outline + : colorScheme.onSecondaryContainer, side: BorderSide( color: isDisabled - ? AppColors.authSecondaryButtonBorder.withValues(alpha: 0.7) - : AppColors.authSecondaryButtonBorder, + ? colorScheme.outlineVariant.withValues(alpha: 0.7) + : colorScheme.outlineVariant, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.full), @@ -48,10 +47,10 @@ class AppButton extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xl), ), child: isLoading - ? const AppLoadingIndicator( + ? AppLoadingIndicator( variant: AppLoadingVariant.button, - color: AppColors.authSecondaryButtonText, - trackColor: AppColors.authSecondaryButtonBorder, + color: colorScheme.onSecondaryContainer, + trackColor: colorScheme.outlineVariant, ) : Text( text, @@ -72,7 +71,7 @@ class AppButton extends StatelessWidget { ? const [] : [ BoxShadow( - color: AppColors.blue300.withValues(alpha: 0.24), + color: colorScheme.primary.withValues(alpha: 0.24), blurRadius: 18, offset: const Offset(0, 10), ), @@ -87,19 +86,17 @@ class AppButton extends StatelessWidget { elevation: const WidgetStatePropertyAll(0), backgroundColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.disabled)) { - return AppColors.authPrimaryButtonDisabled; + return colorScheme.surfaceContainerHighest; } if (states.contains(WidgetState.pressed)) { - return AppColors.authPrimaryButtonPressed; + return colorScheme.primary.withValues(alpha: 0.85); } - return AppColors.authPrimaryButton; + return colorScheme.primary; }), - foregroundColor: const WidgetStatePropertyAll( - AppColors.authPrimaryButtonText, - ), + foregroundColor: WidgetStatePropertyAll(colorScheme.onPrimary), overlayColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.pressed)) { - return AppColors.white.withValues(alpha: 0.08); + return colorScheme.onPrimary.withValues(alpha: 0.08); } return null; }), @@ -113,10 +110,10 @@ class AppButton extends StatelessWidget { ), ), child: isLoading - ? const AppLoadingIndicator( + ? AppLoadingIndicator( variant: AppLoadingVariant.button, - color: AppColors.authPrimaryButtonText, - trackColor: AppColors.blue400, + color: colorScheme.onPrimary, + trackColor: colorScheme.primaryContainer, ) : Text( text, @@ -125,8 +122,8 @@ class AppButton extends StatelessWidget { fontWeight: FontWeight.w700, letterSpacing: 0.2, color: isDisabled - ? AppColors.authLinkMuted - : AppColors.authPrimaryButtonText, + ? colorScheme.outline + : colorScheme.onPrimary, ), ), ), diff --git a/apps/lib/shared/widgets/app_input.dart b/apps/lib/shared/widgets/app_input.dart index 34ca7e7..b6c35ca 100644 --- a/apps/lib/shared/widgets/app_input.dart +++ b/apps/lib/shared/widgets/app_input.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import '../../core/theme/design_tokens.dart'; class AppInput extends StatelessWidget { final String label; @@ -25,15 +24,16 @@ class AppInput extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, - style: const TextStyle( + style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, - color: AppColors.slate600, + color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 6), diff --git a/apps/lib/shared/widgets/app_loading_indicator.dart b/apps/lib/shared/widgets/app_loading_indicator.dart index c88440c..60c6169 100644 --- a/apps/lib/shared/widgets/app_loading_indicator.dart +++ b/apps/lib/shared/widgets/app_loading_indicator.dart @@ -40,49 +40,46 @@ class AppLoadingIndicator extends StatelessWidget { }; } - Color get _resolvedColor { - return color ?? - switch (variant) { - AppLoadingVariant.surface => AppColors.blue500, - AppLoadingVariant.inline => AppColors.slate500, - AppLoadingVariant.button => AppColors.white, - }; - } - - Color get _resolvedTrackColor { - return trackColor ?? - switch (variant) { - AppLoadingVariant.surface => AppColors.blue100, - AppLoadingVariant.inline => AppColors.slate200, - AppLoadingVariant.button => AppColors.blue300, - }; - } - - bool get _resolvedWithContainer { - return withContainer ?? - switch (variant) { - AppLoadingVariant.surface => true, - AppLoadingVariant.inline => false, - AppLoadingVariant.button => false, - }; - } - - Widget _buildSpinner() { + Widget _buildSpinner(Color color, Color trackColor) { return SizedBox( width: _resolvedSize, height: _resolvedSize, child: CircularProgressIndicator( strokeWidth: _resolvedStrokeWidth, - color: _resolvedColor, - backgroundColor: _resolvedTrackColor, + color: color, + backgroundColor: trackColor, ), ); } @override Widget build(BuildContext context) { - if (!_resolvedWithContainer) { - return _buildSpinner(); + final colorScheme = Theme.of(context).colorScheme; + + final resolvedColor = + color ?? + switch (variant) { + AppLoadingVariant.surface => colorScheme.primary, + AppLoadingVariant.inline => colorScheme.onSurfaceVariant, + AppLoadingVariant.button => colorScheme.onPrimary, + }; + + final resolvedTrackColor = + trackColor ?? + switch (variant) { + AppLoadingVariant.surface => colorScheme.primaryContainer, + AppLoadingVariant.inline => colorScheme.outlineVariant, + AppLoadingVariant.button => colorScheme.secondary, + }; + + if (withContainer == false || + (withContainer == null && + switch (variant) { + AppLoadingVariant.surface => true, + AppLoadingVariant.inline => false, + AppLoadingVariant.button => false, + })) { + return _buildSpinner(resolvedColor, resolvedTrackColor); } return Container( @@ -90,18 +87,18 @@ class AppLoadingIndicator extends StatelessWidget { height: _resolvedSize + AppSpacing.md, padding: const EdgeInsets.all(AppSpacing.xs), decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.full), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: colorScheme.outlineVariant), boxShadow: [ BoxShadow( - color: AppColors.slate200.withValues(alpha: 0.55), + color: colorScheme.outlineVariant.withValues(alpha: 0.55), blurRadius: AppRadius.md, offset: const Offset(0, AppSpacing.xs), ), ], ), - child: _buildSpinner(), + child: _buildSpinner(resolvedColor, resolvedTrackColor), ); } } diff --git a/apps/lib/shared/widgets/app_pressable.dart b/apps/lib/shared/widgets/app_pressable.dart index 2709178..db8dee1 100644 --- a/apps/lib/shared/widgets/app_pressable.dart +++ b/apps/lib/shared/widgets/app_pressable.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import '../../core/theme/design_tokens.dart'; - class AppPressable extends StatefulWidget { const AppPressable({ super.key, @@ -25,25 +23,22 @@ class _AppPressableState extends State { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return AnimatedScale( scale: _isPressed ? widget.pressedScale : 1, duration: const Duration(milliseconds: 110), curve: Curves.easeOut, child: Material( - color: Colors.transparent, + color: colorScheme.surface.withValues(alpha: 0), child: InkWell( borderRadius: widget.borderRadius, onTap: widget.onTap, onHighlightChanged: (pressed) { - if (_isPressed == pressed) { - return; - } - setState(() { - _isPressed = pressed; - }); + if (_isPressed == pressed) return; + setState(() => _isPressed = pressed); }, - splashColor: AppColors.blue100.withValues(alpha: 0.32), - highlightColor: AppColors.blue50.withValues(alpha: 0.28), + splashColor: colorScheme.primary.withValues(alpha: 0.12), + highlightColor: colorScheme.primary.withValues(alpha: 0.08), child: widget.child, ), ), diff --git a/apps/lib/shared/widgets/app_pull_refresh_feedback.dart b/apps/lib/shared/widgets/app_pull_refresh_feedback.dart index 46bd4b6..47b98fd 100644 --- a/apps/lib/shared/widgets/app_pull_refresh_feedback.dart +++ b/apps/lib/shared/widgets/app_pull_refresh_feedback.dart @@ -18,6 +18,7 @@ class AppPullRefreshFeedback extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; final resolvedLabel = label ?? context.l10n.commonRefreshing; return IgnorePointer( child: AnimatedOpacity( @@ -31,24 +32,24 @@ class AppPullRefreshFeedback extends StatelessWidget { vertical: AppSpacing.xs, ), decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.full), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: colorScheme.outlineVariant), ), child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - const AppLoadingIndicator( + AppLoadingIndicator( variant: AppLoadingVariant.inline, - color: AppColors.blue500, - trackColor: AppColors.blue100, + color: colorScheme.primary, + trackColor: colorScheme.primaryContainer, ), const SizedBox(width: AppSpacing.sm), Text( resolvedLabel, style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: AppColors.slate600, + color: colorScheme.onSurfaceVariant, fontWeight: FontWeight.w500, ), ), diff --git a/apps/lib/shared/widgets/app_selection_sheet.dart b/apps/lib/shared/widgets/app_selection_sheet.dart index 28583b0..0c33495 100644 --- a/apps/lib/shared/widgets/app_selection_sheet.dart +++ b/apps/lib/shared/widgets/app_selection_sheet.dart @@ -20,8 +20,9 @@ Future showAppSelectionSheet( final result = await showModalBottomSheet( context: context, isScrollControlled: true, - backgroundColor: Colors.transparent, + backgroundColor: Theme.of(context).colorScheme.surface.withValues(alpha: 0), builder: (sheetContext) { + final colorScheme = Theme.of(sheetContext).colorScheme; return SafeArea( top: false, child: Container( @@ -33,9 +34,9 @@ Future showAppSelectionSheet( ), padding: const EdgeInsets.symmetric(vertical: AppSpacing.lg), decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: colorScheme.outlineVariant), ), child: Column( mainAxisSize: MainAxisSize.min, @@ -46,10 +47,10 @@ Future showAppSelectionSheet( child: Text( title, textAlign: TextAlign.center, - style: const TextStyle( + style: TextStyle( fontSize: 17, fontWeight: FontWeight.w700, - color: AppColors.slate900, + color: colorScheme.onSurface, ), ), ), @@ -62,9 +63,9 @@ Future showAppSelectionSheet( isSelected: isSelected, ); }), - const SizedBox(height: AppSpacing.sm), - const Divider(height: 1, color: AppColors.border), - const SizedBox(height: AppSpacing.sm), + SizedBox(height: AppSpacing.sm), + Divider(height: 1, color: colorScheme.outlineVariant), + SizedBox(height: AppSpacing.sm), Padding( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.lg), child: SizedBox( @@ -90,6 +91,7 @@ Widget _buildItem( required AppSelectionItem item, required bool isSelected, }) { + final colorScheme = Theme.of(sheetContext).colorScheme; return InkWell( onTap: () => Navigator.of(sheetContext).pop(item.value), child: Container( @@ -105,12 +107,14 @@ Widget _buildItem( style: TextStyle( fontSize: 15, fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, - color: isSelected ? AppColors.blue600 : AppColors.slate800, + color: isSelected + ? colorScheme.primary + : colorScheme.onSurfaceVariant, ), ), ), if (isSelected) - const Icon(Icons.check, size: 20, color: AppColors.blue600), + Icon(Icons.check, size: 20, color: colorScheme.primary), ], ), ), diff --git a/apps/lib/shared/widgets/app_sheet_input_field.dart b/apps/lib/shared/widgets/app_sheet_input_field.dart index af8b114..b39eba2 100644 --- a/apps/lib/shared/widgets/app_sheet_input_field.dart +++ b/apps/lib/shared/widgets/app_sheet_input_field.dart @@ -50,16 +50,17 @@ class _AppSheetInputFieldState extends State { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.label, - style: const TextStyle( + style: TextStyle( fontFamily: 'Inter', fontSize: 14, fontWeight: FontWeight.w600, - color: AppColors.slate700, + color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: AppSpacing.sm), @@ -67,17 +68,17 @@ class _AppSheetInputFieldState extends State { duration: const Duration(milliseconds: 140), curve: Curves.easeOut, decoration: BoxDecoration( - color: AppColors.slate50, + color: colorScheme.surfaceContainerLowest, borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all( color: _isFocused - ? AppColors.borderQuaternary - : AppColors.borderSecondary, + ? colorScheme.primary + : colorScheme.outlineVariant, ), boxShadow: _isFocused ? [ BoxShadow( - color: AppColors.blue200.withValues(alpha: 0.35), + color: colorScheme.secondary.withValues(alpha: 0.35), blurRadius: AppRadius.sm, offset: const Offset(0, AppSpacing.xs / 2), ), @@ -91,7 +92,9 @@ class _AppSheetInputFieldState extends State { maxLines: widget.maxLines, decoration: InputDecoration( hintText: widget.hint, - hintStyle: const TextStyle(color: AppColors.slate400), + hintStyle: TextStyle( + color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6), + ), border: InputBorder.none, contentPadding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, diff --git a/apps/lib/shared/widgets/app_toggle_switch.dart b/apps/lib/shared/widgets/app_toggle_switch.dart index a2c023e..3166f06 100644 --- a/apps/lib/shared/widgets/app_toggle_switch.dart +++ b/apps/lib/shared/widgets/app_toggle_switch.dart @@ -22,6 +22,7 @@ class AppToggleSwitch extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return GestureDetector( onTap: onChanged == null ? null : () => onChanged!(!value), child: Opacity( @@ -32,13 +33,13 @@ class AppToggleSwitch extends StatelessWidget { padding: const EdgeInsets.all(AppSpacing.xs / 2), decoration: BoxDecoration( color: value - ? (activeBackgroundColor ?? AppColors.blue100) - : (inactiveBackgroundColor ?? AppColors.surfaceTertiary), + ? (activeBackgroundColor ?? colorScheme.primaryContainer) + : (inactiveBackgroundColor ?? colorScheme.tertiaryContainer), borderRadius: BorderRadius.circular(AppRadius.full), border: Border.all( color: value - ? (activeBorderColor ?? AppColors.blue300) - : (inactiveBorderColor ?? AppColors.borderSecondary), + ? (activeBorderColor ?? colorScheme.tertiary) + : (inactiveBorderColor ?? colorScheme.outlineVariant), ), ), child: AnimatedAlign( @@ -48,9 +49,9 @@ class AppToggleSwitch extends StatelessWidget { width: AppSpacing.lg + AppSpacing.xs, height: AppSpacing.lg + AppSpacing.xs, decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.full), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: colorScheme.outlineVariant), ), ), ), diff --git a/apps/lib/shared/widgets/back_title_page_header.dart b/apps/lib/shared/widgets/back_title_page_header.dart index b475d89..751a6df 100644 --- a/apps/lib/shared/widgets/back_title_page_header.dart +++ b/apps/lib/shared/widgets/back_title_page_header.dart @@ -21,6 +21,7 @@ class BackTitlePageHeader extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return SizedBox( height: height, child: Stack( @@ -50,10 +51,10 @@ class BackTitlePageHeader extends StatelessWidget { title, maxLines: 1, overflow: TextOverflow.ellipsis, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, - color: AppColors.slate900, + color: colorScheme.onSurface, ), ), ), diff --git a/apps/lib/features/calendar/presentation/widgets/bottom_dock.dart b/apps/lib/shared/widgets/bottom_dock.dart similarity index 68% rename from apps/lib/features/calendar/presentation/widgets/bottom_dock.dart rename to apps/lib/shared/widgets/bottom_dock.dart index d80d38d..dec3976 100644 --- a/apps/lib/features/calendar/presentation/widgets/bottom_dock.dart +++ b/apps/lib/shared/widgets/bottom_dock.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:lucide_icons/lucide_icons.dart'; -import '../../../../core/theme/design_tokens.dart'; + +import '../../core/theme/design_tokens.dart'; enum DockTab { todo, calendar } @@ -20,6 +21,7 @@ class BottomDock extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Container( height: 72, padding: const EdgeInsets.only( @@ -31,21 +33,24 @@ class BottomDock extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, - children: [_buildToggle(), _buildHomeBtn()], + children: [ + _buildToggle(context, colorScheme), + _buildHomeBtn(context, colorScheme), + ], ), ); } - Widget _buildToggle() { + Widget _buildToggle(BuildContext context, ColorScheme colorScheme) { return Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 4), decoration: BoxDecoration( - color: AppColors.todoToggleBg, + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.xxl), - border: Border.all(color: AppColors.todoToggleBorder), + border: Border.all(color: colorScheme.outlineVariant), boxShadow: [ BoxShadow( - color: AppColors.slate200.withValues(alpha: 0.45), + color: colorScheme.shadow.withValues(alpha: 0.12), blurRadius: AppRadius.sm, offset: const Offset(0, AppSpacing.xs / 2), ), @@ -59,12 +64,14 @@ class BottomDock extends StatelessWidget { icon: LucideIcons.listTodo, isActive: activeTab == DockTab.todo, onTap: onTodoTap, + colorScheme: colorScheme, ), const SizedBox(width: 4), _buildToggleItem( icon: LucideIcons.calendar, isActive: activeTab == DockTab.calendar, onTap: onCalendarTap, + colorScheme: colorScheme, ), ], ), @@ -74,10 +81,11 @@ class BottomDock extends StatelessWidget { Widget _buildToggleItem({ required IconData icon, required bool isActive, + required ColorScheme colorScheme, VoidCallback? onTap, }) { return Material( - color: Colors.transparent, + color: colorScheme.surface.withValues(alpha: 0), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(AppRadius.xl), @@ -87,27 +95,31 @@ class BottomDock extends StatelessWidget { width: 44, height: 44, decoration: BoxDecoration( - color: isActive ? AppColors.todoToggleActiveBg : Colors.transparent, + color: isActive + ? colorScheme.secondaryContainer + : colorScheme.surface.withValues(alpha: 0), borderRadius: BorderRadius.circular(AppRadius.xl), border: Border.all( color: isActive - ? AppColors.todoToggleActiveBorder - : Colors.transparent, + ? colorScheme.primary.withValues(alpha: 0.35) + : colorScheme.surface.withValues(alpha: 0), ), ), child: Icon( icon, size: 20, - color: isActive ? AppColors.blue600 : AppColors.slate700, + color: isActive + ? colorScheme.primary + : colorScheme.onSurfaceVariant, ), ), ), ); } - Widget _buildHomeBtn() { + Widget _buildHomeBtn(BuildContext context, ColorScheme colorScheme) { return Material( - color: Colors.transparent, + color: colorScheme.surface.withValues(alpha: 0), child: InkWell( key: const ValueKey('bottom_dock_home_button'), onTap: onHomeTap, @@ -116,21 +128,21 @@ class BottomDock extends StatelessWidget { width: 44, height: 44, decoration: BoxDecoration( - color: AppColors.todoToggleBg, + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.todoToggleBorder), + border: Border.all(color: colorScheme.outlineVariant), boxShadow: [ BoxShadow( - color: AppColors.slate200.withValues(alpha: 0.42), + color: colorScheme.shadow.withValues(alpha: 0.12), blurRadius: AppRadius.sm, offset: const Offset(0, AppSpacing.xs / 2), ), ], ), - child: const Icon( + child: Icon( LucideIcons.home, size: 20, - color: AppColors.slate700, + color: colorScheme.onSurfaceVariant, ), ), ), diff --git a/apps/lib/shared/widgets/chat_bubble.dart b/apps/lib/shared/widgets/chat_bubble.dart index 5390fbe..1661dab 100644 --- a/apps/lib/shared/widgets/chat_bubble.dart +++ b/apps/lib/shared/widgets/chat_bubble.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import '../../../core/l10n/l10n.dart'; -import '../../../core/theme/design_tokens.dart'; enum MessageSender { user, ai } @@ -23,6 +22,7 @@ class ChatBubble extends StatelessWidget { @override Widget build(BuildContext context) { final isUser = sender == MessageSender.user; + final colorScheme = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), @@ -36,7 +36,10 @@ class ChatBubble extends StatelessWidget { padding: const EdgeInsets.only(bottom: 6), child: Text( _formatTimestamp(timestamp), - style: const TextStyle(fontSize: 11, color: AppColors.slate400), + style: TextStyle( + fontSize: 11, + color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7), + ), ), ), Container( @@ -45,11 +48,11 @@ class ChatBubble extends StatelessWidget { ), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( - color: isUser ? AppColors.blue500 : AppColors.white, + color: isUser ? colorScheme.primary : colorScheme.surface, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.06), + color: colorScheme.shadow.withValues(alpha: 0.08), blurRadius: 8, offset: const Offset(0, 2), ), @@ -60,7 +63,9 @@ class ChatBubble extends StatelessWidget { content, style: TextStyle( fontSize: 15, - color: isUser ? AppColors.white : AppColors.slate700, + color: isUser + ? colorScheme.onPrimary + : colorScheme.onSurface, height: 1.45, ), ) diff --git a/apps/lib/shared/widgets/confirm_sheet.dart b/apps/lib/shared/widgets/confirm_sheet.dart index 89bbf9f..48f7f11 100644 --- a/apps/lib/shared/widgets/confirm_sheet.dart +++ b/apps/lib/shared/widgets/confirm_sheet.dart @@ -18,8 +18,10 @@ Future showConfirmSheet( final result = await showModalBottomSheet( context: context, isScrollControlled: true, - backgroundColor: Colors.transparent, + backgroundColor: Theme.of(context).colorScheme.surface.withValues(alpha: 0), builder: (sheetContext) { + final colorScheme = Theme.of(sheetContext).colorScheme; + return SafeArea( top: false, child: Container( @@ -31,9 +33,9 @@ Future showConfirmSheet( ), padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: colorScheme.outlineVariant), ), child: Column( mainAxisSize: MainAxisSize.min, @@ -42,17 +44,20 @@ Future showConfirmSheet( Text( title, textAlign: TextAlign.center, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, - color: AppColors.slate900, + color: colorScheme.onSurface, ), ), const SizedBox(height: AppSpacing.xs), Text( message, textAlign: TextAlign.center, - style: const TextStyle(fontSize: 14, color: AppColors.slate500), + style: TextStyle( + fontSize: 14, + color: colorScheme.onSurfaceVariant, + ), ), const SizedBox(height: AppSpacing.lg), SizedBox( @@ -63,16 +68,18 @@ Future showConfirmSheet( alignment: Alignment.center, decoration: BoxDecoration( color: isDestructive - ? AppColors.feedbackErrorIcon - : AppColors.blue600, + ? colorScheme.error + : colorScheme.primary, borderRadius: BorderRadius.circular(AppRadius.full), ), child: Text( resolvedConfirmText, - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, - color: AppColors.white, + color: isDestructive + ? colorScheme.onError + : colorScheme.onPrimary, ), ), ), diff --git a/apps/lib/shared/widgets/destructive_action_sheet.dart b/apps/lib/shared/widgets/destructive_action_sheet.dart index a45da61..ee5b8b1 100644 --- a/apps/lib/shared/widgets/destructive_action_sheet.dart +++ b/apps/lib/shared/widgets/destructive_action_sheet.dart @@ -13,8 +13,10 @@ Future showDestructiveActionSheet( final result = await showModalBottomSheet( context: context, isScrollControlled: true, - backgroundColor: Colors.transparent, + backgroundColor: Theme.of(context).colorScheme.surface.withValues(alpha: 0), builder: (sheetContext) { + final colorScheme = Theme.of(sheetContext).colorScheme; + return SafeArea( top: false, child: Container( @@ -26,9 +28,9 @@ Future showDestructiveActionSheet( ), padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.xl), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: colorScheme.outlineVariant), ), child: Column( mainAxisSize: MainAxisSize.min, @@ -37,17 +39,20 @@ Future showDestructiveActionSheet( Text( title, textAlign: TextAlign.center, - style: const TextStyle( + style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, - color: AppColors.slate900, + color: colorScheme.onSurface, ), ), const SizedBox(height: AppSpacing.xs), Text( message, textAlign: TextAlign.center, - style: const TextStyle(fontSize: 14, color: AppColors.slate500), + style: TextStyle( + fontSize: 14, + color: colorScheme.onSurfaceVariant, + ), ), const SizedBox(height: AppSpacing.lg), SizedBox( @@ -57,15 +62,15 @@ Future showDestructiveActionSheet( child: Container( alignment: Alignment.center, decoration: BoxDecoration( - color: AppColors.feedbackErrorIcon, + color: colorScheme.error, borderRadius: BorderRadius.circular(AppRadius.full), ), child: Text( confirmText, - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, - color: AppColors.white, + color: colorScheme.onError, ), ), ), diff --git a/apps/lib/shared/widgets/detail_header_action_menu.dart b/apps/lib/shared/widgets/detail_header_action_menu.dart index d37ddb6..1525e2a 100644 --- a/apps/lib/shared/widgets/detail_header_action_menu.dart +++ b/apps/lib/shared/widgets/detail_header_action_menu.dart @@ -72,7 +72,7 @@ class _DetailHeaderActionMenuState extends State> { CompositedTransformFollower( link: _layerLink, showWhenUnlinked: false, - offset: const Offset( + offset: Offset( _buttonSize - _menuWidth, _buttonSize + AppSpacing.sm, ), @@ -101,18 +101,19 @@ class _DetailHeaderActionMenuState extends State> { } Widget _buildMenuCard() { + final colorScheme = Theme.of(context).colorScheme; return Material( - color: Colors.transparent, + color: colorScheme.surface.withValues(alpha: 0), child: Container( width: _menuWidth, padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm), decoration: BoxDecoration( - color: AppColors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.lg), - border: Border.all(color: AppColors.borderSecondary), + border: Border.all(color: colorScheme.outlineVariant), boxShadow: [ BoxShadow( - color: AppColors.slate300.withValues(alpha: 0.42), + color: colorScheme.shadow.withValues(alpha: 0.42), blurRadius: AppSpacing.xl, offset: const Offset(0, AppSpacing.sm), ), @@ -125,12 +126,14 @@ class _DetailHeaderActionMenuState extends State> { for (int i = 0; i < widget.items.length; i++) ...[ _buildMenuItem(widget.items[i]), if (i < widget.items.length - 1) - const Padding( - padding: EdgeInsets.symmetric(horizontal: AppSpacing.md), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.md, + ), child: Divider( height: 1, thickness: 1, - color: AppColors.slate100, + color: colorScheme.outlineVariant, ), ), ], @@ -141,21 +144,26 @@ class _DetailHeaderActionMenuState extends State> { } Widget _buildMenuItem(DetailHeaderActionItem item) { + final colorScheme = Theme.of(context).colorScheme; final textColor = item.isDestructive - ? AppColors.red500 - : (item.enabled ? AppColors.slate700 : AppColors.slate400); + ? colorScheme.error + : (item.enabled ? colorScheme.onSurface : colorScheme.onSurfaceVariant); final pressedColor = item.isDestructive - ? AppColors.feedbackErrorSurface - : AppColors.surfaceInfoLight; + ? colorScheme.errorContainer + : colorScheme.primaryContainer; return SizedBox( height: AppSpacing.xxl * 2, child: Material( - color: Colors.transparent, + color: colorScheme.surface.withValues(alpha: 0), child: InkWell( borderRadius: BorderRadius.circular(AppRadius.md), - splashColor: item.enabled ? pressedColor : Colors.transparent, - highlightColor: item.enabled ? pressedColor : Colors.transparent, + splashColor: item.enabled + ? pressedColor + : colorScheme.surface.withValues(alpha: 0), + highlightColor: item.enabled + ? pressedColor + : colorScheme.surface.withValues(alpha: 0), onTap: item.enabled ? () => _handleSelect(item.value) : null, child: Padding( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), @@ -190,6 +198,7 @@ class _DetailHeaderActionMenuState extends State> { return const SizedBox.shrink(); } + final colorScheme = Theme.of(context).colorScheme; return CompositedTransformTarget( link: _layerLink, child: GestureDetector( @@ -200,19 +209,19 @@ class _DetailHeaderActionMenuState extends State> { height: _buttonSize, decoration: BoxDecoration( color: _isMenuOpen - ? AppColors.surfaceInfo - : AppColors.surfaceTertiary, + ? colorScheme.secondaryContainer + : colorScheme.tertiaryContainer, borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all( color: _isMenuOpen - ? AppColors.borderQuaternary - : AppColors.borderTertiary, + ? colorScheme.outlineVariant + : colorScheme.outline, ), ), - child: const Icon( + child: Icon( Icons.more_horiz, size: AppSpacing.lg + AppSpacing.xs, - color: AppColors.slate600, + color: colorScheme.onSurfaceVariant, ), ), ), diff --git a/apps/lib/shared/widgets/error_retry_surface.dart b/apps/lib/shared/widgets/error_retry_surface.dart index c0dde6e..4e7f904 100644 --- a/apps/lib/shared/widgets/error_retry_surface.dart +++ b/apps/lib/shared/widgets/error_retry_surface.dart @@ -18,6 +18,8 @@ class ErrorRetrySurface extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Center( child: Padding( padding: EdgeInsets.symmetric(horizontal: horizontalPadding), @@ -28,7 +30,7 @@ class ErrorRetrySurface extends StatelessWidget { Text( message, textAlign: TextAlign.center, - style: const TextStyle(color: AppColors.red500), + style: TextStyle(color: colorScheme.error), ), const SizedBox(height: AppSpacing.md), AppButton(text: context.l10n.commonRetry, onPressed: onRetry), diff --git a/apps/lib/shared/widgets/fixed_length_code_input.dart b/apps/lib/shared/widgets/fixed_length_code_input.dart index fd87a74..70781a5 100644 --- a/apps/lib/shared/widgets/fixed_length_code_input.dart +++ b/apps/lib/shared/widgets/fixed_length_code_input.dart @@ -99,6 +99,7 @@ class _FixedLengthCodeInputState extends State { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; final chars = widget.controller.text.split(''); final slotHeight = AppSpacing.xl * 2 + AppSpacing.sm; final slotSpacing = AppSpacing.sm; @@ -113,17 +114,17 @@ class _FixedLengthCodeInputState extends State { duration: const Duration(milliseconds: 180), padding: const EdgeInsets.all(AppSpacing.sm), decoration: BoxDecoration( - color: AppColors.authSectionBackground, + color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.xl), border: Border.all( color: _isFocused - ? AppColors.authInputFocus - : AppColors.authSectionBorder, + ? colorScheme.primary + : colorScheme.outlineVariant, ), boxShadow: _isFocused ? [ BoxShadow( - color: AppColors.blue200.withValues(alpha: 0.28), + color: colorScheme.secondary.withValues(alpha: 0.28), blurRadius: 18, offset: const Offset(0, 8), ), @@ -164,6 +165,7 @@ class _FixedLengthCodeInputState extends State { chars: chars, slotHeight: slotHeight, isComplete: isComplete, + colorScheme: colorScheme, ), ), if (index != widget.length - 1) @@ -185,6 +187,7 @@ class _FixedLengthCodeInputState extends State { required List chars, required double slotHeight, required bool isComplete, + required ColorScheme colorScheme, }) { final hasChar = index < chars.length; final isActive = @@ -195,19 +198,19 @@ class _FixedLengthCodeInputState extends State { height: slotHeight, alignment: Alignment.center, decoration: BoxDecoration( - color: hasChar ? AppColors.white : AppColors.authInputBackground, + color: hasChar ? colorScheme.surface : colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all( color: isActive - ? AppColors.authPrimaryButton + ? colorScheme.primary : isComplete - ? AppColors.authSecondaryButtonBorder - : AppColors.authInputBorder, + ? colorScheme.outlineVariant + : colorScheme.outline, ), boxShadow: isActive ? [ BoxShadow( - color: AppColors.blue200.withValues(alpha: 0.32), + color: colorScheme.secondary.withValues(alpha: 0.32), blurRadius: 14, offset: const Offset(0, 6), ), @@ -219,7 +222,7 @@ class _FixedLengthCodeInputState extends State { style: TextStyle( fontSize: AppSpacing.xl, fontWeight: FontWeight.w600, - color: hasChar ? AppColors.slate900 : AppColors.authLinkMuted, + color: hasChar ? colorScheme.onSurface : colorScheme.onSurfaceVariant, ), ), ); diff --git a/apps/lib/shared/widgets/link_button.dart b/apps/lib/shared/widgets/link_button.dart index ce8a43c..44ee064 100644 --- a/apps/lib/shared/widgets/link_button.dart +++ b/apps/lib/shared/widgets/link_button.dart @@ -20,9 +20,10 @@ class LinkButton extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; final color = enabled - ? (foregroundColor ?? AppColors.authLinkText) - : AppColors.slate300; + ? (foregroundColor ?? colorScheme.primary) + : colorScheme.outline; return TextButton( onPressed: enabled ? onTap : null, diff --git a/apps/lib/shared/widgets/message_composer.dart b/apps/lib/shared/widgets/message_composer.dart index 5610030..9b107b8 100644 --- a/apps/lib/shared/widgets/message_composer.dart +++ b/apps/lib/shared/widgets/message_composer.dart @@ -73,6 +73,7 @@ class MessageComposer extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return KeyedSubtree( key: messageComposerContainerKey, child: Container( @@ -82,19 +83,19 @@ class MessageComposer extends StatelessWidget { vertical: AppSpacing.sm, ), decoration: BoxDecoration( - color: AppColors.homeComposerShell, + color: colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.xxl), - border: Border.all(color: AppColors.homeComposerBorder), - boxShadow: const [ + border: Border.all(color: colorScheme.outlineVariant), + boxShadow: [ BoxShadow( - color: AppColors.slate200, + color: colorScheme.surfaceContainerHighest.withValues(alpha: 1), blurRadius: AppRadius.lg, - offset: Offset(AppSpacing.none, AppSpacing.sm), + offset: const Offset(AppSpacing.none, AppSpacing.sm), ), BoxShadow( - color: AppColors.white, + color: colorScheme.surface, blurRadius: AppRadius.md, - offset: Offset(AppSpacing.none, -AppSpacing.xs), + offset: const Offset(AppSpacing.none, -AppSpacing.xs), ), ], ), @@ -114,30 +115,30 @@ class MessageComposer extends StatelessWidget { icon: Icon( LucideIcons.plus, size: iconSize, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), ), ), ), const SizedBox(width: AppSpacing.sm), - Expanded(child: _buildCenterArea()), + Expanded(child: _buildCenterArea(colorScheme)), const SizedBox(width: AppSpacing.sm), IconButton( key: messageComposerRightButtonKey, visualDensity: VisualDensity.compact, onPressed: onTapRightAction, icon: _isTranscribing - ? const AppLoadingIndicator( + ? AppLoadingIndicator( variant: AppLoadingVariant.inline, size: AppSpacing.lg, strokeWidth: AppSpacing.xs / 2, - color: AppColors.blue600, - trackColor: AppColors.blue100, + color: colorScheme.primary, + trackColor: colorScheme.primaryContainer, ) : Icon( _resolveRightIcon(), size: iconSize, - color: _resolveRightIconColor(), + color: _resolveRightIconColor(colorScheme), ), ), ], @@ -147,7 +148,7 @@ class MessageComposer extends StatelessWidget { ); } - Widget _buildCenterArea() { + Widget _buildCenterArea(ColorScheme colorScheme) { return SizedBox( height: composerMinHeight, child: AnimatedSwitcher( @@ -155,7 +156,10 @@ class MessageComposer extends StatelessWidget { switchInCurve: Curves.easeOut, switchOutCurve: Curves.easeOut, child: _isHoldMode - ? _buildHoldToSpeakArea(key: const ValueKey('hold_mode')) + ? _buildHoldToSpeakArea( + key: const ValueKey('hold_mode'), + colorScheme: colorScheme, + ) : _buildTextInputArea(key: const ValueKey('text_mode')), ), ); @@ -165,7 +169,10 @@ class MessageComposer extends StatelessWidget { return SizedBox(key: key, height: composerMinHeight, child: textInputChild); } - Widget _buildHoldToSpeakArea({required Key key}) { + Widget _buildHoldToSpeakArea({ + required Key key, + required ColorScheme colorScheme, + }) { return RawGestureDetector( key: messageComposerHoldAreaKey, behavior: HitTestBehavior.opaque, @@ -176,8 +183,8 @@ class MessageComposer extends StatelessWidget { duration: const Duration(milliseconds: _holdActivateDurationMs), ), (instance) { - instance.onLongPressStart = (_) => onHoldToSpeakStart(); - instance.onLongPressEnd = (_) => onHoldToSpeakEnd(); + instance.onLongPressStart = (details) => onHoldToSpeakStart(); + instance.onLongPressEnd = (details) => onHoldToSpeakEnd(); instance.onLongPressMoveUpdate = onHoldToSpeakMoveUpdate; instance.onLongPressCancel = onHoldToSpeakCancel; }, @@ -188,12 +195,12 @@ class MessageComposer extends StatelessWidget { width: double.infinity, height: composerMinHeight, alignment: Alignment.center, - child: _buildHoldToSpeakContent(), + child: _buildHoldToSpeakContent(colorScheme), ), ); } - Widget _buildHoldToSpeakContent() { + Widget _buildHoldToSpeakContent(ColorScheme colorScheme) { final l10n = L10n.current; final resolvedRecordingText = recordingText ?? l10n.homeRecordingReleaseSend; @@ -208,7 +215,7 @@ class MessageComposer extends StatelessWidget { alignment: Alignment.center, child: Text( resolvedRecordingText, - style: const TextStyle(color: AppColors.slate700), + style: TextStyle(color: colorScheme.onSurface), ), ); } @@ -220,13 +227,13 @@ class MessageComposer extends StatelessWidget { const SizedBox(height: AppSpacing.xs), Text( resolvedRecordingText, - style: const TextStyle(color: AppColors.slate700), + style: TextStyle(color: colorScheme.onSurface), ), const SizedBox(height: AppSpacing.xs), Text( resolvedRecordingHintText, key: messageComposerRecordingHintKey, - style: const TextStyle(color: AppColors.slate500), + style: TextStyle(color: colorScheme.onSurfaceVariant), ), ], ); @@ -237,7 +244,7 @@ class MessageComposer extends StatelessWidget { alignment: Alignment.center, child: Text( resolvedTranscribingText, - style: const TextStyle(color: AppColors.slate500), + style: TextStyle(color: colorScheme.onSurfaceVariant), ), ); } @@ -246,7 +253,7 @@ class MessageComposer extends StatelessWidget { alignment: Alignment.center, child: Text( resolvedHoldToSpeakText, - style: const TextStyle(color: AppColors.slate500), + style: TextStyle(color: colorScheme.onSurfaceVariant), ), ); } @@ -261,10 +268,10 @@ class MessageComposer extends StatelessWidget { return _isHoldMode ? LucideIcons.keyboard : LucideIcons.mic; } - Color _resolveRightIconColor() { + Color _resolveRightIconColor(ColorScheme colorScheme) { if (isWaitingAgent || hasMessage) { - return AppColors.blue600; + return colorScheme.primary; } - return AppColors.slate500; + return colorScheme.onSurfaceVariant; } } diff --git a/apps/lib/shared/widgets/page_header.dart b/apps/lib/shared/widgets/page_header.dart index b2c7183..55cf07f 100644 --- a/apps/lib/shared/widgets/page_header.dart +++ b/apps/lib/shared/widgets/page_header.dart @@ -42,10 +42,13 @@ class _BackButtonState extends State { @override Widget build(BuildContext context) { - final background = _isPressed ? AppColors.surfaceInfo : AppColors.white; + final colorScheme = Theme.of(context).colorScheme; + final background = _isPressed + ? colorScheme.secondaryContainer + : colorScheme.surface; final borderColor = _isPressed - ? AppColors.borderQuaternary - : AppColors.borderTertiary; + ? colorScheme.outlineVariant + : colorScheme.outline; return AnimatedScale( scale: _isPressed ? 0.96 : 1, @@ -60,13 +63,13 @@ class _BackButtonState extends State { boxShadow: _isPressed ? const [] : [ - const BoxShadow( - color: AppColors.white, + BoxShadow( + color: colorScheme.surface, blurRadius: AppRadius.sm, - offset: Offset(0, -1), + offset: const Offset(0, -1), ), BoxShadow( - color: AppColors.slate200.withValues(alpha: 0.42), + color: colorScheme.shadow.withValues(alpha: 0.42), blurRadius: AppRadius.md, offset: const Offset(0, AppSpacing.xs), ), @@ -76,39 +79,29 @@ class _BackButtonState extends State { width: AppSpacing.xl * 2, height: AppSpacing.xl * 2, child: Material( - color: Colors.transparent, + color: colorScheme.surface.withValues(alpha: 0), child: InkWell( borderRadius: BorderRadius.circular(AppRadius.full), onTap: widget.onPressed ?? () => Navigator.of(context).pop(), onTapDown: (_) { - if (_isPressed) { - return; - } - setState(() { - _isPressed = true; - }); + if (_isPressed) return; + setState(() => _isPressed = true); }, onTapCancel: () { - if (!_isPressed) { - return; - } - setState(() { - _isPressed = false; - }); + if (!_isPressed) return; + setState(() => _isPressed = false); }, onTapUp: (_) { - if (!_isPressed) { - return; - } - setState(() { - _isPressed = false; - }); + if (!_isPressed) return; + setState(() => _isPressed = false); }, child: Center( child: Icon( Icons.chevron_left, size: AppSpacing.lg + AppSpacing.xs, - color: _isPressed ? AppColors.blue600 : AppColors.slate700, + color: _isPressed + ? colorScheme.primary + : colorScheme.onSurfaceVariant, ), ), ), diff --git a/apps/lib/shared/widgets/phone_prefix_selector.dart b/apps/lib/shared/widgets/phone_prefix_selector.dart index 0ac85a3..2c5503c 100644 --- a/apps/lib/shared/widgets/phone_prefix_selector.dart +++ b/apps/lib/shared/widgets/phone_prefix_selector.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import '../../core/theme/design_tokens.dart'; class PhonePrefixSelector extends StatelessWidget { const PhonePrefixSelector({ @@ -15,8 +14,9 @@ class PhonePrefixSelector extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Padding( - padding: const EdgeInsets.only(left: AppSpacing.md, right: AppSpacing.sm), + padding: const EdgeInsets.only(left: 12, right: 8), child: PopupMenuButton( onSelected: onChanged, itemBuilder: (context) => items @@ -24,26 +24,24 @@ class PhonePrefixSelector extends StatelessWidget { (item) => PopupMenuItem(value: item, child: Text(item)), ) .toList(growable: false), - color: AppColors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.md), - ), + color: colorScheme.surface, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( value, - style: const TextStyle( + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, - color: AppColors.slate700, + color: colorScheme.onSurface, ), ), - const SizedBox(width: AppSpacing.xs), - const Icon( + const SizedBox(width: 4), + Icon( Icons.arrow_drop_down, size: 18, - color: AppColors.slate500, + color: colorScheme.onSurfaceVariant, ), ], ), diff --git a/apps/lib/shared/widgets/toast/toast.dart b/apps/lib/shared/widgets/toast/toast.dart index 0e7f1b9..3dbb73a 100644 --- a/apps/lib/shared/widgets/toast/toast.dart +++ b/apps/lib/shared/widgets/toast/toast.dart @@ -92,6 +92,7 @@ class _ToastWidgetState extends State<_ToastWidget> @override Widget build(BuildContext context) { final config = ToastTypeConfig.fromType(context, widget.type); + final colorScheme = Theme.of(context).colorScheme; return Positioned( top: MediaQuery.of(context).padding.top + 12, @@ -115,7 +116,7 @@ class _ToastWidgetState extends State<_ToastWidget> border: Border.all(color: config.borderColor), boxShadow: [ BoxShadow( - color: AppColors.slate900.withValues(alpha: 0.08), + color: colorScheme.shadow.withValues(alpha: 0.08), blurRadius: 24, offset: const Offset(0, 10), ), diff --git a/apps/lib/shared/widgets/toast/toast_type_config.dart b/apps/lib/shared/widgets/toast/toast_type_config.dart index 0e8c974..e74f760 100644 --- a/apps/lib/shared/widgets/toast/toast_type_config.dart +++ b/apps/lib/shared/widgets/toast/toast_type_config.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import '../../../core/l10n/l10n.dart'; import 'toast_type.dart'; -import '../../../core/theme/design_tokens.dart'; class ToastTypeConfig { final Color surfaceColor; @@ -22,36 +21,38 @@ class ToastTypeConfig { static ToastTypeConfig fromType(BuildContext context, ToastType type) { final l10n = context.l10n; + final colorScheme = Theme.of(context).colorScheme; + return switch (type) { ToastType.success => ToastTypeConfig( - surfaceColor: AppColors.feedbackSuccessSurface, - borderColor: AppColors.feedbackSuccessBorder, - iconColor: AppColors.feedbackSuccessIcon, - textColor: AppColors.feedbackSuccessText, + surfaceColor: colorScheme.tertiaryContainer, + borderColor: colorScheme.tertiary, + iconColor: colorScheme.tertiary, + textColor: colorScheme.onTertiaryContainer, label: l10n.toastLabelSuccess, icon: Icons.check_circle_outline, ), ToastType.warning => ToastTypeConfig( - surfaceColor: AppColors.feedbackWarningSurface, - borderColor: AppColors.feedbackWarningBorder, - iconColor: AppColors.feedbackWarningIcon, - textColor: AppColors.feedbackWarningText, + surfaceColor: colorScheme.secondaryContainer, + borderColor: colorScheme.secondary, + iconColor: colorScheme.secondary, + textColor: colorScheme.onSecondaryContainer, label: l10n.toastLabelWarning, icon: Icons.warning_amber_rounded, ), ToastType.error => ToastTypeConfig( - surfaceColor: AppColors.feedbackErrorSurface, - borderColor: AppColors.feedbackErrorBorder, - iconColor: AppColors.feedbackErrorIcon, - textColor: AppColors.feedbackErrorText, + surfaceColor: colorScheme.errorContainer, + borderColor: colorScheme.error, + iconColor: colorScheme.error, + textColor: colorScheme.onErrorContainer, label: l10n.toastLabelError, icon: Icons.error_outline, ), ToastType.info => ToastTypeConfig( - surfaceColor: AppColors.feedbackInfoSurface, - borderColor: AppColors.feedbackInfoBorder, - iconColor: AppColors.feedbackInfoIcon, - textColor: AppColors.feedbackInfoText, + surfaceColor: colorScheme.primaryContainer, + borderColor: colorScheme.primary, + iconColor: colorScheme.primary, + textColor: colorScheme.onPrimaryContainer, label: l10n.toastLabelInfo, icon: Icons.info_outline, ), diff --git a/apps/test/app/router/app_router_redirect_test.dart b/apps/test/app/router/app_router_redirect_test.dart new file mode 100644 index 0000000..b7e7932 --- /dev/null +++ b/apps/test/app/router/app_router_redirect_test.dart @@ -0,0 +1,105 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:social_app/app/di/injection.dart'; +import 'package:social_app/app/router/app_router.dart'; +import 'package:social_app/app/router/app_routes.dart'; +import 'package:social_app/core/network/i_api_client.dart'; +import 'package:social_app/features/chat/presentation/bloc/chat_bloc.dart'; +import 'package:social_app/features/auth/presentation/bloc/auth_state.dart'; + +void main() { + setUp(() async { + if (sl.isRegistered()) { + await sl.unregister(); + } + sl.registerSingleton(ChatBloc(apiClient: _FakeApiClient())); + }); + + tearDown(() async { + if (sl.isRegistered()) { + await sl.unregister(); + } + }); + + group('resolveAuthRedirect', () { + test('redirects unauthenticated home access to login', () { + final result = resolveAuthRedirect( + authState: const AuthUnauthenticated(), + matchedLocation: AppRoutes.homeMain, + ); + + expect(result, AppRoutes.authLogin); + }); + + test('redirects authenticated login access to home', () { + final result = resolveAuthRedirect( + authState: const AuthAuthenticated( + user: AuthUser(id: 'u1', phone: '13800138000'), + ), + matchedLocation: AppRoutes.authLogin, + ); + + expect(result, AppRoutes.homeMain); + }); + + test('redirects auth checking state to boot route', () { + final result = resolveAuthRedirect( + authState: AuthLoading(), + matchedLocation: AppRoutes.homeMain, + ); + + expect(result, AppRoutes.authBoot); + }); + + test('redirects boot route to login for unauthenticated state', () { + final result = resolveAuthRedirect( + authState: const AuthUnauthenticated(), + matchedLocation: AppRoutes.authBoot, + ); + + expect(result, AppRoutes.authLogin); + }); + }); + + test('home route screen is wrapped with ChatBloc provider', () { + final widget = buildHomeRouteScreen(); + + expect(widget, isA>()); + }); +} + +class _FakeApiClient implements IApiClient { + @override + Future> delete(String path, {data, Options? options}) { + throw UnimplementedError(); + } + + @override + Future> get(String path, {Options? options}) { + throw UnimplementedError(); + } + + @override + Future> getSseLines( + String path, { + Map? headers, + }) { + throw UnimplementedError(); + } + + @override + Future> patch(String path, {data, Options? options}) { + throw UnimplementedError(); + } + + @override + Future> post(String path, {data, Options? options}) { + throw UnimplementedError(); + } + + @override + Future> put(String path, {data, Options? options}) { + throw UnimplementedError(); + } +} diff --git a/apps/test/data/cache/cached_repository_test.dart b/apps/test/data/cache/cached_repository_test.dart new file mode 100644 index 0000000..a387c43 --- /dev/null +++ b/apps/test/data/cache/cached_repository_test.dart @@ -0,0 +1,65 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:social_app/data/cache/cache_policy.dart'; +import 'package:social_app/data/cache/cached_repository.dart'; +import 'package:social_app/data/cache/hybrid_cache_store.dart'; +import 'package:social_app/data/cache/memory_cache_store.dart'; +import 'package:social_app/data/cache/persistent_cache_store.dart'; + +class _IntCachedRepository extends CachedRepository { + int loadCount = 0; + + _IntCachedRepository({required super.store}) + : super( + policy: const CachePolicy( + softTtl: Duration(hours: 1), + hardTtl: Duration(hours: 2), + minRefreshInterval: Duration(minutes: 10), + ), + ); + + Future fetch({bool forceRefresh = false}) { + return getOrLoad( + key: 'test:number', + forceRefresh: forceRefresh, + loadFromRemote: () async { + loadCount += 1; + return loadCount; + }, + ); + } +} + +void main() { + group('CachedRepository', () { + test('reads from cache after first load', () async { + final repo = _IntCachedRepository( + store: HybridCacheStore( + memory: MemoryCacheStore(), + persistent: PersistentCacheStore(), + ), + ); + + final first = await repo.fetch(); + final second = await repo.fetch(); + + expect(first, 1); + expect(second, 1); + expect(repo.loadCount, 1); + }); + + test('force refresh bypasses cached value', () async { + final repo = _IntCachedRepository( + store: HybridCacheStore( + memory: MemoryCacheStore(), + persistent: PersistentCacheStore(), + ), + ); + + await repo.fetch(); + final refreshed = await repo.fetch(forceRefresh: true); + + expect(refreshed, 2); + expect(repo.loadCount, 2); + }); + }); +} diff --git a/apps/test/data/repositories/shared_repositories_test.dart b/apps/test/data/repositories/shared_repositories_test.dart new file mode 100644 index 0000000..ac24bb7 --- /dev/null +++ b/apps/test/data/repositories/shared_repositories_test.dart @@ -0,0 +1,182 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:social_app/core/network/i_api_client.dart'; +import 'package:social_app/data/repositories/calendar_event_repository.dart'; +import 'package:social_app/data/repositories/friend_repository.dart'; +import 'package:social_app/data/repositories/inbox_repository.dart'; +import 'package:social_app/data/repositories/models/calendar_event.dart'; +import 'package:social_app/data/repositories/models/friend_request.dart'; +import 'package:social_app/data/repositories/models/inbox_message.dart'; +import 'package:social_app/data/repositories/user_repository.dart'; + +class _FakeApiClient implements IApiClient { + final Map _getResponses = {}; + final Map _postResponses = {}; + final Map _patchResponses = {}; + + void setGet(String path, dynamic data) => _getResponses[path] = data; + + void setPost(String path, dynamic data) => _postResponses[path] = data; + + void setPatch(String path, dynamic data) => _patchResponses[path] = data; + + @override + Future> get(String path, {Options? options}) async { + if (!_getResponses.containsKey(path)) { + throw StateError('missing GET mock for $path'); + } + return Response( + requestOptions: RequestOptions(path: path), + data: _getResponses[path] as T, + ); + } + + @override + Future> post(String path, {data, Options? options}) async { + if (!_postResponses.containsKey(path)) { + throw StateError('missing POST mock for $path'); + } + return Response( + requestOptions: RequestOptions(path: path), + data: _postResponses[path] as T, + ); + } + + @override + Future> patch(String path, {data, Options? options}) async { + if (!_patchResponses.containsKey(path)) { + throw StateError('missing PATCH mock for $path'); + } + return Response( + requestOptions: RequestOptions(path: path), + data: _patchResponses[path] as T, + ); + } + + @override + Future> delete(String path, {data, Options? options}) { + throw UnimplementedError(); + } + + @override + Future> getSseLines( + String path, { + Map? headers, + }) { + throw UnimplementedError(); + } + + @override + Future> put(String path, {data, Options? options}) { + throw UnimplementedError(); + } +} + +void main() { + test('InboxRepository maps message type and status', () async { + final client = _FakeApiClient(); + client.setGet('/api/v1/inbox/messages?is_read=false', [ + { + 'id': 'm1', + 'recipient_id': 'u1', + 'sender_id': 'u2', + 'message_type': 'calendar', + 'schedule_item_id': 's1', + 'friendship_id': null, + 'content': {'type': 'invite'}, + 'is_read': false, + 'status': 'pending', + 'created_at': '2026-03-27T08:00:00Z', + }, + ]); + + final repository = InboxRepositoryImpl(client); + final result = await repository.getMessages(isRead: false); + + expect(result.single.messageType, InboxMessageType.calendar); + expect(result.single.status, InboxMessageStatus.pending); + expect(result.single.scheduleItemId, 's1'); + }); + + test('FriendRepository maps request status', () async { + final client = _FakeApiClient(); + client.setGet('/api/v1/friends/requests/f1', { + 'id': 'f1', + 'sender': {'id': 'u1', 'username': 'alice', 'avatar_url': null}, + 'recipient': {'id': 'u2', 'username': 'bob', 'avatar_url': null}, + 'content': 'hi', + 'status': 'accepted', + 'created_at': '2026-03-27T08:00:00Z', + }); + + final repository = FriendRepositoryImpl(client); + final request = await repository.getRequestById('f1'); + + expect(request.status, FriendRequestStatus.accepted); + expect(request.sender.username, 'alice'); + }); + + test( + 'FriendRepository batch lookup fails when any request is missing', + () async { + final client = _FakeApiClient(); + client.setGet('/api/v1/friends/requests/f1', { + 'id': 'f1', + 'sender': {'id': 'u1', 'username': 'alice', 'avatar_url': null}, + 'recipient': {'id': 'u2', 'username': 'bob', 'avatar_url': null}, + 'content': 'hi', + 'status': 'pending', + 'created_at': '2026-03-27T08:00:00Z', + }); + + final repository = FriendRepositoryImpl(client); + await expectLater( + repository.getRequestsByIds(['f1', 'missing']), + throwsStateError, + ); + }, + ); + + test('CalendarEventRepository maps archived status', () async { + final client = _FakeApiClient(); + client.setGet('/api/v1/schedule-items/e1', { + 'id': 'e1', + 'owner_id': 'u1', + 'permission': 1, + 'is_owner': true, + 'title': 'event', + 'description': null, + 'start_at': '2026-03-27T08:00:00Z', + 'end_at': null, + 'timezone': 'UTC', + 'metadata': null, + 'source_type': 'manual', + 'status': 'archived', + 'created_at': '2026-03-27T08:00:00Z', + 'updated_at': '2026-03-27T09:00:00Z', + }); + + final repository = CalendarEventRepositoryImpl(client); + final event = await repository.getById('e1'); + + expect(event.status, CalendarEventStatus.archived); + }); + + test('UserRepository returns shared user summary', () async { + final client = _FakeApiClient(); + client.setGet('/api/v1/users/me', { + 'id': 'u1', + 'username': 'alice', + 'phone': null, + 'avatar_url': 'https://img', + 'bio': null, + }); + + final repository = UserRepositoryImpl(client); + final me = await repository.getMe(); + + expect(me.id, 'u1'); + expect(me.username, 'alice'); + expect(me.avatarUrl, 'https://img'); + }); +} diff --git a/apps/test/features/settings/data/services/user_profile_cache_repository_test.dart b/apps/test/features/settings/data/services/user_profile_cache_repository_test.dart new file mode 100644 index 0000000..d0ee9c9 --- /dev/null +++ b/apps/test/features/settings/data/services/user_profile_cache_repository_test.dart @@ -0,0 +1,71 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:social_app/data/cache/hybrid_cache_store.dart'; +import 'package:social_app/data/cache/memory_cache_store.dart'; +import 'package:social_app/data/models/user_profile.dart'; +import 'package:social_app/data/cache/persistent_cache_store.dart'; +import 'package:social_app/features/settings/data/services/user_profile_cache_repository.dart'; + +void main() { + group('UserProfileCacheRepository', () { + test('keeps in-memory snapshot and invalidates correctly', () async { + var remoteCalls = 0; + final repository = UserProfileCacheRepository( + store: HybridCacheStore( + memory: MemoryCacheStore(), + persistent: PersistentCacheStore(), + ), + remoteLoader: () async { + remoteCalls += 1; + return UserProfile(id: 'u1', username: 'user-$remoteCalls'); + }, + ); + + final first = await repository.getProfile(); + final second = await repository.getProfile(); + + expect(first.username, 'user-1'); + expect(second.username, 'user-1'); + expect(repository.cachedUser?.username, 'user-1'); + expect(remoteCalls, 1); + + await repository.invalidate(); + + expect(repository.cachedUser, isNull); + + final afterInvalidate = await repository.getProfile(); + expect(afterInvalidate.username, 'user-2'); + expect(remoteCalls, 2); + }); + + test( + 'invalidate prevents stale in-flight refresh from restoring cache', + () async { + final completer = Completer(); + final repository = UserProfileCacheRepository( + store: HybridCacheStore( + memory: MemoryCacheStore(), + persistent: PersistentCacheStore(), + ), + remoteLoader: () => completer.future, + ); + + final pending = repository.getProfile(forceRefresh: true); + await repository.invalidate(); + + completer.complete( + const UserProfile(id: 'u-old', username: 'old-user'), + ); + await pending; + + expect(repository.cachedUser, isNull); + + final cachedEntry = await repository.readCacheEntry( + UserProfileCacheRepository.cacheKey, + ); + expect(cachedEntry, isNull); + }, + ); + }); +} diff --git a/backend/src/core/config/static/route/frontend_routes.yaml b/backend/src/core/config/static/route/frontend_routes.yaml index 658b968..748c7b5 100644 --- a/backend/src/core/config/static/route/frontend_routes.yaml +++ b/backend/src/core/config/static/route/frontend_routes.yaml @@ -6,12 +6,12 @@ routes: category: auth auth_required: false - route_id: auth.login - path: / + path: /login description: Login entry for unauthenticated users. category: auth auth_required: false - route_id: home.main - path: /home + path: / description: Main assistant home screen. category: home auth_required: true diff --git a/docs/bugs/2026-03-27-repository缓存抽象.md b/docs/bugs/2026-03-27-repository缓存抽象.md deleted file mode 100644 index 5222add..0000000 --- a/docs/bugs/2026-03-27-repository缓存抽象.md +++ /dev/null @@ -1,140 +0,0 @@ -# Repository 缓存层抽象优化 - -## 问题描述 - -### 现有架构 - -``` -┌─────────────────────────────────────────┐ -│ HybridCacheStore │ -│ (Memory + Persistent 二级缓存) │ -├─────────────────────────────────────────┤ -│ CacheEntry │ -│ (value + fetchedAt 时间戳) │ -├─────────────────────────────────────────┤ -│ CachePolicy │ -│ (softTtl / hardTtl / minRefreshInterval)│ -├─────────────────────────────────────────┤ -│ CacheInvalidator │ -│ (统一失效管理) │ -└─────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────┐ -│ CalendarRepository │ ← 重复实现 -│ TodoRepository │ ← 重复实现 -│ UsersRepository │ ← 重复实现 -│ ... │ -└─────────────────────────────────────────┘ -``` - -### 重复内容 - -| 重复内容 | 例子 | -|---------|------| -| key 命名空间 | `calendar:day:$day`、`todo:list:pending` | -| 缓存读取逻辑 | `store.read>(key)` | -| 数据转换 | API 返回 → CacheEntry 包装 | -| 刷新逻辑 | `_refreshDayAndRead()` | -| 强制刷新 | `forceRefresh` 参数处理 | -| 后台刷新防重 | `_refreshInFlight` map | - -### 涉及文件 - -- `apps/lib/features/calendar/data/services/calendar_repository.dart` -- `apps/lib/features/todo/data/todo_repository.dart` -- `apps/lib/features/contacts/data/users/users_repository_impl.dart` -- `apps/lib/features/settings/data/services/user_profile_cache_repository.dart` - -## 建议方案 - -### 1. 抽取 `CachedRepository` 基类 - -```dart -abstract class CachedRepository { - HybridCacheStore get store; - CacheInvalidator get invalidator; - CachePolicy get policy; - - String get namespace; // 'calendar', 'todo', etc. - - Future getOrLoad( - String key, { - bool forceRefresh = false, - required Future Function() loader, - }); - - Future invalidate(String key); - - String buildKey(String suffix); -} -``` - -### 2. 各模块简化 - -```dart -// CalendarRepository -class CalendarRepository extends CachedRepository, ScheduleItemModel> { - @override - String get namespace => 'calendar'; - - @override - Future> getDayEvents(DateTime date, {bool forceRefresh}) { - return getOrLoad( - 'day:${_formatDate(date)}', - forceRefresh: forceRefresh, - loader: () => calendarService.getEventsForDay(date), - ); - } - - String _formatDate(DateTime date) => - '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; -} - -// TodoRepository -class TodoRepository extends CachedRepository, TodoResponse> { - @override - String get namespace => 'todo'; - - Future> getPendingTodos({bool forceRefresh = false}) { - return getOrLoad( - 'list:pending', - forceRefresh: forceRefresh, - loader: () => api.getPendingTodos(), - ); - } -} -``` - -### 3. 可选:泛型缓存装饰器 - -```dart -class CachedApiCall { - final HybridCacheStore store; - final CachePolicy policy; - final String key; - final DateTime Function() now; - - Future execute(Future Function() loader); -} -``` - -## 收益 - -| 收益 | 说明 | -|------|------| -| 减少重复代码 | 各 Repository 移除 60%+ 相似逻辑 | -| 统一缓存行为 | 刷新策略、key 格式、并发控制一致 | -| 易维护 | 修复 bug 或优化逻辑只需改一处 | -| 易测试 | 基类可独立测试,子类继承即可 | - -## 前置依赖 - -- 现有 `HybridCacheStore`、`CacheEntry`、`CachePolicy`、`CacheInvalidator` 已就绪 -- 无需引入新依赖 - -## 状态 - -- [ ] 待评估优先级 -- [ ] 待设计 CachedRepository 基类接口 -- [ ] 先在一个 Repository 上试点 -- [ ] 推广到其他 Repository diff --git a/docs/bugs/AppTheme硬编码颜色且缺失DarkMode.md b/docs/bugs/AppTheme硬编码颜色且缺失DarkMode.md deleted file mode 100644 index 692fd17..0000000 --- a/docs/bugs/AppTheme硬编码颜色且缺失DarkMode.md +++ /dev/null @@ -1,90 +0,0 @@ -# AppTheme 硬编码颜色且缺失 Dark Mode - -## 问题描述 - -### 1. 颜色硬编码 - -`AppTheme` 和各组件大量直接引用 `AppColors` 静态常量,而非 `Theme.of(context).colorScheme`: - -```dart -// app_theme.dart -appBarTheme: const AppBarTheme( - backgroundColor: AppColors.background, // 硬编码 - foregroundColor: AppColors.slate900, // 硬编码 -), - -elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primary, // 硬编码 - foregroundColor: AppColors.primaryForeground, - ), -), -``` - -这导致: -- 主题切换时颜色不会改变 -- 组件无法响应系统深色模式 -- 违反 Flutter Material Design 规范 - -### 2. 缺失 Dark Mode - -`AppTheme` 只有 `light` getter,没有 `dark`: - -```dart -static ThemeData get light => ThemeData(...); -``` - -`LinksyApp` 硬编码使用 light: -```dart -theme: AppTheme.light, -locale: const Locale('zh'), -``` - -## 正确做法 - -### 颜色应使用 ThemeData - -```dart -// 正确示例 -appBarTheme: AppBarTheme( - backgroundColor: Theme.of(context).colorScheme.surface, - foregroundColor: Theme.of(context).colorScheme.onSurface, -), - -// ColorScheme 应由 ThemeData 生成 -colorScheme: ColorScheme.fromSeed( - seedColor: AppColors.primary, - brightness: Brightness.light, // 或 Brightness.dark -), -``` - -### 支持 Dark Mode - -```dart -class AppTheme { - static ThemeData get light => ThemeData( - brightness: Brightness.light, - colorScheme: ColorScheme.fromSeed( - seedColor: AppColors.primary, - brightness: Brightness.light, - ), - ); - - static ThemeData get dark => ThemeData( - brightness: Brightness.dark, - colorScheme: ColorScheme.fromSeed( - seedColor: AppColors.primary, - brightness: Brightness.dark, - ), - ); -} -``` - -## 相关文件 - -- `apps/lib/core/theme/app_theme.dart` -- `apps/lib/core/theme/design_tokens.dart` - -## 修复优先级 - -**低** - 当前只有 light 模式,不影响功能 diff --git a/docs/bugs/AuthSessionBootstrapper旧代码应删除.md b/docs/bugs/AuthSessionBootstrapper旧代码应删除.md deleted file mode 100644 index 5faf8f2..0000000 --- a/docs/bugs/AuthSessionBootstrapper旧代码应删除.md +++ /dev/null @@ -1,43 +0,0 @@ -# AuthSessionBootstrapper 旧代码应删除 - -## 文件位置 - -`apps/lib/app/startup/auth_session_bootstrapper.dart` - -## 问题描述 - -`AuthSessionBootstrapper` 是遗留代码,用于在用户登录时同步日历事件和通知提醒。 - -### 代码问题 - -```dart -Future syncForAuthState(AuthState state) async { - if (state is! AuthAuthenticated) { - _syncedUserId = null; - return; - } - // 获取180天日历事件并重建通知提醒 - final events = await _calendarService.getEventsForRange(start, end); - await _notificationService.rebuildUpcomingReminders(events); - ... -} -``` - -1. **同步逻辑已迁移** - `CalendarService` 和 `LocalNotificationService` 应自己管理缓存生命周期,无需登录时手动触发 -2. **内存缓存不可靠** - `_syncedUserId` 仅内存存储,App 重启后失效 -3. **静默失败** - 同步失败被 `catch (_)` 吞掉,无日志无重试 -4. **180 天硬编码** - 时间范围未从配置读取 - -## 处理方式 - -**直接删除**: -- 删除 `apps/lib/app/startup/auth_session_bootstrapper.dart` -- 确认无调用处后,清理 `startup/` 目录(若为空) - -## 相关文件 - -- `apps/lib/app/startup/auth_session_bootstrapper.dart` - -## 修复优先级 - -**低** - 功能层面暂无影响,但属于应清理的技术债 diff --git a/docs/bugs/LinksyApp强制依赖ChatBloc.md b/docs/bugs/LinksyApp强制依赖ChatBloc.md deleted file mode 100644 index 518b05c..0000000 --- a/docs/bugs/LinksyApp强制依赖ChatBloc.md +++ /dev/null @@ -1,49 +0,0 @@ -# LinksyApp 强制依赖 ChatBloc - -## 问题描述 - -`LinksyApp` (app.dart) 作为应用根节点,被迫在 `MultiBlocProvider` 中注入 `ChatBloc`: - -```dart -return MultiBlocProvider( - providers: [ - BlocProvider.value(value: authBloc), - BlocProvider( - create: (_) => ChatBloc(apiClient: sl()), - ), - ], - ... -); -``` - -这导致: -1. 应用启动时就创建 `ChatBloc` 实例(内存浪费) -2. `LinksyApp` 需要知道"存在 ChatBloc 这个 Feature" -3. 违反单一职责原则:根节点应只负责全局配置,不应了解具体 Feature - -## 根本原因 - -`HomeScreen` 是默认首页,其内部需要 `ChatBloc`。为了让它通过 `context.read()` 获取,被迫在根节点提供。 - -## 正确做法 - -ChatBloc 应该在路由级别按需注入: - -```dart -GoRoute( - path: '/', - builder: (context) => BlocProvider( - create: (_) => ChatBloc(apiClient: sl()), - child: const HomeScreen(), - ), -) -``` - -## 相关文件 - -- `apps/lib/app/app.dart` -- `apps/lib/features/home/presentation/screens/home_screen.dart` - -## 修复优先级 - -**中等** - 功能正常但架构不合理,属于技术债 diff --git a/docs/bugs/main与AuthBloc耦合.md b/docs/bugs/main与AuthBloc耦合.md deleted file mode 100644 index 6d1407b..0000000 --- a/docs/bugs/main与AuthBloc耦合.md +++ /dev/null @@ -1,118 +0,0 @@ -# main.dart 与认证模块耦合 - -## 问题描述 - -当前 `main.dart` 直接依赖了 `AuthBloc` 和 `AuthStarted`,违反了依赖反转原则。 - -## 当前代码 - -```dart -// main.dart -import 'features/auth/presentation/bloc/auth_bloc.dart'; -import 'features/auth/presentation/bloc/auth_event.dart'; - -void main() async { - // ... - final authBloc = sl(); - authBloc.add(AuthStarted()); - runApp(LinksyApp(authBloc: authBloc)); -} -``` - -## 问题 - -1. **启动逻辑与认证模块耦合** - - main.dart 需要知道 `AuthBloc` 的存在 - - 需要知道 `AuthStarted` 事件 - - 需要手动触发启动事件 - -2. **AuthBloc 被暴露3层** - ``` - main.dart → LinksyApp → createAppRouter → redirect() - ``` - 每层都传 authBloc,不优雅 - -## 建议方案 - -### 1. 启动逻辑下沉到 LinksyApp - -```dart -// main.dart -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - await configureDependencies(); - await AppConstants.init(); - runApp(LinksyApp()); -} - -// app.dart (LinksyApp) -class LinksyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - final authBloc = sl(); - authBloc.add(AuthStarted()); - - return BlocProvider.value( - value: authBloc, - child: // ... - ); - } -} -``` - -### 2. 路由守卫由 LinksyApp 内部管理 - -```dart -// app.dart -class LinksyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return BlocListener( - listener: (context, state) { - final router = GoRouter.of(context); - if (state is AuthUnauthenticated) { - router.go(AppRoutes.authLogin); - } else if (state is AuthAuthenticated) { - if (router.matchedLocation == AppRoutes.authLogin) { - router.go(AppRoutes.homeMain); - } - } - }, - child: MaterialApp.router( - routerConfig: createAppRouter(), - ), - ); - } -} -``` - -### 3. createAppRouter 不再需要 authBloc 参数 - -```dart -// app_router.dart -GoRouter createAppRouter() { - return GoRouter( - // 不再有 redirect 回调 - // 路由守卫由 BlocListener 在 LinksyApp 统一处理 - ); -} -``` - -## 收益 - -| 收益 | 说明 | -|------|------| -| 解耦 | main.dart 完全不知道 AuthBloc 存在 | -| 单一职责 | LinksyApp 统一管理状态监听和路由跳转 | -| 易测试 | main.dart 不再需要 mock AuthBloc | - -## 涉及文件 - -- `apps/lib/main.dart` -- `apps/lib/app/app.dart` -- `apps/lib/app/router/app_router.dart` -- `apps/lib/features/auth/presentation/bloc/auth_bloc.dart` - -## 状态 - -- [ ] 待修复 diff --git a/docs/bugs/sharedpreferences缺少统一管理模型.md b/docs/bugs/sharedpreferences缺少统一管理模型.md deleted file mode 100644 index 50ca2ca..0000000 --- a/docs/bugs/sharedpreferences缺少统一管理模型.md +++ /dev/null @@ -1,85 +0,0 @@ -# SharedPreferences 缺少统一管理模型 - -## 问题描述 - -当前 `SharedPreferences` 的使用散落各处,缺乏统一的数据模型约束: - -### 现状 - -1. **Key 散落** - - `reminder_notification_callbacks.dart` 中定义:`'calendar_reminder_pending_notification_responses_v1'` - - 各处直接使用字符串 key,容易拼写错误或冲突 - -2. **重复获取实例** - ```dart - final prefs = await SharedPreferences.getInstance(); // 每次都重新获取 - ``` - -3. **序列化逻辑分散** - - `ReminderNotificationCallbacks` 自己处理 JSON 序列化/反序列化 - - 其他模块可能重复相同逻辑 - -4. **注册但未统一封装** - - `injection.dart` 只注册了 `SharedPreferences` 实例 - - 没有封装成可复用的数据访问层 - -## 影响 - -- 维护困难:Key 散落,修改时需要全局搜索 -- 容易出错:拼写错误难以发现 -- 代码重复:序列化逻辑可能在多处重复实现 -- 可测试性差:直接依赖 `SharedPreferences.getInstance()` - -## 建议方案 - -### 1. 创建 `AppPreferences` 数据模型 - -```dart -class AppPreferences { - static const String _pendingNotificationsKey = - 'calendar_reminder_pending_notification_responses_v1'; - - final SharedPreferences _prefs; - - AppPreferences(this._prefs); - - List get pendingNotifications { - final list = _prefs.getStringList(_pendingNotificationsKey) ?? []; - return list.map(_decode).toList(); - } - - Future setPendingNotifications(List value) { - return _prefs.setStringList(_pendingNotificationsKey, value.map(_encode).toList()); - } - - // 其他偏好设置... -} -``` - -### 2. 在 injection.dart 中注册 - -```dart -final sharedPreferences = await SharedPreferences.getInstance(); -sl.registerSingleton(AppPreferences(sharedPreferences)); -``` - -### 3. 使用方通过接口访问 - -```dart -// 之前 -final prefs = await SharedPreferences.getInstance(); -await prefs.setStringList(key, value); - -// 之后 -sl().setPendingNotifications(value); -``` - -## 涉及文件 - -- `apps/lib/app/di/injection.dart` - 注册逻辑 -- `apps/lib/features/notification/data/services/reminder_notification_callbacks.dart` - 主要使用方 -- `apps/lib/features/notification/data/services/ios_notification_payload_bridge.dart` - 另一使用方 - -## 状态 - -- [ ] 待修复 diff --git a/docs/bugs/服务层与Repository层职责混乱.md b/docs/bugs/服务层与Repository层职责混乱.md deleted file mode 100644 index 5ce8c5a..0000000 --- a/docs/bugs/服务层与Repository层职责混乱.md +++ /dev/null @@ -1,123 +0,0 @@ -# 服务层与 Repository 层职责混乱 - -## 问题描述 - -当前 `CalendarService`、`SettingsUserCache`、`UserProfileCacheRepository` 等服务/仓库职责边界模糊,存在大量重复逻辑和不必要的封装。 - -## 问题1:SettingsUserCache 不该存在 - -### 当前结构 - -``` -SettingsUserCache UserProfileCacheRepository -┌─────────────────┐ ┌─────────────────────────┐ -│ - _cachedUser │ ←→ │ - HybridCacheStore │ -│ - getProfile() │ │ - CachePolicy │ -│ - set() │ │ - getProfile() │ -│ - invalidate() │ │ - setCached() │ -└─────────────────┘ └─────────────────────────┘ -``` - -### 问题 - -- `SettingsUserCache` 只是给 `UserProfileCacheRepository` 包了一层内存缓存 -- 两者的 `getProfile()`、`invalidate()` 逻辑几乎相同 -- 这是重复包装,应该合并 - -## 问题2:Repository 缓存逻辑重复 - -### 涉及文件 - -- `apps/lib/features/calendar/data/services/calendar_repository.dart` -- `apps/lib/features/settings/data/services/user_profile_cache_repository.dart` -- `apps/lib/features/todo/data/todo_repository.dart` - -### 代码重复率:90% - -```dart -// CalendarRepository -Future> getDayEvents({bool forceRefresh}) async { - if (forceRefresh) return _refreshDayAndRead(...); - final cached = await store.read>(key); - if (cached == null) return _refreshDayAndRead(...); - final decision = policy.evaluate(now: now(), fetchedAt: cached.fetchedAt); - if (decision.shouldRefreshInBackground) _refreshInBackground(); - if (decision.mustBlockForNetwork || !decision.canUseCached) { - return _refreshDayAndRead(...); - } - return cached.value; -} - -// UserProfileCacheRepository -Future getProfile({bool forceRefresh}) async { - if (forceRefresh) return _refreshAndRead(); - final cached = await store.read>(cacheKey); - if (cached == null) return _refreshAndRead(); - final decision = policy.evaluate(now: now(), fetchedAt: cached.fetchedAt); - if (decision.shouldRefreshInBackground) _refreshInBackground(); - if (decision.mustBlockForNetwork || !decision.canUseCached) { - return _refreshAndRead(); - } - return cached.value; -} -``` - -## 问题3:CalendarService 不必要的延迟初始化 - -```dart -class CalendarService { - CalendarApi? _calendarApi; - - CalendarApi get _api { - if (_calendarApi != null) return _calendarApi; - _calendarApi = CalendarApi(_apiClient); // 为什么懒加载? - return _calendarApi; - } -} -``` - -已经传入了 `IApiClient`,API 还在构造时懒加载,多此一举。 - -## 问题4:分层不清 - -| 类名 | 类型 | 问题 | -|------|------|------| -| `CalendarService` | Service | 依赖 Repository,该叫 Repository | -| `UserProfileCacheRepository` | Repository | 名字带 Cache,但 Repository 都带缓存 | -| `SettingsUserCache` | ??? | 内存缓存层,不该独立存在 | -| `TodoRepository` | Repository | 正确 | - -## 应该的设计 - -``` -Repository 层(纯数据 + 缓存) -├── CalendarRepository ← 继承 CachedRepository -├── UserProfileRepository ← 继承 CachedRepository -└── TodoRepository ← 继承 CachedRepository - -Service 层(业务逻辑 + 跨 Repository 编排) -├── CalendarService ← 只做业务编排,不直接调 API -├── NotificationService ← 跨模块通知逻辑 -└── ReminderActionExecutor ← 跨模块提醒执行 -``` - -## 修复步骤 - -1. **删除** `SettingsUserCache`,合并到 `UserProfileCacheRepository` -2. **抽取** `CachedRepository` 基类(见 `docs/todo/2026-03-27-repository缓存抽象.md`) -3. **简化** `CalendarService`,移除不必要的懒加载 -4. **统一命名**: - - 带缓存的 Repository 统一继承基类 - - Service 只做业务编排,不处理缓存 - -## 涉及文件 - -- `apps/lib/features/calendar/data/services/calendar_service.dart` -- `apps/lib/features/calendar/data/services/calendar_repository.dart` -- `apps/lib/features/settings/data/services/settings_user_cache.dart` -- `apps/lib/features/settings/data/services/user_profile_cache_repository.dart` -- `apps/lib/features/todo/data/todo_repository.dart` - -## 状态 - -- [ ] 待修复 diff --git a/docs/bugs/根路径定义为登录页.md b/docs/bugs/根路径定义为登录页.md deleted file mode 100644 index 9942618..0000000 --- a/docs/bugs/根路径定义为登录页.md +++ /dev/null @@ -1,34 +0,0 @@ -# 路由语义混乱:根路径 `/` 定义为登录页 - -## 问题描述 - -`app_routes.dart` 中根路径 `/` 被定义为登录页: - -```dart -static const authBoot = '/boot'; -static const authLogin = '/'; // 根路径是登录页 -static const homeMain = '/home'; // 首页反而在 /home -``` - -这导致: -- `/` 应该指向首页的直觉 expectation 违反 -- 根路径无法放置真实首页内容 -- 与 `homeMain = '/home'` 语义不一致 - -## 正确做法 - -根路径 `/` 应保留给首页,登录页应使用独立路径如 `/login`: - -```dart -static const authLogin = '/login'; -static const homeMain = '/'; -``` - -## 相关文件 - -- `apps/lib/app/router/app_routes.dart` -- `apps/lib/app/router/app_router.dart` - -## 修复优先级 - -**低** - 功能正常,属于历史遗留设计问题 diff --git a/docs/plans/2026-03-27-app-theme-dark-mode-migration.md b/docs/plans/2026-03-27-app-theme-dark-mode-migration.md new file mode 100644 index 0000000..06a5b89 --- /dev/null +++ b/docs/plans/2026-03-27-app-theme-dark-mode-migration.md @@ -0,0 +1,126 @@ +# App Theme Dark Mode and Hardcoded Color Migration Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Enable automatic light/dark mode following system settings and gradually migrate UI color usage from direct `AppColors` references to theme-driven semantic colors. + +**Architecture:** Introduce a dual-theme foundation in `AppTheme` (`light` and `dark`) and route app root to `ThemeMode.system`. Then migrate color access in batches: first shared widgets, then high-traffic feature pages, then long-tail modules. Keep `AppColors` as design-token source only inside theme/token layers during migration. + +**Tech Stack:** Flutter Material 3 (`ThemeData`, `ColorScheme`, `ThemeMode`), existing design tokens (`AppColors`, `AppSpacing`, `AppRadius`), flutter analyze/test verification. + +--- + +### Task 1: Theme Foundation and System Dark Mode Wiring + +**Files:** +- Modify: `apps/lib/core/theme/app_theme.dart` +- Modify: `apps/lib/app/app.dart` + +**Step 1: Build dual-theme entry points** + +- Add `AppTheme.dark` and a shared private builder `AppTheme._buildTheme(Brightness)`. +- Ensure light and dark themes both use Material 3 and `ColorScheme.fromSeed`. + +**Step 2: Remove theme-level hardcoded semantic colors** + +- Replace direct `AppColors` usage in app bar/button/input decoration theme with color-scheme-driven values. +- Keep radii and spacing tokens unchanged. + +**Step 3: Wire root app to system mode** + +- In `MaterialApp.router`, set `theme`, `darkTheme`, and `themeMode: ThemeMode.system`. + +**Step 4: Verify** + +Run: `cd apps && flutter analyze` +Expected: no new analyzer errors. + +--- + +### Task 2: Shared Widget Batch (P0) - Replace Direct AppColors with Theme Semantics + +**Files:** +- Modify: `apps/lib/shared/widgets/error_retry_surface.dart` +- Modify: `apps/lib/shared/widgets/confirm_sheet.dart` +- Modify: `apps/lib/shared/widgets/destructive_action_sheet.dart` +- Modify: `apps/lib/shared/widgets/toast/toast_type_config.dart` + +**Step 1: Migrate simple error surface** + +- Use `Theme.of(context).colorScheme.error` for retry message color. + +**Step 2: Migrate confirm/destructive sheets** + +- Replace sheet background/border/text/primary-action color bindings with `ColorScheme` semantic roles (`surface`, `outlineVariant`, `onSurface`, `onSurfaceVariant`, `primary`, `onPrimary`, `error`, `onError`). + +**Step 3: Migrate toast style mapping** + +- Replace static feedback colors with semantic color-scheme mappings per toast type. +- Preserve existing icon and l10n label behavior. + +**Step 4: Verify** + +Run: `cd apps && flutter analyze` +Expected: no new analyzer errors. + +--- + +### Task 3: Batch Migration Governance and Rollout Rules + +**Files:** +- Modify: `docs/bugs/AppTheme硬编码颜色且缺失DarkMode.md` + +**Step 1: Document batch strategy** + +- Add phased rollout strategy: Foundation -> Shared widgets (P0) -> Core pages (P1) -> Long tail (P2) -> guardrails. + +**Step 2: Define acceptance and rollback criteria** + +- Include per-batch verification (`flutter analyze`, targeted widget tests/manual dark mode checks). +- Require small-scope commits per batch for safe rollback. + +--- + +### Task 4: P1/P2 Execution Backlog (Follow-up) + +**Files:** +- Modify: `apps/lib/features/home/**` +- Modify: `apps/lib/features/settings/**` +- Modify: `apps/lib/features/auth/**` +- Modify: remaining `apps/lib/features/**` + +**Step 1: Prioritize by user path** + +- P1 order: home -> settings -> auth. +- P2: remaining feature pages and low-frequency screens. + +**Step 2: Migrate per file with semantic mapping** + +- Replace direct `AppColors` usage in widget code with `Theme.of(context).colorScheme` or scoped theme helpers. +- Keep visual behavior equivalent first; optimize polish after semantic migration is stable. + +**Step 3: Verify per batch** + +Run: +- `cd apps && flutter analyze` +- `cd apps && flutter test` (targeted where tests exist) + +Expected: no regressions in targeted flows and theme switching. + +--- + +## Risks and Mitigations + +- Risk: dark mode contrast regression in legacy widgets. + - Mitigation: migrate shared widgets first, then high-traffic pages with screenshot/manual checks. +- Risk: mixed semantic + static colors during transition. + - Mitigation: enforce migration order and avoid adding new direct `AppColors` in UI layers. +- Risk: broad-scope change fatigue. + - Mitigation: ship in small batches with isolated verification and rollback points. + +## Done Criteria + +- `MaterialApp` follows `ThemeMode.system` with `theme` and `darkTheme` configured. +- Theme layer no longer relies on static semantic colors for app-wide component themes. +- P0 shared widget batch migrated to theme semantics. +- Migration plan and execution progress documented in bug tracker doc. diff --git a/docs/plans/2026-03-27-data-repositories-cache-strategy.md b/docs/plans/2026-03-27-data-repositories-cache-strategy.md new file mode 100644 index 0000000..53085c2 --- /dev/null +++ b/docs/plans/2026-03-27-data-repositories-cache-strategy.md @@ -0,0 +1,144 @@ +# Data Repositories Cache Strategy Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Establish a shared cache abstraction and shared data-repository entrypoints so cross-feature data access no longer depends on direct feature-to-feature data imports. + +**Architecture:** Keep cache infrastructure centralized in `apps/lib/data/cache/`, and expose cross-feature data through `apps/lib/data/repositories/` facades registered in DI as singletons. Feature screens consume repositories, while cache policy/key/invalidation remain in repository layer. + +**Tech Stack:** Flutter, Dart, GetIt, flutter_test + +--- + +### Task 1: Finalize Shared Cache Foundation + +**Files:** +- Modify: `apps/lib/data/cache/cached_repository.dart` +- Modify: `apps/lib/features/settings/data/services/user_profile_cache_repository.dart` +- Test: `apps/test/features/settings/data/services/user_profile_cache_repository_test.dart` + +**Step 1: Write the failing test** + +Add an invalidate-vs-inflight regression case in `user_profile_cache_repository_test.dart` asserting stale in-flight refresh cannot restore `cachedUser` after invalidate. + +**Step 2: Run test to verify it fails** + +Run: `flutter test test/features/settings/data/services/user_profile_cache_repository_test.dart` + +Expected: FAIL before generation-guard fix. + +**Step 3: Write minimal implementation** + +Add generation/version guard in `UserProfileCacheRepository` so `invalidate()` increments generation and stale async loader results are ignored for in-memory snapshot updates. + +**Step 4: Run test to verify it passes** + +Run: `flutter test test/features/settings/data/services/user_profile_cache_repository_test.dart` + +Expected: PASS. + +**Step 5: Commit** + +```bash +git add apps/lib/data/cache/cached_repository.dart apps/lib/features/settings/data/services/user_profile_cache_repository.dart apps/test/features/settings/data/services/user_profile_cache_repository_test.dart +git commit -m "refactor: harden user profile cache invalidation race handling" +``` + +### Task 2: Introduce Shared Data Repositories Module + +**Files:** +- Create: `apps/lib/data/repositories/inbox_repository.dart` +- Create: `apps/lib/data/repositories/calendar_event_repository.dart` +- Create: `apps/lib/data/repositories/user_repository.dart` +- Modify: `apps/lib/app/di/injection.dart` + +**Step 1: Write the failing test** + +Add a DI registration smoke test (or lightweight compile-level usage test) that resolves the new repositories from GetIt. + +**Step 2: Run test to verify it fails** + +Run: `flutter test ` + +Expected: FAIL because repositories are not yet defined/registered. + +**Step 3: Write minimal implementation** + +Implement repository facades that wrap existing APIs (`InboxApi`, `CalendarApi`, `UsersApi`) and register them as singletons in DI. + +**Step 4: Run test to verify it passes** + +Run: `flutter test ` + +Expected: PASS. + +**Step 5: Commit** + +```bash +git add apps/lib/data/repositories apps/lib/app/di/injection.dart +git commit -m "refactor: add shared data repositories module" +``` + +### Task 3: Migrate Cross-Feature Screens to Shared Repositories + +**Files:** +- Modify: `apps/lib/features/messages/presentation/screens/message_invite_detail_screen.dart` +- Modify: `apps/lib/features/messages/presentation/screens/message_invite_list_screen.dart` +- Modify: `apps/lib/features/todo/presentation/screens/todo_edit_screen.dart` + +**Step 1: Write the failing test** + +Add targeted tests for message detail/list data loading and todo edit schedule loading using repository dependencies. + +**Step 2: Run test to verify it fails** + +Run: `flutter test ` + +Expected: FAIL before dependency switch. + +**Step 3: Write minimal implementation** + +Replace direct API injections with shared repository injections from `apps/lib/data/repositories/*`. + +**Step 4: Run test to verify it passes** + +Run: `flutter test ` + +Expected: PASS. + +**Step 5: Commit** + +```bash +git add apps/lib/features/messages/presentation/screens/message_invite_detail_screen.dart apps/lib/features/messages/presentation/screens/message_invite_list_screen.dart apps/lib/features/todo/presentation/screens/todo_edit_screen.dart +git commit -m "refactor: switch cross-feature screens to shared repositories" +``` + +### Task 4: Verification and Cleanup + +**Files:** +- Modify: `apps/AGENTS.md` +- Modify: `docs/bugs/2026-03-27-repository缓存抽象.md` +- Modify: `docs/bugs/服务层与Repository层职责混乱.md` + +**Step 1: Run full targeted verification** + +Run: `flutter analyze lib/data/cache lib/data/repositories lib/app/di/injection.dart lib/features/messages/presentation/screens/message_invite_detail_screen.dart lib/features/messages/presentation/screens/message_invite_list_screen.dart lib/features/todo/presentation/screens/todo_edit_screen.dart` + +Expected: No issues. + +**Step 2: Run regression tests** + +Run: `flutter test test/data/cache/cached_repository_test.dart test/features/settings/data/services/user_profile_cache_repository_test.dart test/app/router/app_router_redirect_test.dart` + +Expected: PASS. + +**Step 3: Update docs status** + +Mark relevant bug docs as in-progress/resolved and document new layering rules. + +**Step 4: Commit** + +```bash +git add apps/AGENTS.md docs/bugs/2026-03-27-repository缓存抽象.md docs/bugs/服务层与Repository层职责混乱.md +git commit -m "docs: document shared repository and cache strategy" +```