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",
|
||||
"cwd": "mnemo_cards_web",
|
||||
"cwd": "mnemo_cards_web_v2",
|
||||
"request": "launch",
|
||||
"type": "dart"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 (_) {
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 тема
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ class PackCard extends StatelessWidget {
|
|||
}
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
height: cardHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in a new issue