stuff
This commit is contained in:
parent
baaa08e98e
commit
be55610403
20 changed files with 460 additions and 166 deletions
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
|
@ -131,7 +131,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "mnemo_cards_web",
|
"name": "mnemo_cards_web",
|
||||||
"cwd": "mnemo_cards_web",
|
"cwd": "mnemo_cards_web_v2",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"type": "dart"
|
"type": "dart"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,42 @@
|
||||||
# Progress Log
|
# 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
|
## 2025-11-16 (Evening) - Let's Encrypt SSL Certificate Setup ✅ COMPLETED
|
||||||
|
|
||||||
**Feature:** SSL Certificate Configuration for API Domain (api.mnemo-cards.online)
|
**Feature:** SSL Certificate Configuration for API Domain (api.mnemo-cards.online)
|
||||||
|
|
|
||||||
|
|
@ -252,21 +252,46 @@ class PacksApiV2 {
|
||||||
|
|
||||||
/// GET /api/v2/packs/{packId}/buy
|
/// GET /api/v2/packs/{packId}/buy
|
||||||
/// Returns pack purchase details (includes rewarded ads offer when available)
|
/// Returns pack purchase details (includes rewarded ads offer when available)
|
||||||
|
/// Works for both authenticated and unauthenticated users
|
||||||
@Route.get('/packs/<packId>/buy')
|
@Route.get('/packs/<packId>/buy')
|
||||||
@OpenApiRoute()
|
@OpenApiRoute()
|
||||||
Future<Response> getPackBuyPage(Request request, String packId) async {
|
Future<Response> getPackBuyPage(Request request, String packId) async {
|
||||||
try {
|
try {
|
||||||
|
// Validate pack ID format first
|
||||||
|
final packIdInt = int.tryParse(packId);
|
||||||
|
if (packIdInt == null) {
|
||||||
|
return _badRequest('Invalid pack ID');
|
||||||
|
}
|
||||||
|
|
||||||
final user = request.user;
|
final user = request.user;
|
||||||
|
|
||||||
|
// For unauthenticated users, return public buy page
|
||||||
if (user == null) {
|
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);
|
// For authenticated users, check if they already own the pack
|
||||||
if (buyDto == null) {
|
try {
|
||||||
return _conflict('Pack already purchased');
|
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 (_) {
|
} on FormatException catch (_) {
|
||||||
return _badRequest('Invalid pack ID');
|
return _badRequest('Invalid pack ID');
|
||||||
} on StateError catch (_) {
|
} on StateError catch (_) {
|
||||||
|
|
|
||||||
|
|
@ -649,5 +649,116 @@ void main() {
|
||||||
expect(responseBody['error'], equals('Not Found'));
|
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'));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,10 @@ class _HomePageState extends State<HomePage> {
|
||||||
),
|
),
|
||||||
itemCount: packs.length,
|
itemCount: packs.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return PackCard(pack: packs[index]);
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
|
child: PackCard(pack: packs[index]),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -272,36 +272,41 @@ class _PackDetailsPageState extends State<PackDetailsPage> {
|
||||||
|
|
||||||
/// Builds the Buy Pack button
|
/// Builds the Buy Pack button
|
||||||
Widget _buildBuyPackButton(Color packColor) {
|
Widget _buildBuyPackButton(Color packColor) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
// Используем контрастный цвет для текста на цветном фоне
|
||||||
|
// Для темной темы используем белый, для светлой - тоже белый для контраста
|
||||||
|
final textColor = colorScheme.onPrimary;
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: FilledButton(
|
child: FilledButton(
|
||||||
onPressed: _isNavigatingToPurchase ? null : _navigateToPurchase,
|
onPressed: _isNavigatingToPurchase ? null : _navigateToPurchase,
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
backgroundColor: packColor,
|
backgroundColor: packColor,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: textColor,
|
||||||
minimumSize: const Size.fromHeight(56),
|
minimumSize: const Size.fromHeight(56),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12.0),
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: _isNavigatingToPurchase
|
child: _isNavigatingToPurchase
|
||||||
? const SizedBox(
|
? SizedBox(
|
||||||
height: 20,
|
height: 20,
|
||||||
width: 20,
|
width: 20,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
valueColor: AlwaysStoppedAnimation<Color>(textColor),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Row(
|
: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.shopping_cart, size: 20),
|
Icon(Icons.shopping_cart, size: 20, color: textColor),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'Buy Pack',
|
'Buy Pack',
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
color: Colors.white,
|
color: textColor,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -486,20 +491,28 @@ class _PackDetailsPageState extends State<PackDetailsPage> {
|
||||||
),
|
),
|
||||||
if (test.count > 0) ...[
|
if (test.count > 0) ...[
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Container(
|
Builder(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
builder: (context) {
|
||||||
decoration: BoxDecoration(
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
color: packColor,
|
return Container(
|
||||||
borderRadius: BorderRadius.circular(10),
|
padding: const EdgeInsets.symmetric(
|
||||||
),
|
horizontal: 6,
|
||||||
child: Text(
|
vertical: 2,
|
||||||
'${test.count}',
|
),
|
||||||
style: const TextStyle(
|
decoration: BoxDecoration(
|
||||||
fontSize: 12,
|
color: packColor,
|
||||||
fontWeight: FontWeight.w700,
|
borderRadius: BorderRadius.circular(10),
|
||||||
color: Colors.white,
|
),
|
||||||
),
|
child: Text(
|
||||||
),
|
'${test.count}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
@ -767,21 +780,26 @@ class _PackDetailsPageState extends State<PackDetailsPage> {
|
||||||
// Иконка избранного
|
// Иконка избранного
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () async => await _toggleCardFavorite(card.id),
|
onTap: () async => await _toggleCardFavorite(card.id),
|
||||||
child: Container(
|
child: Builder(
|
||||||
padding: const EdgeInsets.all(4),
|
builder: (context) {
|
||||||
decoration: BoxDecoration(
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
color: Colors.white.withValues(alpha: 0.9),
|
return Container(
|
||||||
borderRadius: BorderRadius.circular(4),
|
padding: const EdgeInsets.all(4),
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
child: Icon(
|
color: colorScheme.surface.withValues(alpha: 0.9),
|
||||||
_isCardFavorite(card.id)
|
borderRadius: BorderRadius.circular(4),
|
||||||
? Icons.favorite
|
),
|
||||||
: Icons.favorite_border,
|
child: Icon(
|
||||||
size: 16,
|
_isCardFavorite(card.id)
|
||||||
color: _isCardFavorite(card.id)
|
? Icons.favorite
|
||||||
? Colors.red
|
: Icons.favorite_border,
|
||||||
: Colors.grey,
|
size: 16,
|
||||||
),
|
color: _isCardFavorite(card.id)
|
||||||
|
? Colors.red
|
||||||
|
: colorScheme.onSurface.withOpacity(0.6),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
@ -796,17 +814,22 @@ class _PackDetailsPageState extends State<PackDetailsPage> {
|
||||||
name: 'PackDetailsPage',
|
name: 'PackDetailsPage',
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Builder(
|
||||||
padding: const EdgeInsets.all(4),
|
builder: (context) {
|
||||||
decoration: BoxDecoration(
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
color: Colors.white.withValues(alpha: 0.9),
|
return Container(
|
||||||
borderRadius: BorderRadius.circular(4),
|
padding: const EdgeInsets.all(4),
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
child: const Icon(
|
color: colorScheme.surface.withValues(alpha: 0.9),
|
||||||
Icons.visibility_outlined,
|
borderRadius: BorderRadius.circular(4),
|
||||||
size: 16,
|
),
|
||||||
color: Colors.grey,
|
child: Icon(
|
||||||
),
|
Icons.visibility_outlined,
|
||||||
|
size: 16,
|
||||||
|
color: colorScheme.onSurface.withOpacity(0.6),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -202,9 +202,9 @@ class _ProfilePageState extends State<ProfilePage> {
|
||||||
? user.email![0]
|
? user.email![0]
|
||||||
: 'U')
|
: 'U')
|
||||||
.toUpperCase(),
|
.toUpperCase(),
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
color: Colors.white,
|
color: theme.colorScheme.onPrimary,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -57,20 +57,33 @@ class _StatisticsPageState extends State<StatisticsPage> with TickerProviderStat
|
||||||
foregroundColor: theme.colorScheme.onSurface,
|
foregroundColor: theme.colorScheme.onSurface,
|
||||||
bottom: PreferredSize(
|
bottom: PreferredSize(
|
||||||
preferredSize: const Size.fromHeight(48),
|
preferredSize: const Size.fromHeight(48),
|
||||||
child: Container(
|
child: Container(
|
||||||
color: theme.colorScheme.surface,
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surface,
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: theme.dividerColor,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
child: TabBar(
|
child: TabBar(
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
isScrollable: true,
|
isScrollable: true,
|
||||||
tabAlignment: TabAlignment.start,
|
tabAlignment: TabAlignment.start,
|
||||||
labelColor: theme.colorScheme.primary,
|
labelColor: theme.colorScheme.onSurface,
|
||||||
unselectedLabelColor: theme.colorScheme.onSurfaceVariant,
|
unselectedLabelColor: theme.colorScheme.onSurfaceVariant,
|
||||||
indicatorColor: theme.colorScheme.primary,
|
indicatorColor: theme.colorScheme.primary,
|
||||||
indicatorWeight: 3,
|
indicatorWeight: 3,
|
||||||
|
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
||||||
|
dividerColor: Colors.transparent,
|
||||||
labelStyle: theme.textTheme.labelLarge?.copyWith(
|
labelStyle: theme.textTheme.labelLarge?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
unselectedLabelStyle: theme.textTheme.labelLarge?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
unselectedLabelStyle: theme.textTheme.labelLarge,
|
|
||||||
tabs: const [
|
tabs: const [
|
||||||
Tab(
|
Tab(
|
||||||
icon: Icon(Icons.dashboard_outlined),
|
icon: Icon(Icons.dashboard_outlined),
|
||||||
|
|
|
||||||
|
|
@ -90,15 +90,32 @@ class _TasksPageState extends State<TasksPage> with TickerProviderStateMixin {
|
||||||
),
|
),
|
||||||
|
|
||||||
// Tab bar
|
// Tab bar
|
||||||
TabBar(
|
Builder(
|
||||||
controller: _tabController,
|
builder: (context) {
|
||||||
tabs: const [
|
final theme = Theme.of(context);
|
||||||
Tab(text: 'Все'),
|
return TabBar(
|
||||||
Tab(text: 'Доступные'),
|
controller: _tabController,
|
||||||
Tab(text: 'Выполненные'),
|
labelColor: theme.colorScheme.onSurface,
|
||||||
],
|
unselectedLabelColor: theme.colorScheme.onSurfaceVariant,
|
||||||
onTap: (index) {
|
indicatorColor: theme.colorScheme.primary,
|
||||||
// Handle tab change if needed
|
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
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -381,7 +381,17 @@ class _TestPageState extends State<TestPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: isSelected
|
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,
|
: null,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,15 @@ class AppTheme {
|
||||||
foregroundColor: AppColors.black,
|
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, скругленные углы
|
// Card тема - минимальная elevation, скругленные углы
|
||||||
cardTheme: CardThemeData(
|
cardTheme: CardThemeData(
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
|
|
@ -111,6 +120,10 @@ class AppTheme {
|
||||||
// TabBar тема для темной темы
|
// TabBar тема для темной темы
|
||||||
tabBarTheme: const TabBarThemeData(
|
tabBarTheme: const TabBarThemeData(
|
||||||
labelColor: AppColors.white,
|
labelColor: AppColors.white,
|
||||||
|
unselectedLabelColor: Colors.grey,
|
||||||
|
indicatorColor: AppColors.white,
|
||||||
|
labelStyle: TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
unselectedLabelStyle: TextStyle(fontWeight: FontWeight.w700),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Card тема
|
// Card тема
|
||||||
|
|
|
||||||
|
|
@ -677,16 +677,19 @@ class _CardSide extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
border: Border.all(color: packColor, width: 2),
|
border: Border.all(color: packColor, width: 2),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.1),
|
color: colorScheme.shadow.withOpacity(0.1),
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
offset: const Offset(0, 5),
|
offset: const Offset(0, 5),
|
||||||
),
|
),
|
||||||
|
|
@ -783,15 +786,20 @@ class _CardSide extends StatelessWidget {
|
||||||
|
|
||||||
if (card.translation != null && card.translation!.isNotEmpty) ...[
|
if (card.translation != null && card.translation!.isNotEmpty) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
MnemoText(
|
Builder(
|
||||||
card.translation,
|
builder: (context) {
|
||||||
textStyle: const TextStyle(
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
fontSize: 18,
|
return MnemoText(
|
||||||
fontWeight: FontWeight.w400,
|
card.translation,
|
||||||
color: Colors.black54,
|
textStyle: TextStyle(
|
||||||
),
|
fontSize: 18,
|
||||||
textAlign: TextAlign.center,
|
fontWeight: FontWeight.w400,
|
||||||
maxLines: 2,
|
color: colorScheme.onSurface.withOpacity(0.6),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 2,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
@ -818,14 +826,19 @@ class _CardSide extends StatelessWidget {
|
||||||
maxLines: 4,
|
maxLines: 4,
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Text(
|
Builder(
|
||||||
'Нет мнемоники',
|
builder: (context) {
|
||||||
style: TextStyle(
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
fontSize: 18,
|
return Text(
|
||||||
fontWeight: FontWeight.w400,
|
'Нет мнемоники',
|
||||||
color: Colors.black38,
|
style: TextStyle(
|
||||||
),
|
fontSize: 18,
|
||||||
textAlign: TextAlign.center,
|
fontWeight: FontWeight.w400,
|
||||||
|
color: colorScheme.onSurface.withOpacity(0.38),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -60,9 +60,11 @@ class _CardViewerState extends State<CardViewer> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final packColor = widget.packColor ?? AppColors.borderGray;
|
final packColor = widget.packColor ?? AppColors.borderGray;
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: theme.scaffoldBackgroundColor,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -109,12 +111,12 @@ class _CardViewerState extends State<CardViewer> {
|
||||||
icon: Container(
|
icon: Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black.withOpacity(0.5),
|
color: colorScheme.surface.withOpacity(0.7),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: Icon(
|
||||||
Icons.close,
|
Icons.close,
|
||||||
color: Colors.white,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -130,13 +132,13 @@ class _CardViewerState extends State<CardViewer> {
|
||||||
vertical: 8,
|
vertical: 8,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black.withOpacity(0.5),
|
color: colorScheme.surface.withOpacity(0.7),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'${_currentIndex + 1} / ${widget.cards.length}',
|
'${_currentIndex + 1} / ${widget.cards.length}',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: colorScheme.onSurface,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
|
|
@ -230,12 +232,14 @@ class _CardSide extends StatelessWidget {
|
||||||
final screenSize = MediaQuery.of(context).size;
|
final screenSize = MediaQuery.of(context).size;
|
||||||
final cardWidth = math.min(screenSize.width * 0.9, 600.0);
|
final cardWidth = math.min(screenSize.width * 0.9, 600.0);
|
||||||
final cardHeight = math.min(screenSize.height * 0.7, 800.0);
|
final cardHeight = math.min(screenSize.height * 0.7, 800.0);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: cardWidth,
|
width: cardWidth,
|
||||||
height: cardHeight,
|
height: cardHeight,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: packColor,
|
color: packColor,
|
||||||
|
|
@ -243,7 +247,7 @@ class _CardSide extends StatelessWidget {
|
||||||
),
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.3),
|
color: colorScheme.shadow.withOpacity(0.3),
|
||||||
blurRadius: 20,
|
blurRadius: 20,
|
||||||
offset: const Offset(0, 10),
|
offset: const Offset(0, 10),
|
||||||
),
|
),
|
||||||
|
|
@ -345,15 +349,20 @@ class _CardSide extends StatelessWidget {
|
||||||
|
|
||||||
if (card.translation != null && card.translation!.isNotEmpty) ...[
|
if (card.translation != null && card.translation!.isNotEmpty) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
MnemoText(
|
Builder(
|
||||||
card.translation,
|
builder: (context) {
|
||||||
textStyle: const TextStyle(
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
fontSize: 24,
|
return MnemoText(
|
||||||
fontWeight: FontWeight.w400,
|
card.translation,
|
||||||
color: Colors.black54,
|
textStyle: TextStyle(
|
||||||
),
|
fontSize: 24,
|
||||||
textAlign: TextAlign.center,
|
fontWeight: FontWeight.w400,
|
||||||
maxLines: 3,
|
color: colorScheme.onSurface.withOpacity(0.6),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 3,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
@ -380,14 +389,19 @@ class _CardSide extends StatelessWidget {
|
||||||
maxLines: 5,
|
maxLines: 5,
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Text(
|
Builder(
|
||||||
'Нет мнемоники',
|
builder: (context) {
|
||||||
style: TextStyle(
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
fontSize: 24,
|
return Text(
|
||||||
fontWeight: FontWeight.w400,
|
'Нет мнемоники',
|
||||||
color: Colors.black38,
|
style: TextStyle(
|
||||||
),
|
fontSize: 24,
|
||||||
textAlign: TextAlign.center,
|
fontWeight: FontWeight.w400,
|
||||||
|
color: colorScheme.onSurface.withOpacity(0.38),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,17 @@ class AnswerOptions extends StatelessWidget {
|
||||||
child:
|
child:
|
||||||
(isSelected ||
|
(isSelected ||
|
||||||
(isAnswerSubmitted && isCorrectOption))
|
(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,
|
: null,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ class GameCardShimmer extends StatelessWidget {
|
||||||
AspectRatio(
|
AspectRatio(
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Colors.white,
|
color: isDark ? Colors.grey[800]! : Colors.grey[200]!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Text placeholders
|
// Text placeholders
|
||||||
|
|
@ -34,7 +34,7 @@ class GameCardShimmer extends StatelessWidget {
|
||||||
height: 16,
|
height: 16,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: isDark ? Colors.grey[800]! : Colors.grey[200]!,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -43,7 +43,7 @@ class GameCardShimmer extends StatelessWidget {
|
||||||
height: 12,
|
height: 12,
|
||||||
width: 80,
|
width: 80,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: isDark ? Colors.grey[800]! : Colors.grey[200]!,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ class PackCardShimmer extends StatelessWidget {
|
||||||
height: cardHeight - 2,
|
height: cardHeight - 2,
|
||||||
margin: const EdgeInsets.all(1),
|
margin: const EdgeInsets.all(1),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: isDark ? Colors.grey[800]! : Colors.grey[200]!,
|
||||||
borderRadius: BorderRadius.circular(11),
|
borderRadius: BorderRadius.circular(11),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -54,7 +54,7 @@ class PackCardShimmer extends StatelessWidget {
|
||||||
height: 24,
|
height: 24,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: isDark ? Colors.grey[800]! : Colors.grey[200]!,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -64,7 +64,7 @@ class PackCardShimmer extends StatelessWidget {
|
||||||
height: 16,
|
height: 16,
|
||||||
width: 150,
|
width: 150,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: isDark ? Colors.grey[800]! : Colors.grey[200]!,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -74,7 +74,7 @@ class PackCardShimmer extends StatelessWidget {
|
||||||
height: 14,
|
height: 14,
|
||||||
width: 60,
|
width: 60,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: isDark ? Colors.grey[800]! : Colors.grey[200]!,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ class PackCardVerticalShimmer extends StatelessWidget {
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.all(1),
|
margin: const EdgeInsets.all(1),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: isDark ? Colors.grey[800]! : Colors.grey[200]!,
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
topLeft: Radius.circular(11),
|
topLeft: Radius.circular(11),
|
||||||
topRight: Radius.circular(11),
|
topRight: Radius.circular(11),
|
||||||
|
|
@ -54,7 +54,7 @@ class PackCardVerticalShimmer extends StatelessWidget {
|
||||||
height: 20,
|
height: 20,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: isDark ? Colors.grey[800]! : Colors.grey[200]!,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -64,7 +64,7 @@ class PackCardVerticalShimmer extends StatelessWidget {
|
||||||
height: 14,
|
height: 14,
|
||||||
width: double.infinity * 0.7,
|
width: double.infinity * 0.7,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: isDark ? Colors.grey[800]! : Colors.grey[200]!,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -74,7 +74,7 @@ class PackCardVerticalShimmer extends StatelessWidget {
|
||||||
height: 14,
|
height: 14,
|
||||||
width: 50,
|
width: 50,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: isDark ? Colors.grey[800]! : Colors.grey[200]!,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,6 @@ class PackCard extends StatelessWidget {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4.0),
|
|
||||||
height: cardHeight,
|
height: cardHeight,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
|
|
|
||||||
|
|
@ -144,43 +144,52 @@ import 'mnemo_text.dart';
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 4,
|
bottom: 4,
|
||||||
right: 4,
|
right: 4,
|
||||||
child: Row(
|
child: Builder(
|
||||||
mainAxisSize: MainAxisSize.min,
|
builder: (context) {
|
||||||
children: [
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
// Иконка избранного
|
return Row(
|
||||||
GestureDetector(
|
mainAxisSize: MainAxisSize.min,
|
||||||
onTap: onToggleFavorite,
|
children: [
|
||||||
child: Container(
|
// Иконка избранного
|
||||||
padding: const EdgeInsets.all(2),
|
GestureDetector(
|
||||||
decoration: BoxDecoration(
|
onTap: onToggleFavorite,
|
||||||
color: Colors.white.withOpacity(0.9),
|
child: Container(
|
||||||
borderRadius: BorderRadius.circular(4),
|
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(
|
const SizedBox(width: 4),
|
||||||
isFavorite ? Icons.favorite : Icons.favorite_border,
|
// Иконка просмотра
|
||||||
size: 12,
|
GestureDetector(
|
||||||
color: isFavorite ? Colors.red : Colors.grey,
|
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -158,10 +158,7 @@ class PackCardVertical extends StatelessWidget {
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: tip.bgColor?.asColor ?? packColor.withOpacity(0.4),
|
color: tip.bgColor?.asColor ?? packColor.withOpacity(0.4),
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||||
bottomLeft: Radius.circular(12.0),
|
|
||||||
bottomRight: Radius.circular(12.0),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: tip.build(context),
|
child: tip.build(context),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue