This commit is contained in:
Dmitry 2025-11-21 19:59:11 +03:00
parent baaa08e98e
commit be55610403
20 changed files with 460 additions and 166 deletions

2
.vscode/launch.json vendored
View file

@ -131,7 +131,7 @@
},
{
"name": "mnemo_cards_web",
"cwd": "mnemo_cards_web",
"cwd": "mnemo_cards_web_v2",
"request": "launch",
"type": "dart"
},

View file

@ -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/<packId>/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/<packId>` still requires authentication (for payment creation)
- ✅ Added comprehensive unit tests for buy page endpoint covering all scenarios
**Technical Implementation:**
- **GET `/api/v2/packs/<packId>/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/<packId>`**: 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)

View file

@ -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/<packId>/buy')
@OpenApiRoute()
Future<Response> getPackBuyPage(Request request, String packId) async {
try {
final user = request.user;
if (user == null) {
return _unauthorized('Authentication required');
// 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) {
final buyDto = await _packManager.getPublicBuyPage(packId);
if (buyDto == null) {
return _notFound('Pack not found');
}
return _ok(buyDto.toJson());
}
// 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;
}
} on FormatException catch (_) {
return _badRequest('Invalid pack ID');
} on StateError catch (_) {

View file

@ -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<String, dynamic>;
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<String, dynamic>;
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<String, dynamic>;
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<String, dynamic>;
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<String, dynamic>;
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<String, dynamic>;
expect(response.statusCode, equals(400));
expect(responseBody['error'], equals('Bad Request'));
});
});
}

View file

@ -144,7 +144,10 @@ class _HomePageState extends State<HomePage> {
),
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 {

View file

@ -272,36 +272,41 @@ class _PackDetailsPageState extends State<PackDetailsPage> {
/// 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<Color>(Colors.white),
valueColor: AlwaysStoppedAnimation<Color>(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<PackDetailsPage> {
),
if (test.count > 0) ...[
const SizedBox(width: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
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: const TextStyle(
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: Colors.white,
color: colorScheme.onPrimary,
),
),
);
},
),
],
],
@ -767,10 +780,13 @@ class _PackDetailsPageState extends State<PackDetailsPage> {
// Иконка избранного
GestureDetector(
onTap: () async => await _toggleCardFavorite(card.id),
child: Container(
child: Builder(
builder: (context) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.9),
color: colorScheme.surface.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(4),
),
child: Icon(
@ -780,8 +796,10 @@ class _PackDetailsPageState extends State<PackDetailsPage> {
size: 16,
color: _isCardFavorite(card.id)
? Colors.red
: Colors.grey,
: colorScheme.onSurface.withOpacity(0.6),
),
);
},
),
),
@ -796,17 +814,22 @@ class _PackDetailsPageState extends State<PackDetailsPage> {
name: 'PackDetailsPage',
);
},
child: Container(
child: Builder(
builder: (context) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.9),
color: colorScheme.surface.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(4),
),
child: const Icon(
child: Icon(
Icons.visibility_outlined,
size: 16,
color: Colors.grey,
color: colorScheme.onSurface.withOpacity(0.6),
),
);
},
),
),
],

View file

@ -202,9 +202,9 @@ class _ProfilePageState extends State<ProfilePage> {
? user.email![0]
: 'U')
.toUpperCase(),
style: const TextStyle(
style: TextStyle(
fontSize: 28,
color: Colors.white,
color: theme.colorScheme.onPrimary,
fontWeight: FontWeight.bold,
),
),

View file

@ -58,19 +58,32 @@ class _StatisticsPageState extends State<StatisticsPage> with TickerProviderStat
bottom: PreferredSize(
preferredSize: const Size.fromHeight(48),
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),

View file

@ -90,8 +90,23 @@ class _TasksPageState extends State<TasksPage> with TickerProviderStateMixin {
),
// Tab bar
TabBar(
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: 'Доступные'),
@ -100,6 +115,8 @@ class _TasksPageState extends State<TasksPage> with TickerProviderStateMixin {
onTap: (index) {
// Handle tab change if needed
},
);
},
),
],
),

View file

@ -381,7 +381,17 @@ class _TestPageState extends State<TestPage> {
),
),
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),

View file

@ -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 тема

View file

@ -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(
Builder(
builder: (context) {
final colorScheme = Theme.of(context).colorScheme;
return MnemoText(
card.translation,
textStyle: const TextStyle(
textStyle: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w400,
color: Colors.black54,
color: colorScheme.onSurface.withOpacity(0.6),
),
textAlign: TextAlign.center,
maxLines: 2,
);
},
),
],
],
@ -818,14 +826,19 @@ class _CardSide extends StatelessWidget {
maxLines: 4,
)
else
Text(
Builder(
builder: (context) {
final colorScheme = Theme.of(context).colorScheme;
return Text(
'Нет мнемоники',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w400,
color: Colors.black38,
color: colorScheme.onSurface.withOpacity(0.38),
),
textAlign: TextAlign.center,
);
},
),
],
),

View file

@ -60,9 +60,11 @@ class _CardViewerState extends State<CardViewer> {
@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<CardViewer> {
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<CardViewer> {
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(
Builder(
builder: (context) {
final colorScheme = Theme.of(context).colorScheme;
return MnemoText(
card.translation,
textStyle: const TextStyle(
textStyle: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w400,
color: Colors.black54,
color: colorScheme.onSurface.withOpacity(0.6),
),
textAlign: TextAlign.center,
maxLines: 3,
);
},
),
],
],
@ -380,14 +389,19 @@ class _CardSide extends StatelessWidget {
maxLines: 5,
)
else
Text(
Builder(
builder: (context) {
final colorScheme = Theme.of(context).colorScheme;
return Text(
'Нет мнемоники',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w400,
color: Colors.black38,
color: colorScheme.onSurface.withOpacity(0.38),
),
textAlign: TextAlign.center,
);
},
),
],
),

View file

@ -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,
),

View file

@ -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),
),
),

View file

@ -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),
),
),

View file

@ -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),
),
),

View file

@ -41,7 +41,6 @@ class PackCard extends StatelessWidget {
}
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 4.0),
height: cardHeight,
decoration: BoxDecoration(
color: Colors.transparent,

View file

@ -144,7 +144,10 @@ import 'mnemo_text.dart';
Positioned(
bottom: 4,
right: 4,
child: Row(
child: Builder(
builder: (context) {
final colorScheme = Theme.of(context).colorScheme;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
// Иконка избранного
@ -153,13 +156,17 @@ import 'mnemo_text.dart';
child: Container(
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.9),
color: colorScheme.surface.withOpacity(0.9),
borderRadius: BorderRadius.circular(4),
),
child: Icon(
isFavorite ? Icons.favorite : Icons.favorite_border,
isFavorite
? Icons.favorite
: Icons.favorite_border,
size: 12,
color: isFavorite ? Colors.red : Colors.grey,
color: isFavorite
? Colors.red
: colorScheme.onSurface.withOpacity(0.6),
),
),
),
@ -170,17 +177,19 @@ import 'mnemo_text.dart';
child: Container(
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.9),
color: colorScheme.surface.withOpacity(0.9),
borderRadius: BorderRadius.circular(4),
),
child: const Icon(
child: Icon(
Icons.visibility_outlined,
size: 12,
color: Colors.grey,
color: colorScheme.onSurface.withOpacity(0.6),
),
),
),
],
);
},
),
),
],

View file

@ -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),
),