From be55610403ef7edae33ad2675f8c02e8502571aa Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 21 Nov 2025 19:59:11 +0300 Subject: [PATCH] stuff --- .vscode/launch.json | 2 +- mnemo_cards_backend/PROGRESS.md | 37 ++++++ .../lib/api/v2/packs_api_v2.dart | 37 +++++- .../test/api/v2/packs_api_v2_test.dart | 111 +++++++++++++++++ .../presentation/pages/home/home_page.dart | 5 +- .../pages/pack_details/pack_details_page.dart | 113 +++++++++++------- .../pages/profile/profile_page.dart | 4 +- .../pages/statistics/statistics_page.dart | 21 +++- .../presentation/pages/tasks/tasks_page.dart | 35 ++++-- .../presentation/pages/test/test_page.dart | 12 +- .../lib/presentation/theme/app_theme.dart | 13 ++ .../widgets/card_flipper/card_flipper.dart | 51 +++++--- .../lib/presentation/widgets/card_viewer.dart | 66 ++++++---- .../widgets/game/answer_options.dart | 12 +- .../widgets/loading/game_card_shimmer.dart | 6 +- .../widgets/loading/pack_card_shimmer.dart | 8 +- .../loading/pack_card_vertical_shimmer.dart | 8 +- .../lib/presentation/widgets/pack_card.dart | 1 - .../presentation/widgets/pack_card_item.dart | 79 ++++++------ .../widgets/pack_card_vertical.dart | 5 +- 20 files changed, 460 insertions(+), 166 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index add9edc..f3c32ec 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -131,7 +131,7 @@ }, { "name": "mnemo_cards_web", - "cwd": "mnemo_cards_web", + "cwd": "mnemo_cards_web_v2", "request": "launch", "type": "dart" }, diff --git a/mnemo_cards_backend/PROGRESS.md b/mnemo_cards_backend/PROGRESS.md index bdbe3c5..0a6b035 100644 --- a/mnemo_cards_backend/PROGRESS.md +++ b/mnemo_cards_backend/PROGRESS.md @@ -1,5 +1,42 @@ # Progress Log +## 2025-01-XX - Buy Page Access Without Authentication ✅ COMPLETED + +**Feature:** Allow unauthenticated access to pack buy page endpoint + +**Completed Tasks:** +- ✅ Modified `/api/v2/packs//buy` GET endpoint to work without authentication +- ✅ Unauthenticated users can now view buy page using `getPublicBuyPage` +- ✅ Authenticated users still use `getBuyPage` with ownership checks +- ✅ POST endpoint `/api/v2/purchases/packs/` still requires authentication (for payment creation) +- ✅ Added comprehensive unit tests for buy page endpoint covering all scenarios + +**Technical Implementation:** +- **GET `/api/v2/packs//buy`**: Now accessible without authentication + - Unauthenticated users: Returns public buy page via `PackManager.getPublicBuyPage()` + - Authenticated users: Returns buy page with ownership check via `PackManager.getBuyPage()` + - Returns 409 Conflict if authenticated user already owns the pack +- **POST `/api/v2/purchases/packs/`**: Still requires authentication (unchanged) + - This endpoint creates the actual payment, so authentication is required + +**Error Handling:** +- Invalid pack ID format: Returns 400 Bad Request +- Pack not found: Returns 404 Not Found +- Pack already purchased (authenticated): Returns 409 Conflict +- All errors properly handled for both authenticated and unauthenticated requests + +**Tests Added:** +- Test for unauthenticated user accessing buy page +- Test for authenticated user without pack accessing buy page +- Test for authenticated user who already owns pack (409 Conflict) +- Test for non-existent pack (404) for both authenticated and unauthenticated +- Test for invalid pack ID format (400) + +**Files Modified:** +- `lib/api/v2/packs_api_v2.dart` - Updated `getPackBuyPage` method +- `test/api/v2/packs_api_v2_test.dart` - Added comprehensive test suite + + ## 2025-11-16 (Evening) - Let's Encrypt SSL Certificate Setup ✅ COMPLETED **Feature:** SSL Certificate Configuration for API Domain (api.mnemo-cards.online) diff --git a/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart b/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart index e759d6a..9845209 100644 --- a/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart @@ -252,21 +252,46 @@ class PacksApiV2 { /// GET /api/v2/packs/{packId}/buy /// Returns pack purchase details (includes rewarded ads offer when available) + /// Works for both authenticated and unauthenticated users @Route.get('/packs//buy') @OpenApiRoute() Future getPackBuyPage(Request request, String packId) async { try { + // Validate pack ID format first + final packIdInt = int.tryParse(packId); + if (packIdInt == null) { + return _badRequest('Invalid pack ID'); + } + final user = request.user; + + // For unauthenticated users, return public buy page if (user == null) { - return _unauthorized('Authentication required'); + final buyDto = await _packManager.getPublicBuyPage(packId); + if (buyDto == null) { + return _notFound('Pack not found'); + } + return _ok(buyDto.toJson()); } - final buyDto = await _packManager.getBuyPage(packId, user); - if (buyDto == null) { - return _conflict('Pack already purchased'); + // For authenticated users, check if they already own the pack + try { + final buyDto = await _packManager.getBuyPage(packId, user); + if (buyDto == null) { + return _conflict('Pack already purchased'); + } + return _ok(buyDto.toJson()); + } catch (e) { + // getBuyPage may throw if pack doesn't exist (via _fetchPackModel) + // _fetchPackModel uses ! operator which throws on null + final errorString = e.toString(); + if (errorString.contains('Null check') || + errorString.contains('null') || + e is StateError) { + return _notFound('Pack not found'); + } + rethrow; } - - return _ok(buyDto.toJson()); } on FormatException catch (_) { return _badRequest('Invalid pack ID'); } on StateError catch (_) { diff --git a/mnemo_cards_backend/test/api/v2/packs_api_v2_test.dart b/mnemo_cards_backend/test/api/v2/packs_api_v2_test.dart index 6093eae..f9e1840 100644 --- a/mnemo_cards_backend/test/api/v2/packs_api_v2_test.dart +++ b/mnemo_cards_backend/test/api/v2/packs_api_v2_test.dart @@ -649,5 +649,116 @@ void main() { expect(responseBody['error'], equals('Not Found')); }); }); + + group('PacksApiV2 - Get Pack Buy Page', () { + test('should return buy page for unauthenticated user', () async { + final request = buildRequest( + 'GET', + 'http://localhost/api/v2/packs/11/buy', + ); + + final response = await packsApiV2.getPackBuyPage(request, '11'); + final responseBody = jsonDecode(await response.readAsString()) + as Map; + + expect(response.statusCode, equals(200)); + expect(responseBody['id'], equals('11')); + expect(responseBody['price'], equals('199')); + expect(responseBody.containsKey('cards'), isTrue); + }); + + test('should return buy page for authenticated user without pack', () async { + final request = buildRequest( + 'GET', + 'http://localhost/api/v2/packs/11/buy', + user: testUser, + ); + + final response = await packsApiV2.getPackBuyPage(request, '11'); + final responseBody = jsonDecode(await response.readAsString()) + as Map; + + expect(response.statusCode, equals(200)); + expect(responseBody['id'], equals('11')); + expect(responseBody['price'], equals('199')); + expect(responseBody.containsKey('cards'), isTrue); + }); + + test('should return 409 for authenticated user who already owns pack', () async { + // First, add pack to user + await testIsar.writeTxn(() async { + final user = await testIsar.userModels.get(1); + if (user != null) { + await user.packs.load(); + final pack = await testIsar.cardPackModels.get(11); + if (pack != null) { + user.packs.add(pack); + await user.packs.save(); + } + } + }); + + // Reload user to get updated packs + final updatedUser = await testIsar.userModels.get(1); + expect(updatedUser, isNotNull); + + final request = buildRequest( + 'GET', + 'http://localhost/api/v2/packs/11/buy', + user: updatedUser, + ); + + final response = await packsApiV2.getPackBuyPage(request, '11'); + final responseBody = jsonDecode(await response.readAsString()) + as Map; + + expect(response.statusCode, equals(409)); + expect(responseBody['error'], equals('Conflict')); + expect(responseBody['message'], equals('Pack already purchased')); + }); + + test('should return 404 for non-existent pack (unauthenticated)', () async { + final request = buildRequest( + 'GET', + 'http://localhost/api/v2/packs/999/buy', + ); + + final response = await packsApiV2.getPackBuyPage(request, '999'); + final responseBody = jsonDecode(await response.readAsString()) + as Map; + + expect(response.statusCode, equals(404)); + expect(responseBody['error'], equals('Not Found')); + }); + + test('should return 404 for non-existent pack (authenticated)', () async { + final request = buildRequest( + 'GET', + 'http://localhost/api/v2/packs/999/buy', + user: testUser, + ); + + final response = await packsApiV2.getPackBuyPage(request, '999'); + final responseBody = jsonDecode(await response.readAsString()) + as Map; + + expect(response.statusCode, equals(404)); + expect(responseBody['error'], equals('Not Found')); + }); + + test('should return 400 for invalid pack ID', () async { + final request = buildRequest( + 'GET', + 'http://localhost/api/v2/packs/invalid/buy', + ); + + final response = await packsApiV2.getPackBuyPage(request, 'invalid'); + final responseBody = jsonDecode(await response.readAsString()) + as Map; + + expect(response.statusCode, equals(400)); + expect(responseBody['error'], equals('Bad Request')); + }); + }); } diff --git a/mnemo_cards_web_v2/lib/presentation/pages/home/home_page.dart b/mnemo_cards_web_v2/lib/presentation/pages/home/home_page.dart index e9a881e..fab2807 100644 --- a/mnemo_cards_web_v2/lib/presentation/pages/home/home_page.dart +++ b/mnemo_cards_web_v2/lib/presentation/pages/home/home_page.dart @@ -144,7 +144,10 @@ class _HomePageState extends State { ), itemCount: packs.length, itemBuilder: (context, index) { - return PackCard(pack: packs[index]); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: PackCard(pack: packs[index]), + ); }, ); } else { diff --git a/mnemo_cards_web_v2/lib/presentation/pages/pack_details/pack_details_page.dart b/mnemo_cards_web_v2/lib/presentation/pages/pack_details/pack_details_page.dart index 09fe46a..3e5aa9a 100644 --- a/mnemo_cards_web_v2/lib/presentation/pages/pack_details/pack_details_page.dart +++ b/mnemo_cards_web_v2/lib/presentation/pages/pack_details/pack_details_page.dart @@ -272,36 +272,41 @@ class _PackDetailsPageState extends State { /// Builds the Buy Pack button Widget _buildBuyPackButton(Color packColor) { + final colorScheme = Theme.of(context).colorScheme; + // Используем контрастный цвет для текста на цветном фоне + // Для темной темы используем белый, для светлой - тоже белый для контраста + final textColor = colorScheme.onPrimary; + return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: FilledButton( onPressed: _isNavigatingToPurchase ? null : _navigateToPurchase, style: FilledButton.styleFrom( backgroundColor: packColor, - foregroundColor: Colors.white, + foregroundColor: textColor, minimumSize: const Size.fromHeight(56), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.0), ), ), child: _isNavigatingToPurchase - ? const SizedBox( + ? SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), + valueColor: AlwaysStoppedAnimation(textColor), ), ) : Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.shopping_cart, size: 20), + Icon(Icons.shopping_cart, size: 20, color: textColor), const SizedBox(width: 8), Text( 'Buy Pack', style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Colors.white, + color: textColor, fontWeight: FontWeight.bold, ), ), @@ -486,20 +491,28 @@ class _PackDetailsPageState extends State { ), if (test.count > 0) ...[ const SizedBox(width: 4), - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: packColor, - borderRadius: BorderRadius.circular(10), - ), - child: Text( - '${test.count}', - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w700, - color: Colors.white, - ), - ), + Builder( + builder: (context) { + final colorScheme = Theme.of(context).colorScheme; + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: packColor, + borderRadius: BorderRadius.circular(10), + ), + child: Text( + '${test.count}', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: colorScheme.onPrimary, + ), + ), + ); + }, ), ], ], @@ -767,21 +780,26 @@ class _PackDetailsPageState extends State { // Иконка избранного GestureDetector( onTap: () async => await _toggleCardFavorite(card.id), - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.9), - borderRadius: BorderRadius.circular(4), - ), - child: Icon( - _isCardFavorite(card.id) - ? Icons.favorite - : Icons.favorite_border, - size: 16, - color: _isCardFavorite(card.id) - ? Colors.red - : Colors.grey, - ), + child: Builder( + builder: (context) { + final colorScheme = Theme.of(context).colorScheme; + return Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: colorScheme.surface.withValues(alpha: 0.9), + borderRadius: BorderRadius.circular(4), + ), + child: Icon( + _isCardFavorite(card.id) + ? Icons.favorite + : Icons.favorite_border, + size: 16, + color: _isCardFavorite(card.id) + ? Colors.red + : colorScheme.onSurface.withOpacity(0.6), + ), + ); + }, ), ), @@ -796,17 +814,22 @@ class _PackDetailsPageState extends State { name: 'PackDetailsPage', ); }, - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.9), - borderRadius: BorderRadius.circular(4), - ), - child: const Icon( - Icons.visibility_outlined, - size: 16, - color: Colors.grey, - ), + child: Builder( + builder: (context) { + final colorScheme = Theme.of(context).colorScheme; + return Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: colorScheme.surface.withValues(alpha: 0.9), + borderRadius: BorderRadius.circular(4), + ), + child: Icon( + Icons.visibility_outlined, + size: 16, + color: colorScheme.onSurface.withOpacity(0.6), + ), + ); + }, ), ), ], diff --git a/mnemo_cards_web_v2/lib/presentation/pages/profile/profile_page.dart b/mnemo_cards_web_v2/lib/presentation/pages/profile/profile_page.dart index 74eb43b..ea54818 100644 --- a/mnemo_cards_web_v2/lib/presentation/pages/profile/profile_page.dart +++ b/mnemo_cards_web_v2/lib/presentation/pages/profile/profile_page.dart @@ -202,9 +202,9 @@ class _ProfilePageState extends State { ? user.email![0] : 'U') .toUpperCase(), - style: const TextStyle( + style: TextStyle( fontSize: 28, - color: Colors.white, + color: theme.colorScheme.onPrimary, fontWeight: FontWeight.bold, ), ), diff --git a/mnemo_cards_web_v2/lib/presentation/pages/statistics/statistics_page.dart b/mnemo_cards_web_v2/lib/presentation/pages/statistics/statistics_page.dart index 1cfe255..4615799 100644 --- a/mnemo_cards_web_v2/lib/presentation/pages/statistics/statistics_page.dart +++ b/mnemo_cards_web_v2/lib/presentation/pages/statistics/statistics_page.dart @@ -57,20 +57,33 @@ class _StatisticsPageState extends State with TickerProviderStat foregroundColor: theme.colorScheme.onSurface, bottom: PreferredSize( preferredSize: const Size.fromHeight(48), - child: Container( - color: theme.colorScheme.surface, + child: Container( + decoration: BoxDecoration( + color: theme.colorScheme.surface, + border: Border( + bottom: BorderSide( + color: theme.dividerColor, + width: 1, + ), + ), + ), child: TabBar( controller: _tabController, isScrollable: true, tabAlignment: TabAlignment.start, - labelColor: theme.colorScheme.primary, + labelColor: theme.colorScheme.onSurface, unselectedLabelColor: theme.colorScheme.onSurfaceVariant, indicatorColor: theme.colorScheme.primary, indicatorWeight: 3, + overlayColor: WidgetStateProperty.all(Colors.transparent), + dividerColor: Colors.transparent, labelStyle: theme.textTheme.labelLarge?.copyWith( fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface, + ), + unselectedLabelStyle: theme.textTheme.labelLarge?.copyWith( + color: theme.colorScheme.onSurfaceVariant, ), - unselectedLabelStyle: theme.textTheme.labelLarge, tabs: const [ Tab( icon: Icon(Icons.dashboard_outlined), diff --git a/mnemo_cards_web_v2/lib/presentation/pages/tasks/tasks_page.dart b/mnemo_cards_web_v2/lib/presentation/pages/tasks/tasks_page.dart index fad94c2..6c2e93e 100644 --- a/mnemo_cards_web_v2/lib/presentation/pages/tasks/tasks_page.dart +++ b/mnemo_cards_web_v2/lib/presentation/pages/tasks/tasks_page.dart @@ -90,15 +90,32 @@ class _TasksPageState extends State with TickerProviderStateMixin { ), // Tab bar - TabBar( - controller: _tabController, - tabs: const [ - Tab(text: 'Все'), - Tab(text: 'Доступные'), - Tab(text: 'Выполненные'), - ], - onTap: (index) { - // Handle tab change if needed + Builder( + builder: (context) { + final theme = Theme.of(context); + return TabBar( + controller: _tabController, + labelColor: theme.colorScheme.onSurface, + unselectedLabelColor: theme.colorScheme.onSurfaceVariant, + indicatorColor: theme.colorScheme.primary, + indicatorWeight: 3, + dividerColor: Colors.transparent, + labelStyle: theme.textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface, + ), + unselectedLabelStyle: theme.textTheme.labelLarge?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + tabs: const [ + Tab(text: 'Все'), + Tab(text: 'Доступные'), + Tab(text: 'Выполненные'), + ], + onTap: (index) { + // Handle tab change if needed + }, + ); }, ), ], diff --git a/mnemo_cards_web_v2/lib/presentation/pages/test/test_page.dart b/mnemo_cards_web_v2/lib/presentation/pages/test/test_page.dart index 1dfe40d..cbb6b26 100644 --- a/mnemo_cards_web_v2/lib/presentation/pages/test/test_page.dart +++ b/mnemo_cards_web_v2/lib/presentation/pages/test/test_page.dart @@ -381,7 +381,17 @@ class _TestPageState extends State { ), ), child: isSelected - ? const Icon(Icons.check, color: Colors.white, size: 16) + ? Builder( + builder: (context) { + final colorScheme = + Theme.of(context).colorScheme; + return Icon( + Icons.check, + color: colorScheme.onPrimary, + size: 16, + ); + }, + ) : null, ), const SizedBox(width: 12), diff --git a/mnemo_cards_web_v2/lib/presentation/theme/app_theme.dart b/mnemo_cards_web_v2/lib/presentation/theme/app_theme.dart index 2b399ba..18ac1c8 100644 --- a/mnemo_cards_web_v2/lib/presentation/theme/app_theme.dart +++ b/mnemo_cards_web_v2/lib/presentation/theme/app_theme.dart @@ -39,6 +39,15 @@ class AppTheme { foregroundColor: AppColors.black, ), + // TabBar тема для светлой темы + tabBarTheme: const TabBarThemeData( + labelColor: AppColors.black, + unselectedLabelColor: AppColors.borderGray, + indicatorColor: AppColors.black, + labelStyle: TextStyle(fontWeight: FontWeight.w600), + unselectedLabelStyle: TextStyle(fontWeight: FontWeight.w700), + ), + // Card тема - минимальная elevation, скругленные углы cardTheme: CardThemeData( elevation: 1, @@ -111,6 +120,10 @@ class AppTheme { // TabBar тема для темной темы tabBarTheme: const TabBarThemeData( labelColor: AppColors.white, + unselectedLabelColor: Colors.grey, + indicatorColor: AppColors.white, + labelStyle: TextStyle(fontWeight: FontWeight.w600), + unselectedLabelStyle: TextStyle(fontWeight: FontWeight.w700), ), // Card тема diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/card_flipper/card_flipper.dart b/mnemo_cards_web_v2/lib/presentation/widgets/card_flipper/card_flipper.dart index 6578086..d1b0d1e 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/card_flipper/card_flipper.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/card_flipper/card_flipper.dart @@ -677,16 +677,19 @@ class _CardSide extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + return Container( width: width, height: height, decoration: BoxDecoration( - color: Colors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(20), border: Border.all(color: packColor, width: 2), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: colorScheme.shadow.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 5), ), @@ -783,15 +786,20 @@ class _CardSide extends StatelessWidget { if (card.translation != null && card.translation!.isNotEmpty) ...[ const SizedBox(height: 8), - MnemoText( - card.translation, - textStyle: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w400, - color: Colors.black54, - ), - textAlign: TextAlign.center, - maxLines: 2, + Builder( + builder: (context) { + final colorScheme = Theme.of(context).colorScheme; + return MnemoText( + card.translation, + textStyle: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w400, + color: colorScheme.onSurface.withOpacity(0.6), + ), + textAlign: TextAlign.center, + maxLines: 2, + ); + }, ), ], ], @@ -818,14 +826,19 @@ class _CardSide extends StatelessWidget { maxLines: 4, ) else - Text( - 'Нет мнемоники', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w400, - color: Colors.black38, - ), - textAlign: TextAlign.center, + Builder( + builder: (context) { + final colorScheme = Theme.of(context).colorScheme; + return Text( + 'Нет мнемоники', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w400, + color: colorScheme.onSurface.withOpacity(0.38), + ), + textAlign: TextAlign.center, + ); + }, ), ], ), diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/card_viewer.dart b/mnemo_cards_web_v2/lib/presentation/widgets/card_viewer.dart index ea45711..a44874e 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/card_viewer.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/card_viewer.dart @@ -60,9 +60,11 @@ class _CardViewerState extends State { @override Widget build(BuildContext context) { final packColor = widget.packColor ?? AppColors.borderGray; + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; return Scaffold( - backgroundColor: Colors.black, + backgroundColor: theme.scaffoldBackgroundColor, body: SafeArea( child: Stack( children: [ @@ -109,12 +111,12 @@ class _CardViewerState extends State { icon: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), + color: colorScheme.surface.withOpacity(0.7), shape: BoxShape.circle, ), - child: const Icon( + child: Icon( Icons.close, - color: Colors.white, + color: colorScheme.onSurface, ), ), ), @@ -130,13 +132,13 @@ class _CardViewerState extends State { vertical: 8, ), decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), + color: colorScheme.surface.withOpacity(0.7), borderRadius: BorderRadius.circular(20), ), child: Text( '${_currentIndex + 1} / ${widget.cards.length}', - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: colorScheme.onSurface, fontSize: 16, fontWeight: FontWeight.w600, ), @@ -230,12 +232,14 @@ class _CardSide extends StatelessWidget { final screenSize = MediaQuery.of(context).size; final cardWidth = math.min(screenSize.width * 0.9, 600.0); final cardHeight = math.min(screenSize.height * 0.7, 800.0); + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; return Container( width: cardWidth, height: cardHeight, decoration: BoxDecoration( - color: Colors.white, + color: colorScheme.surface, borderRadius: BorderRadius.circular(24), border: Border.all( color: packColor, @@ -243,7 +247,7 @@ class _CardSide extends StatelessWidget { ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.3), + color: colorScheme.shadow.withOpacity(0.3), blurRadius: 20, offset: const Offset(0, 10), ), @@ -345,15 +349,20 @@ class _CardSide extends StatelessWidget { if (card.translation != null && card.translation!.isNotEmpty) ...[ const SizedBox(height: 16), - MnemoText( - card.translation, - textStyle: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.w400, - color: Colors.black54, - ), - textAlign: TextAlign.center, - maxLines: 3, + Builder( + builder: (context) { + final colorScheme = Theme.of(context).colorScheme; + return MnemoText( + card.translation, + textStyle: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w400, + color: colorScheme.onSurface.withOpacity(0.6), + ), + textAlign: TextAlign.center, + maxLines: 3, + ); + }, ), ], ], @@ -380,14 +389,19 @@ class _CardSide extends StatelessWidget { maxLines: 5, ) else - Text( - 'Нет мнемоники', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w400, - color: Colors.black38, - ), - textAlign: TextAlign.center, + Builder( + builder: (context) { + final colorScheme = Theme.of(context).colorScheme; + return Text( + 'Нет мнемоники', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w400, + color: colorScheme.onSurface.withOpacity(0.38), + ), + textAlign: TextAlign.center, + ); + }, ), ], ), diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/game/answer_options.dart b/mnemo_cards_web_v2/lib/presentation/widgets/game/answer_options.dart index 43690a7..e7d816b 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/game/answer_options.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/game/answer_options.dart @@ -173,7 +173,17 @@ class AnswerOptions extends StatelessWidget { child: (isSelected || (isAnswerSubmitted && isCorrectOption)) - ? Icon(Icons.check, size: 12, color: Colors.white) + ? Builder( + builder: (context) { + final colorScheme = + Theme.of(context).colorScheme; + return Icon( + Icons.check, + size: 12, + color: colorScheme.onPrimary, + ); + }, + ) : null, ), diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/loading/game_card_shimmer.dart b/mnemo_cards_web_v2/lib/presentation/widgets/loading/game_card_shimmer.dart index 7c55507..b20c62f 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/loading/game_card_shimmer.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/loading/game_card_shimmer.dart @@ -21,7 +21,7 @@ class GameCardShimmer extends StatelessWidget { AspectRatio( aspectRatio: 1, child: Container( - color: Colors.white, + color: isDark ? Colors.grey[800]! : Colors.grey[200]!, ), ), // Text placeholders @@ -34,7 +34,7 @@ class GameCardShimmer extends StatelessWidget { height: 16, width: double.infinity, decoration: BoxDecoration( - color: Colors.white, + color: isDark ? Colors.grey[800]! : Colors.grey[200]!, borderRadius: BorderRadius.circular(4), ), ), @@ -43,7 +43,7 @@ class GameCardShimmer extends StatelessWidget { height: 12, width: 80, decoration: BoxDecoration( - color: Colors.white, + color: isDark ? Colors.grey[800]! : Colors.grey[200]!, borderRadius: BorderRadius.circular(4), ), ), diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/loading/pack_card_shimmer.dart b/mnemo_cards_web_v2/lib/presentation/widgets/loading/pack_card_shimmer.dart index a4b15c4..35cd4a7 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/loading/pack_card_shimmer.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/loading/pack_card_shimmer.dart @@ -34,7 +34,7 @@ class PackCardShimmer extends StatelessWidget { height: cardHeight - 2, margin: const EdgeInsets.all(1), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? Colors.grey[800]! : Colors.grey[200]!, borderRadius: BorderRadius.circular(11), ), ), @@ -54,7 +54,7 @@ class PackCardShimmer extends StatelessWidget { height: 24, width: double.infinity, decoration: BoxDecoration( - color: Colors.white, + color: isDark ? Colors.grey[800]! : Colors.grey[200]!, borderRadius: BorderRadius.circular(4), ), ), @@ -64,7 +64,7 @@ class PackCardShimmer extends StatelessWidget { height: 16, width: 150, decoration: BoxDecoration( - color: Colors.white, + color: isDark ? Colors.grey[800]! : Colors.grey[200]!, borderRadius: BorderRadius.circular(4), ), ), @@ -74,7 +74,7 @@ class PackCardShimmer extends StatelessWidget { height: 14, width: 60, decoration: BoxDecoration( - color: Colors.white, + color: isDark ? Colors.grey[800]! : Colors.grey[200]!, borderRadius: BorderRadius.circular(4), ), ), diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/loading/pack_card_vertical_shimmer.dart b/mnemo_cards_web_v2/lib/presentation/widgets/loading/pack_card_vertical_shimmer.dart index 62769f6..08e3133 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/loading/pack_card_vertical_shimmer.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/loading/pack_card_vertical_shimmer.dart @@ -31,7 +31,7 @@ class PackCardVerticalShimmer extends StatelessWidget { child: Container( margin: const EdgeInsets.all(1), decoration: BoxDecoration( - color: Colors.white, + color: isDark ? Colors.grey[800]! : Colors.grey[200]!, borderRadius: const BorderRadius.only( topLeft: Radius.circular(11), topRight: Radius.circular(11), @@ -54,7 +54,7 @@ class PackCardVerticalShimmer extends StatelessWidget { height: 20, width: double.infinity, decoration: BoxDecoration( - color: Colors.white, + color: isDark ? Colors.grey[800]! : Colors.grey[200]!, borderRadius: BorderRadius.circular(4), ), ), @@ -64,7 +64,7 @@ class PackCardVerticalShimmer extends StatelessWidget { height: 14, width: double.infinity * 0.7, decoration: BoxDecoration( - color: Colors.white, + color: isDark ? Colors.grey[800]! : Colors.grey[200]!, borderRadius: BorderRadius.circular(4), ), ), @@ -74,7 +74,7 @@ class PackCardVerticalShimmer extends StatelessWidget { height: 14, width: 50, decoration: BoxDecoration( - color: Colors.white, + color: isDark ? Colors.grey[800]! : Colors.grey[200]!, borderRadius: BorderRadius.circular(4), ), ), diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/pack_card.dart b/mnemo_cards_web_v2/lib/presentation/widgets/pack_card.dart index 983318f..bbd051a 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/pack_card.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/pack_card.dart @@ -41,7 +41,6 @@ class PackCard extends StatelessWidget { } }, child: Container( - margin: const EdgeInsets.symmetric(vertical: 4.0), height: cardHeight, decoration: BoxDecoration( color: Colors.transparent, diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/pack_card_item.dart b/mnemo_cards_web_v2/lib/presentation/widgets/pack_card_item.dart index 00e6557..632ff75 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/pack_card_item.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/pack_card_item.dart @@ -144,43 +144,52 @@ import 'mnemo_text.dart'; Positioned( bottom: 4, right: 4, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - // Иконка избранного - GestureDetector( - onTap: onToggleFavorite, - child: Container( - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.9), - borderRadius: BorderRadius.circular(4), + child: Builder( + builder: (context) { + final colorScheme = Theme.of(context).colorScheme; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Иконка избранного + GestureDetector( + onTap: onToggleFavorite, + child: Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: colorScheme.surface.withOpacity(0.9), + borderRadius: BorderRadius.circular(4), + ), + child: Icon( + isFavorite + ? Icons.favorite + : Icons.favorite_border, + size: 12, + color: isFavorite + ? Colors.red + : colorScheme.onSurface.withOpacity(0.6), + ), + ), ), - child: Icon( - isFavorite ? Icons.favorite : Icons.favorite_border, - size: 12, - color: isFavorite ? Colors.red : Colors.grey, + const SizedBox(width: 4), + // Иконка просмотра + GestureDetector( + onTap: onView ?? onTap, + child: Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: colorScheme.surface.withOpacity(0.9), + borderRadius: BorderRadius.circular(4), + ), + child: Icon( + Icons.visibility_outlined, + size: 12, + color: colorScheme.onSurface.withOpacity(0.6), + ), + ), ), - ), - ), - const SizedBox(width: 4), - // Иконка просмотра - GestureDetector( - onTap: onView ?? onTap, - child: Container( - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.9), - borderRadius: BorderRadius.circular(4), - ), - child: const Icon( - Icons.visibility_outlined, - size: 12, - color: Colors.grey, - ), - ), - ), - ], + ], + ); + }, ), ), ], diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/pack_card_vertical.dart b/mnemo_cards_web_v2/lib/presentation/widgets/pack_card_vertical.dart index c6fc28f..c18fd51 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/pack_card_vertical.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/pack_card_vertical.dart @@ -158,10 +158,7 @@ class PackCardVertical extends StatelessWidget { alignment: Alignment.center, decoration: BoxDecoration( color: tip.bgColor?.asColor ?? packColor.withOpacity(0.4), - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(12.0), - bottomRight: Radius.circular(12.0), - ), + borderRadius: const BorderRadius.all(Radius.circular(12.0)), ), child: tip.build(context), ),