From f6d68fb1fc57cec25b0dd9ff2f7ea6a3232db085 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 17 Dec 2025 22:54:48 +0300 Subject: [PATCH] stuff --- mnemo_cards_admin/src/api/cards.ts | 2 +- mnemo_cards_admin/src/api/voices.ts | 2 +- .../src/components/PackTestsManager.tsx | 214 +++++ mnemo_cards_admin/src/pages/PacksPage.tsx | 49 +- mnemo_cards_admin/src/pages/TestsPage.tsx | 17 + mnemo_cards_admin/src/types/models.ts | 8 + .../lib/api/v2/admin_cards_api_v2.dart | 825 +++++++++--------- .../lib/api/v2/admin_packs_api_v2.dart | 55 +- .../lib/api/v2/admin_tests_api_v2.dart | 31 +- .../lib/api/v2/packs_api_v2.dart | 6 - .../lib/database/daos/test_dao.dart | 15 + .../lib/mnemo_cards_common.dart | 12 + .../lib/src/dtos/admin/add_voice_request.dart | 25 + .../src/dtos/admin/add_voice_request.g.dart | 83 ++ .../src/dtos/admin/admin_voice_response.dart | 28 + .../dtos/admin/admin_voice_response.g.dart | 126 +++ .../src/dtos/admin/create_card_request.dart | 58 ++ .../src/dtos/admin/create_card_request.g.dart | 194 ++++ .../src/dtos/admin/create_card_response.dart | 27 + .../dtos/admin/create_card_response.g.dart | 96 ++ .../lib/src/dtos/admin/success_response.dart | 22 + .../src/dtos/admin/success_response.g.dart | 80 ++ .../src/dtos/admin/voice_list_response.dart | 21 + .../src/dtos/admin/voice_list_response.g.dart | 70 ++ .../lib/src/dtos/common/error_response.dart | 26 + .../lib/src/dtos/common/error_response.g.dart | 112 +++ .../src/dtos/common/paginated_response.dart | 32 + .../src/dtos/common/paginated_response.g.dart | 133 +++ .../widgets/card_flipper/card_flipper.dart | 14 - 29 files changed, 1944 insertions(+), 439 deletions(-) create mode 100644 mnemo_cards_admin/src/components/PackTestsManager.tsx create mode 100644 mnemo_cards_common/lib/src/dtos/admin/add_voice_request.dart create mode 100644 mnemo_cards_common/lib/src/dtos/admin/add_voice_request.g.dart create mode 100644 mnemo_cards_common/lib/src/dtos/admin/admin_voice_response.dart create mode 100644 mnemo_cards_common/lib/src/dtos/admin/admin_voice_response.g.dart create mode 100644 mnemo_cards_common/lib/src/dtos/admin/create_card_request.dart create mode 100644 mnemo_cards_common/lib/src/dtos/admin/create_card_request.g.dart create mode 100644 mnemo_cards_common/lib/src/dtos/admin/create_card_response.dart create mode 100644 mnemo_cards_common/lib/src/dtos/admin/create_card_response.g.dart create mode 100644 mnemo_cards_common/lib/src/dtos/admin/success_response.dart create mode 100644 mnemo_cards_common/lib/src/dtos/admin/success_response.g.dart create mode 100644 mnemo_cards_common/lib/src/dtos/admin/voice_list_response.dart create mode 100644 mnemo_cards_common/lib/src/dtos/admin/voice_list_response.g.dart create mode 100644 mnemo_cards_common/lib/src/dtos/common/error_response.dart create mode 100644 mnemo_cards_common/lib/src/dtos/common/error_response.g.dart create mode 100644 mnemo_cards_common/lib/src/dtos/common/paginated_response.dart create mode 100644 mnemo_cards_common/lib/src/dtos/common/paginated_response.g.dart diff --git a/mnemo_cards_admin/src/api/cards.ts b/mnemo_cards_admin/src/api/cards.ts index cf993eb..d4898e0 100644 --- a/mnemo_cards_admin/src/api/cards.ts +++ b/mnemo_cards_admin/src/api/cards.ts @@ -112,7 +112,7 @@ export const cardsApi = { }, // Delete a card by ID - deleteCard: async (cardId: string): Promise<{ success: boolean; message: string }> => { + deleteCard: async (cardId: string): Promise<{ success: boolean; message?: string }> => { try { const response = await adminApiClient.delete(`/api/v2/admin/cards/${cardId}`) return response.data diff --git a/mnemo_cards_admin/src/api/voices.ts b/mnemo_cards_admin/src/api/voices.ts index e215331..e78e8a5 100644 --- a/mnemo_cards_admin/src/api/voices.ts +++ b/mnemo_cards_admin/src/api/voices.ts @@ -36,7 +36,7 @@ export const voicesApi = { removeCardVoice: async ( cardId: string, voiceId: string - ): Promise<{ success: boolean }> => { + ): Promise<{ success: boolean; message?: string }> => { const response = await adminApiClient.delete( `/api/v2/admin/cards/${cardId}/voices/${voiceId}` ) diff --git a/mnemo_cards_admin/src/components/PackTestsManager.tsx b/mnemo_cards_admin/src/components/PackTestsManager.tsx new file mode 100644 index 0000000..8ed5bff --- /dev/null +++ b/mnemo_cards_admin/src/components/PackTestsManager.tsx @@ -0,0 +1,214 @@ +import { useState, useEffect } from 'react' +import { useQuery } from '@tanstack/react-query' +import { testsApi } from '@/api/tests' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { Badge } from '@/components/ui/badge' +import { Search, Check } from 'lucide-react' + +interface PackTestsManagerProps { + currentTestIds: string[] + onTestsChange: (addIds: string[], removeIds: string[]) => void + disabled?: boolean +} + +export function PackTestsManager({ + currentTestIds, + onTestsChange, + disabled = false, +}: PackTestsManagerProps) { + const [search, setSearch] = useState('') + const [selectedTests, setSelectedTests] = useState>(new Set()) + const [removedTests, setRemovedTests] = useState>(new Set()) + + // Load all tests with search + const { data: testsData, isLoading } = useQuery({ + queryKey: ['tests', 1, 100, search], // Limit to 100 (backend max) + queryFn: () => testsApi.getTests({ page: 1, limit: 100, search }), + enabled: !disabled, + }) + + // Initialize selected tests from currentTestIds when it changes + useEffect(() => { + const initialSelected = new Set( + currentTestIds.filter((id) => !removedTests.has(id.toString())) + ) + setSelectedTests(initialSelected) + // Reset removed tests when currentTestIds changes (e.g., when opening dialog) + setRemovedTests(new Set()) + }, [currentTestIds.join(',')]) // Use join to detect array changes + + // Calculate which tests to add/remove when selection changes + useEffect(() => { + const currentlySelected = Array.from(selectedTests) + const removed = Array.from(removedTests) + + // Tests to add: selected but not in currentTestIds and not removed + const toAdd = currentlySelected.filter( + (id) => !currentTestIds.includes(id) && !removed.includes(id) + ) + // Tests to remove: in removedTests + const toRemove = removed.filter((id) => currentTestIds.includes(id)) + + if (toAdd.length > 0 || toRemove.length > 0) { + onTestsChange(toAdd, toRemove) + } else if (selectedTests.size > 0 || removedTests.size > 0) { + // Also notify if selection was cleared + onTestsChange([], []) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedTests.size, removedTests.size, currentTestIds.join(',')]) + + const handleToggleTest = (testId: string) => { + if (disabled || !testId) return + + const testIdStr = String(testId) + const isCurrentlySelected = selectedTests.has(testIdStr) && !removedTests.has(testIdStr) + const isInCurrentPack = currentTestIds.includes(testIdStr) + + if (isCurrentlySelected) { + // Deselect test + const newSelected = new Set(selectedTests) + newSelected.delete(testIdStr) + setSelectedTests(newSelected) + + // If it was in current pack, mark as removed + if (isInCurrentPack) { + setRemovedTests((prev) => new Set([...prev, testIdStr])) + } + } else { + // Select test + const newSelected = new Set(selectedTests) + newSelected.add(testIdStr) + setSelectedTests(newSelected) + + // If it was marked as removed, unmark it + if (removedTests.has(testIdStr)) { + setRemovedTests((prev) => { + const newRemoved = new Set(prev) + newRemoved.delete(testIdStr) + return newRemoved + }) + } + } + } + + const isTestSelected = (testId: string | number) => { + const testIdStr = String(testId) + return selectedTests.has(testIdStr) && !removedTests.has(testIdStr) + } + + const isTestInCurrentPack = (testId: string | number) => { + return currentTestIds.includes(String(testId)) + } + + const allTests = testsData?.items || [] + const filteredTests = search + ? allTests.filter( + (test) => + test.name?.toLowerCase().includes(search.toLowerCase()) || + test.id?.toLowerCase().includes(search.toLowerCase()) + ) + : allTests + + return ( +
+
+ +
+ + setSearch(e.target.value)} + className="max-w-sm" + disabled={disabled} + /> +
+

+ Selected tests: {selectedTests.size - removedTests.size} / {allTests.length} +

+
+ + {isLoading ? ( +
Loading tests...
+ ) : ( +
+ + + + + ID + Name + Questions + Version + Status + + + + {filteredTests.length === 0 ? ( + + + No tests found + + + ) : ( + filteredTests + .filter((test) => test.id) // Only show tests with IDs + .map((test) => { + const testIdStr = String(test.id!) + const selected = isTestSelected(testIdStr) + const inCurrentPack = isTestInCurrentPack(testIdStr) + const newlyRemoved = removedTests.has(testIdStr) + + return ( + handleToggleTest(testIdStr)} + > + + {selected ? ( + + ) : ( +
+ )} + + {test.id || '-'} + {test.name} + + {typeof test.questions === 'number' + ? test.questions + : test.questions?.length || 0} + + {test.version || 'N/A'} + + {newlyRemoved ? ( + Removed + ) : selected && inCurrentPack ? ( + In Pack + ) : selected ? ( + Selected + ) : inCurrentPack ? ( + In Pack + ) : null} + + + ) + }) + )} + +
+
+ )} +
+ ) +} diff --git a/mnemo_cards_admin/src/pages/PacksPage.tsx b/mnemo_cards_admin/src/pages/PacksPage.tsx index e7c2ea6..0cb0c28 100644 --- a/mnemo_cards_admin/src/pages/PacksPage.tsx +++ b/mnemo_cards_admin/src/pages/PacksPage.tsx @@ -38,6 +38,7 @@ import { Textarea } from '@/components/ui/textarea' import { Checkbox } from '@/components/ui/checkbox' import { ImageUpload } from '@/components/ui/image-upload' import { PackCardsManager } from '@/components/PackCardsManager' +import { PackTestsManager } from '@/components/PackTestsManager' import { Plus, Search, Edit, Trash2, ChevronLeft, ChevronRight } from 'lucide-react' export default function PacksPage() { @@ -72,6 +73,11 @@ export default function PacksPage() { const [cardsToAdd, setCardsToAdd] = useState([]) const [cardsToRemove, setCardsToRemove] = useState([]) + // Test management state + const [currentTestIds, setCurrentTestIds] = useState([]) + const [testsToAdd, setTestsToAdd] = useState([]) + const [testsToRemove, setTestsToRemove] = useState([]) + const limit = 20 // Fetch packs @@ -157,6 +163,9 @@ export default function PacksPage() { setCurrentCardIds([]) setCardsToAdd([]) setCardsToRemove([]) + setCurrentTestIds([]) + setTestsToAdd([]) + setTestsToRemove([]) setIsDialogOpen(true) } @@ -184,6 +193,13 @@ export default function PacksPage() { setCurrentCardIds(cardIds) setCardsToAdd([]) setCardsToRemove([]) + + // Initialize current test IDs from addTestIds (which contains all tests in pack) + const testIds = fullPack.addTestIds?.map((id) => id.toString()) || [] + setCurrentTestIds(testIds) + setTestsToAdd([]) + setTestsToRemove([]) + setIsDialogOpen(true) } catch (error) { const errorMessage = isPacksApiError(error) @@ -200,6 +216,9 @@ export default function PacksPage() { setCurrentCardIds([]) setCardsToAdd([]) setCardsToRemove([]) + setCurrentTestIds([]) + setTestsToAdd([]) + setTestsToRemove([]) } const handleCardsChange = (addIds: string[], removeIds: string[]) => { @@ -207,6 +226,11 @@ export default function PacksPage() { setCardsToRemove(removeIds) } + const handleTestsChange = (addIds: string[], removeIds: string[]) => { + setTestsToAdd(addIds) + setTestsToRemove(removeIds) + } + const handleSubmit = (e: React.FormEvent) => { e.preventDefault() @@ -233,6 +257,8 @@ export default function PacksPage() { cover: formData.cover || undefined, addCardIds: cardsToAdd.length > 0 ? cardsToAdd : undefined, removeCardIds: cardsToRemove.length > 0 ? cardsToRemove : undefined, + addTestIds: testsToAdd.length > 0 ? testsToAdd : undefined, + removeTestIds: testsToRemove.length > 0 ? testsToRemove : undefined, } if (selectedPack) { @@ -589,13 +615,22 @@ export default function PacksPage() { {selectedPack && ( -
- -
+ <> +
+ +
+
+ +
+ )} diff --git a/mnemo_cards_admin/src/pages/TestsPage.tsx b/mnemo_cards_admin/src/pages/TestsPage.tsx index c668011..0d6524b 100644 --- a/mnemo_cards_admin/src/pages/TestsPage.tsx +++ b/mnemo_cards_admin/src/pages/TestsPage.tsx @@ -312,6 +312,7 @@ export default function TestsPage() { ID Name + Packs Questions Version Time @@ -323,6 +324,22 @@ export default function TestsPage() { {test.id || 'N/A'} {test.name} + + {test.packs && test.packs.length > 0 ? ( +
+ {test.packs.map((pack) => ( + + {pack.title} + + ))} +
+ ) : ( + No packs + )} +
{typeof test.questions === 'number' ? test.questions diff --git a/mnemo_cards_admin/src/types/models.ts b/mnemo_cards_admin/src/types/models.ts index f16f5b5..beb9971 100644 --- a/mnemo_cards_admin/src/types/models.ts +++ b/mnemo_cards_admin/src/types/models.ts @@ -11,6 +11,8 @@ export interface GameCardDto { transcriptionMnemo?: string imageBack?: string back?: string + createdAt?: string + updatedAt?: string } export interface EditCardPackDto { @@ -173,6 +175,11 @@ import type { Question } from './questions' export type TestQuestion = Question +export interface TestPackInfo { + id: string + title: string +} + export interface TestDto { id?: string name: string @@ -183,4 +190,5 @@ export interface TestDto { timeSubtitle?: string questions: Question[] statistics?: unknown + packs?: TestPackInfo[] } diff --git a/mnemo_cards_backend/lib/api/v2/admin_cards_api_v2.dart b/mnemo_cards_backend/lib/api/v2/admin_cards_api_v2.dart index fd436b5..9cf5b5f 100644 --- a/mnemo_cards_backend/lib/api/v2/admin_cards_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/admin_cards_api_v2.dart @@ -10,6 +10,9 @@ import 'package:mnemo_cards_backend/api/authorize/access_service.dart'; import 'package:mnemo_cards_backend/api/authorize/helpers.dart'; import 'package:injectable/injectable.dart'; import 'package:drift/drift.dart' as drift; +import 'package:mnemo_cards_common/mnemo_cards_common.dart'; +import 'package:mnemo_cards_backend/api/v2/extensions/game_card_extensions.dart'; +import 'package:mnemo_cards_backend/api/v2/extensions/voice_extensions.dart'; part 'admin_cards_api_v2.g.dart'; @@ -29,6 +32,21 @@ class AdminCardsApiV2 { } } + Response _json( + Object? data, { + int statusCode = 200, + Map headers = const {}, + }) { + return Response( + statusCode, + body: data == null ? null : jsonEncode(data), + headers: { + 'Content-Type': 'application/json', + ...headers, + }, + ); + } + // Helper function to check if string is base64 encoded bool _isBase64(String value) { if (value.isEmpty) return false; @@ -121,14 +139,14 @@ class AdminCardsApiV2 { // Validate pagination if (page < 1) { - return Response.badRequest( - body: json.encode({ - 'error': 'Invalid page parameter', - 'message': 'Page must be greater than 0. Received: $page', - 'field': 'page', - 'details': 'Page numbers start from 1. Please provide a valid page number.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Invalid page parameter', + message: 'Page must be greater than 0. Received: $page', + field: 'page', + details: 'Page numbers start from 1. Please provide a valid page number.', + ).toJson(), + statusCode: 400, ); } // Limit validation: if limit is invalid, set to default; if > 100, cap at 100 @@ -155,44 +173,38 @@ class AdminCardsApiV2 { final offset = (page - 1) * validatedLimit; final paginatedCards = filteredCards.skip(offset).take(validatedLimit).toList(); - // Получить паки для всех карточек (для packId) - final cardsWithPacks = >[]; + // Получить паки для всех карточек (для packId) и конвертировать в DTO + final items = >[]; for (final card in paginatedCards) { final packs = await _db.packDao.getPacksForCard(card.id); final packId = packs.isNotEmpty ? packs.first.id : null; - cardsWithPacks.add({ - 'id': card.id, - 'packId': packId, - 'original': card.original, - 'translation': card.translation, - 'mnemo': card.mnemo, - 'image': _convertImageToUrl(card.image, packId, card.id), - 'back': card.back, - 'transcription': card.transcription, - 'transcriptionMnemo': card.transcriptionMnemo, - 'imageBack': _convertImageBackToUrl(card.imageBack, packId, card.id), - }); + final cardDto = card.toGameCardDtoWithPack( + packId, + _convertImageToUrl, + _convertImageBackToUrl, + ); + // Добавляем packId к JSON карточки + final cardJson = cardDto.toJson(); + cardJson['packId'] = packId; + items.add(cardJson); } - return Response.ok( - json.encode({ - 'items': cardsWithPacks, - 'total': total, - 'page': page, - 'limit': validatedLimit, - 'totalPages': totalPages, - }), - headers: {'Content-Type': 'application/json'}, - ); + return _json({ + 'items': items, + 'total': total, + 'page': page, + 'limit': validatedLimit, + 'totalPages': totalPages, + }); } catch (e, s) { print('Error in getAllCards: $e\n$s'); - return Response.internalServerError( - body: json.encode({ - 'error': 'Internal server error', - 'message': 'Failed to retrieve cards', - 'details': 'An unexpected error occurred while fetching cards. Please try again later or contact support if the problem persists.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Internal server error', + message: 'Failed to retrieve cards', + details: 'An unexpected error occurred while fetching cards. Please try again later or contact support if the problem persists.', + ).toJson(), + statusCode: 500, ); } } @@ -203,26 +215,26 @@ class AdminCardsApiV2 { Future getCard(Request request, String cardId) async { try { if (cardId.isEmpty) { - return Response.badRequest( - body: json.encode({ - 'error': 'Invalid card ID', - 'message': 'Card ID cannot be empty', - 'field': 'cardId', - 'details': 'Please provide a valid card ID to retrieve card details.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Invalid card ID', + message: 'Card ID cannot be empty', + field: 'cardId', + details: 'Please provide a valid card ID to retrieve card details.', + ).toJson(), + statusCode: 400, ); } final card = await _db.packDao.getCardById(cardId); if (card == null) { - return Response.notFound( - json.encode({ - 'error': 'Card not found', - 'message': 'The requested card does not exist or has been deleted', - 'details': 'Card with ID "$cardId" was not found in the database. Please verify the card ID and try again.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Card not found', + message: 'The requested card does not exist or has been deleted', + details: 'Card with ID "$cardId" was not found in the database. Please verify the card ID and try again.', + ).toJson(), + statusCode: 404, ); } @@ -230,32 +242,29 @@ class AdminCardsApiV2 { final packs = await _db.packDao.getPacksForCard(card.id); final packId = packs.isNotEmpty ? packs.first.id : null; - return Response.ok( - json.encode({ - 'id': card.id, - 'packId': packId, - 'original': card.original, - 'translation': card.translation, - 'mnemo': card.mnemo, - 'image': _convertImageToUrl(card.image, packId, card.id), - 'imageBack': _convertImageBackToUrl(card.imageBack, packId, card.id), - 'back': card.back, - 'transcription': card.transcription, - 'transcriptionMnemo': card.transcriptionMnemo, - 'createdAt': card.createdAt.dateTime.toIso8601String(), - 'updatedAt': card.updatedAt.dateTime.toIso8601String(), - }), - headers: {'Content-Type': 'application/json'}, + // Конвертировать в DTO + final cardDto = card.toGameCardDtoWithPack( + packId, + _convertImageToUrl, + _convertImageBackToUrl, ); + + // Добавить packId и даты + final cardJson = cardDto.toJson(); + cardJson['packId'] = packId; + cardJson['createdAt'] = card.createdAt.dateTime.toIso8601String(); + cardJson['updatedAt'] = card.updatedAt.dateTime.toIso8601String(); + + return _json(cardJson); } catch (e, s) { print('Error in getCard: $e\n$s'); - return Response.internalServerError( - body: json.encode({ - 'error': 'Internal server error', - 'message': 'Failed to retrieve card', - 'details': 'An unexpected error occurred while fetching card with ID "$cardId". Please try again later or contact support if the problem persists.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Internal server error', + message: 'Failed to retrieve card', + details: 'An unexpected error occurred while fetching card with ID "$cardId". Please try again later or contact support if the problem persists.', + ).toJson(), + statusCode: 500, ); } } @@ -266,123 +275,136 @@ class AdminCardsApiV2 { Future createCard(Request request) async { try { final body = await request.readAsString(); - final data = json.decode(body) as Map; + final requestDto = CreateCardRequest.fromJson( + json.decode(body) as Map, + ); - // Validate required fields for new cards - final cardIdParam = data['id']; - // Check if this is an update: id must be present, not null, not empty string, and not -1 - // Handle both null and string 'null' cases - final isUpdate = cardIdParam != null - && cardIdParam.toString().trim().isNotEmpty; + // Check if this is an update: id must be present, not null, not empty string + final isUpdate = requestDto.id != null && requestDto.id!.trim().isNotEmpty; if (!isUpdate) { // Validate required fields for new cards - if (data['original'] == null || (data['original'] as String).trim().isEmpty) { - return Response.badRequest( - body: json.encode({ - 'error': 'Validation error', - 'message': 'Original text is required', - 'field': 'original', - 'details': 'Please provide the original word or phrase for the card.', - }), - headers: {'Content-Type': 'application/json'}, + if (requestDto.original == null || requestDto.original!.trim().isEmpty) { + return _json( + ErrorResponse( + error: 'Validation error', + message: 'Original text is required', + field: 'original', + details: 'Please provide the original word or phrase for the card.', + ).toJson(), + statusCode: 400, ); } - if (data['translation'] == null || (data['translation'] as String).trim().isEmpty) { - return Response.badRequest( - body: json.encode({ - 'error': 'Validation error', - 'message': 'Translation is required', - 'field': 'translation', - 'details': 'Please provide the translation for the card.', - }), - headers: {'Content-Type': 'application/json'}, + if (requestDto.translation == null || requestDto.translation!.trim().isEmpty) { + return _json( + ErrorResponse( + error: 'Validation error', + message: 'Translation is required', + field: 'translation', + details: 'Please provide the translation for the card.', + ).toJson(), + statusCode: 400, ); } } // Check if this is an update (has valid id) if (isUpdate) { - final cardId = cardIdParam.toString(); + final cardId = requestDto.id!; final existing = await _db.packDao.getCardById(cardId); if (existing == null) { - return Response.notFound( - json.encode({ - 'error': 'Card not found', - 'message': 'The card you are trying to update does not exist', - 'details': 'Card with ID "$cardId" was not found. The card may have been deleted or the ID may be incorrect.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Card not found', + message: 'The card you are trying to update does not exist', + details: 'Card with ID "$cardId" was not found. The card may have been deleted or the ID may be incorrect.', + ).toJson(), + statusCode: 404, ); } // Update existing card final updated = existing.copyWith( - original: data['original'] ?? existing.original, - translation: data['translation'] ?? existing.translation, - mnemo: data['mnemo'] ?? existing.mnemo, - image: data['image'] ?? existing.image, - imageBack: data['imageBack'] ?? existing.imageBack, - back: data['back'] ?? existing.back, - transcription: data['transcription'] ?? existing.transcription, - transcriptionMnemo: data['transcriptionMnemo'] ?? existing.transcriptionMnemo, + original: requestDto.original ?? existing.original, + translation: requestDto.translation ?? existing.translation, + mnemo: requestDto.mnemo != null + ? drift.Value(requestDto.mnemo) + : const drift.Value.absent(), + image: requestDto.image ?? existing.image, + imageBack: requestDto.imageBack != null + ? drift.Value(requestDto.imageBack) + : const drift.Value.absent(), + back: requestDto.back != null + ? drift.Value(requestDto.back) + : const drift.Value.absent(), + transcription: requestDto.transcription != null + ? drift.Value(requestDto.transcription) + : const drift.Value.absent(), + transcriptionMnemo: requestDto.transcriptionMnemo != null + ? drift.Value(requestDto.transcriptionMnemo) + : const drift.Value.absent(), updatedAt: PgDateTime(DateTime.now()), ); await _db.packDao.updateCard(updated); + // Получить паки для карточки final packs = await _db.packDao.getPacksForCard(updated.id); final packId = packs.isNotEmpty ? packs.first.id : null; - return Response.ok( - json.encode({ - 'success': true, - 'card': { - 'id': updated.id, - 'packId': packId, - 'original': updated.original, - 'translation': updated.translation, - 'mnemo': updated.mnemo, - 'image': _convertImageToUrl(updated.image, packId, updated.id), - 'imageBack': _convertImageBackToUrl(updated.imageBack, packId, updated.id), - 'back': updated.back, - 'transcription': updated.transcription, - 'transcriptionMnemo': updated.transcriptionMnemo, - }, - }), - headers: {'Content-Type': 'application/json'}, + // Конвертировать в DTO + final cardDto = updated.toGameCardDtoWithPack( + packId, + _convertImageToUrl, + _convertImageBackToUrl, + ); + + return _json( + CreateCardResponse( + success: true, + card: cardDto, + packId: packId, + ).toJson(), ); } // Create new card (packId больше нет в GameCards) final companion = GameCardsCompanion.insert( - original: data['original'] as String, - translation: data['translation'] as String, - image: data['image'] as String? ?? '', - mnemo: data['mnemo'] != null ? drift.Value(data['mnemo'] as String) : const drift.Value.absent(), - imageBack: data['imageBack'] != null ? drift.Value(data['imageBack'] as String) : const drift.Value.absent(), - back: data['back'] != null ? drift.Value(data['back'] as String) : const drift.Value.absent(), - transcription: data['transcription'] != null ? drift.Value(data['transcription'] as String) : const drift.Value.absent(), - transcriptionMnemo: data['transcriptionMnemo'] != null ? drift.Value(data['transcriptionMnemo'] as String) : const drift.Value.absent(), + original: requestDto.original!, + translation: requestDto.translation!, + image: requestDto.image ?? '', + mnemo: requestDto.mnemo != null + ? drift.Value(requestDto.mnemo) + : const drift.Value.absent(), + imageBack: requestDto.imageBack != null + ? drift.Value(requestDto.imageBack) + : const drift.Value.absent(), + back: requestDto.back != null + ? drift.Value(requestDto.back) + : const drift.Value.absent(), + transcription: requestDto.transcription != null + ? drift.Value(requestDto.transcription) + : const drift.Value.absent(), + transcriptionMnemo: requestDto.transcriptionMnemo != null + ? drift.Value(requestDto.transcriptionMnemo) + : const drift.Value.absent(), ); final cardId = await _db.packDao.createCard(companion); // Если передан packId, создать связь через CardPackCards - if (data['packId'] != null) { - final packId = data['packId'] as String; - await _db.packDao.addCardToPack(cardId: cardId, packId: packId); + if (requestDto.packId != null) { + await _db.packDao.addCardToPack(cardId: cardId, packId: requestDto.packId!); } final created = await _db.packDao.getCardById(cardId); if (created == null) { - return Response.internalServerError( - body: json.encode({ - 'error': 'Database error', - 'message': 'Failed to retrieve created card', - 'details': 'Card was created successfully but could not be retrieved from the database. The card ID is: $cardId', - 'success': false, - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Database error', + message: 'Failed to retrieve created card', + details: 'Card was created successfully but could not be retrieved from the database. The card ID is: $cardId', + ).toJson(), + statusCode: 500, ); } @@ -390,23 +412,19 @@ class AdminCardsApiV2 { final packs = await _db.packDao.getPacksForCard(cardId); final packId = packs.isNotEmpty ? packs.first.id : null; - return Response.ok( - json.encode({ - 'success': true, - 'card': { - 'id': created.id, - 'packId': packId, - 'original': created.original, - 'translation': created.translation, - 'mnemo': created.mnemo, - 'image': _convertImageToUrl(created.image, packId, created.id), - 'imageBack': _convertImageBackToUrl(created.imageBack, packId, created.id), - 'back': created.back, - 'transcription': created.transcription, - 'transcriptionMnemo': created.transcriptionMnemo, - }, - }), - headers: {'Content-Type': 'application/json'}, + // Конвертировать в DTO + final cardDto = created.toGameCardDtoWithPack( + packId, + _convertImageToUrl, + _convertImageBackToUrl, + ); + + return _json( + CreateCardResponse( + success: true, + card: cardDto, + packId: packId, + ).toJson(), ); } catch (e, s) { print('Error in createCard: $e\n$s'); @@ -423,14 +441,13 @@ class AdminCardsApiV2 { details = 'Required fields are missing or invalid. Please check all required fields are provided.'; } - return Response.internalServerError( - body: json.encode({ - 'error': 'Internal server error', - 'message': 'Failed to save card', - 'details': details, - 'success': false, - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Internal server error', + message: 'Failed to save card', + details: details, + ).toJson(), + statusCode: 500, ); } } @@ -441,60 +458,68 @@ class AdminCardsApiV2 { Future updateCard(Request request, String cardId) async { try { if (cardId.isEmpty) { - return Response.badRequest( - body: json.encode({ - 'error': 'Invalid card ID', - 'message': 'Card ID cannot be empty', - 'field': 'cardId', - 'details': 'Please provide a valid card ID to update the card.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Invalid card ID', + message: 'Card ID cannot be empty', + field: 'cardId', + details: 'Please provide a valid card ID to update the card.', + ).toJson(), + statusCode: 400, ); } final body = await request.readAsString(); - final data = json.decode(body) as Map; + final requestDto = CreateCardRequest.fromJson( + json.decode(body) as Map, + ); final existing = await _db.packDao.getCardById(cardId); if (existing == null) { - return Response.notFound( - json.encode({ - 'error': 'Card not found', - 'message': 'The card you are trying to update does not exist', - 'details': 'Card with ID "$cardId" was not found. The card may have been deleted or the ID may be incorrect.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Card not found', + message: 'The card you are trying to update does not exist', + details: 'Card with ID "$cardId" was not found. The card may have been deleted or the ID may be incorrect.', + ).toJson(), + statusCode: 404, ); } final updated = existing.copyWith( - original: data['original'] ?? existing.original, - translation: data['translation'] ?? existing.translation, - mnemo: data['mnemo'] ?? existing.mnemo, - image: data['image'] ?? existing.image, - imageBack: data['imageBack'] ?? existing.imageBack, - back: data['back'] ?? existing.back, - transcription: data['transcription'] ?? existing.transcription, - transcriptionMnemo: data['transcriptionMnemo'] ?? existing.transcriptionMnemo, + original: requestDto.original ?? existing.original, + translation: requestDto.translation ?? existing.translation, + mnemo: requestDto.mnemo != null + ? drift.Value(requestDto.mnemo) + : const drift.Value.absent(), + image: requestDto.image ?? existing.image, + imageBack: requestDto.imageBack != null + ? drift.Value(requestDto.imageBack) + : const drift.Value.absent(), + back: requestDto.back != null + ? drift.Value(requestDto.back) + : const drift.Value.absent(), + transcription: requestDto.transcription != null + ? drift.Value(requestDto.transcription) + : const drift.Value.absent(), + transcriptionMnemo: requestDto.transcriptionMnemo != null + ? drift.Value(requestDto.transcriptionMnemo) + : const drift.Value.absent(), updatedAt: PgDateTime(DateTime.now()), ); await _db.packDao.updateCard(updated); - return Response.ok( - json.encode({'success': true}), - headers: {'Content-Type': 'application/json'}, - ); + return _json(SuccessResponse(success: true).toJson()); } catch (e, s) { print('Error in updateCard: $e\n$s'); - return Response.internalServerError( - body: json.encode({ - 'error': 'Internal server error', - 'message': 'Failed to update card', - 'details': 'An unexpected error occurred while updating the card. Please try again later.', - 'success': false, - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Internal server error', + message: 'Failed to update card', + details: 'An unexpected error occurred while updating the card. Please try again later.', + ).toJson(), + statusCode: 500, ); } } @@ -510,38 +535,37 @@ class AdminCardsApiV2 { } if (cardId.isEmpty) { - return Response.badRequest( - body: json.encode({ - 'error': 'Invalid card ID', - 'message': 'Card ID cannot be empty', - 'field': 'cardId', - 'details': 'Please provide a valid card ID to delete the card.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Invalid card ID', + message: 'Card ID cannot be empty', + field: 'cardId', + details: 'Please provide a valid card ID to delete the card.', + ).toJson(), + statusCode: 400, ); } // Check if card exists before attempting deletion final existing = await _db.packDao.getCardById(cardId); if (existing == null) { - return Response.notFound( - json.encode({ - 'error': 'Card not found', - 'message': 'The card you are trying to delete does not exist', - 'details': 'Card with ID "$cardId" was not found. It may have already been deleted.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Card not found', + message: 'The card you are trying to delete does not exist', + details: 'Card with ID "$cardId" was not found. It may have already been deleted.', + ).toJson(), + statusCode: 404, ); } await _db.packDao.deleteCard(cardId); - return Response.ok( - json.encode({ - 'success': true, - 'message': 'Card deleted successfully', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + SuccessResponse( + success: true, + message: 'Card deleted successfully', + ).toJson(), ); } catch (e, s) { print('Error in deleteCard: $e\n$s'); @@ -553,14 +577,13 @@ class AdminCardsApiV2 { details = 'Cannot delete card: it is still referenced by one or more packs. Please remove the card from all packs first.'; } - return Response.internalServerError( - body: json.encode({ - 'error': 'Internal server error', - 'message': 'Failed to delete card', - 'details': details, - 'success': false, - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Internal server error', + message: 'Failed to delete card', + details: details, + ).toJson(), + statusCode: 500, ); } } @@ -576,53 +599,43 @@ class AdminCardsApiV2 { } if (cardId.isEmpty) { - return Response.badRequest( - body: json.encode({ - 'error': 'Invalid card ID', - 'message': 'Card ID cannot be empty', - 'field': 'cardId', - 'details': 'Please provide a valid card ID to retrieve voices.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Invalid card ID', + message: 'Card ID cannot be empty', + field: 'cardId', + details: 'Please provide a valid card ID to retrieve voices.', + ).toJson(), + statusCode: 400, ); } // Verify card exists final card = await _db.packDao.getCardById(cardId); if (card == null) { - return Response.notFound( - json.encode({ - 'error': 'Card not found', - 'message': 'The requested card does not exist', - 'details': 'Card with ID "$cardId" was not found. Cannot retrieve voices for a non-existent card.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Card not found', + message: 'The requested card does not exist', + details: 'Card with ID "$cardId" was not found. Cannot retrieve voices for a non-existent card.', + ).toJson(), + statusCode: 404, ); } final voices = await _db.packDao.getCardVoices(cardId); + final voicesDto = voices.map((voice) => voice.toAdminVoiceResponse()).toList(); - return Response.ok( - json.encode({ - 'items': voices.map((voice) => { - 'id': voice.id, - 'cardId': voice.cardId, - 'voiceUrl': voice.voiceUrl, - 'language': voice.language, - 'createdAt': voice.createdAt.dateTime.toIso8601String(), - }).toList(), - }), - headers: {'Content-Type': 'application/json'}, - ); + return _json(VoiceListResponse(items: voicesDto).toJson()); } catch (e, s) { print('Error in getCardVoices: $e\n$s'); - return Response.internalServerError( - body: json.encode({ - 'error': 'Internal server error', - 'message': 'Failed to retrieve card voices', - 'details': 'An unexpected error occurred while fetching voices for card "$cardId". Please try again later.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Internal server error', + message: 'Failed to retrieve card voices', + details: 'An unexpected error occurred while fetching voices for card "$cardId". Please try again later.', + ).toJson(), + statusCode: 500, ); } } @@ -638,112 +651,111 @@ class AdminCardsApiV2 { } if (cardId.isEmpty) { - return Response.badRequest( - body: json.encode({ - 'error': 'Invalid card ID', - 'message': 'Card ID cannot be empty', - 'field': 'cardId', - 'details': 'Please provide a valid card ID to add a voice.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Invalid card ID', + message: 'Card ID cannot be empty', + field: 'cardId', + details: 'Please provide a valid card ID to add a voice.', + ).toJson(), + statusCode: 400, ); } final body = await request.readAsString(); - final data = json.decode(body) as Map; + final requestDto = AddVoiceRequest.fromJson( + json.decode(body) as Map, + ); - final voiceUrl = data['voiceUrl'] as String?; - final language = data['language'] as String? ?? 'en'; - - if (voiceUrl == null || voiceUrl.isEmpty) { - return Response.badRequest( - body: json.encode({ - 'error': 'Validation error', - 'message': 'Voice URL is required', - 'field': 'voiceUrl', - 'details': 'Please provide a valid voice URL (base64 encoded audio data) to add a voice to the card.', - }), - headers: {'Content-Type': 'application/json'}, + if (requestDto.voiceUrl.isEmpty) { + return _json( + ErrorResponse( + error: 'Validation error', + message: 'Voice URL is required', + field: 'voiceUrl', + details: 'Please provide a valid voice URL (base64 encoded audio data) to add a voice to the card.', + ).toJson(), + statusCode: 400, ); } // Validate base64 format (basic check) try { // Remove data URL prefix if present (data:audio/...;base64,) - final base64String = voiceUrl.contains(',') - ? voiceUrl.split(',').last - : voiceUrl; + final base64String = requestDto.voiceUrl.contains(',') + ? requestDto.voiceUrl.split(',').last + : requestDto.voiceUrl; // Basic base64 validation - check if it's valid base64 if (base64String.isEmpty) { - return Response.badRequest( - body: json.encode({ - 'error': 'Validation error', - 'message': 'Invalid base64 audio data', - 'field': 'voiceUrl', - 'details': 'The provided voice URL does not contain valid base64 encoded audio data.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Validation error', + message: 'Invalid base64 audio data', + field: 'voiceUrl', + details: 'The provided voice URL does not contain valid base64 encoded audio data.', + ).toJson(), + statusCode: 400, ); } // Check base64 characters (basic validation) final base64Regex = RegExp(r'^[A-Za-z0-9+/]*={0,2}$'); if (!base64Regex.hasMatch(base64String)) { - return Response.badRequest( - body: json.encode({ - 'error': 'Validation error', - 'message': 'Invalid base64 format', - 'field': 'voiceUrl', - 'details': 'The provided voice URL does not appear to be valid base64 encoded data.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Validation error', + message: 'Invalid base64 format', + field: 'voiceUrl', + details: 'The provided voice URL does not appear to be valid base64 encoded data.', + ).toJson(), + statusCode: 400, ); } // Check size (base64 is ~33% larger than original, so 10MB audio = ~13.3MB base64) // Limit to ~15MB base64 string (roughly 11MB audio) if (base64String.length > 15 * 1024 * 1024) { - return Response.badRequest( - body: json.encode({ - 'error': 'Validation error', - 'message': 'Audio file too large', - 'field': 'voiceUrl', - 'details': 'The audio file is too large. Maximum size is approximately 10MB for the original audio file.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Validation error', + message: 'Audio file too large', + field: 'voiceUrl', + details: 'The audio file is too large. Maximum size is approximately 10MB for the original audio file.', + ).toJson(), + statusCode: 400, ); } } catch (e) { - return Response.badRequest( - body: json.encode({ - 'error': 'Validation error', - 'message': 'Invalid voice URL format', - 'field': 'voiceUrl', - 'details': 'Failed to validate the voice URL. Please ensure it is a valid base64 encoded audio file.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Validation error', + message: 'Invalid voice URL format', + field: 'voiceUrl', + details: 'Failed to validate the voice URL. Please ensure it is a valid base64 encoded audio file.', + ).toJson(), + statusCode: 400, ); } // Verify card exists final card = await _db.packDao.getCardById(cardId); if (card == null) { - return Response.notFound( - json.encode({ - 'error': 'Card not found', - 'message': 'The card you are trying to add a voice to does not exist', - 'details': 'Card with ID "$cardId" was not found. Cannot add voice to a non-existent card.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Card not found', + message: 'The card you are trying to add a voice to does not exist', + details: 'Card with ID "$cardId" was not found. Cannot add voice to a non-existent card.', + ).toJson(), + statusCode: 404, ); } // Create voice model final voiceCompanion = VoiceModelsCompanion.insert( cardId: cardId, - voiceUrl: voiceUrl, - language: language, + voiceUrl: requestDto.voiceUrl, + language: requestDto.language ?? 'en', ); final voiceId = await _db.packDao.createVoice(voiceCompanion); @@ -753,19 +765,21 @@ class AdminCardsApiV2 { final voice = await _db.packDao.getVoiceById(voiceId); - return Response.ok( - json.encode({ - 'success': true, - 'voice': voice != null ? { - 'id': voice.id, - 'cardId': voice.cardId, - 'voiceUrl': voice.voiceUrl, - 'language': voice.language, - 'createdAt': voice.createdAt.dateTime.toIso8601String(), - } : null, - }), - headers: {'Content-Type': 'application/json'}, - ); + if (voice == null) { + return _json( + ErrorResponse( + error: 'Database error', + message: 'Failed to retrieve created voice', + details: 'Voice was created successfully but could not be retrieved from the database. The voice ID is: $voiceId', + ).toJson(), + statusCode: 500, + ); + } + + return _json({ + 'success': true, + 'voice': voice.toAdminVoiceResponse().toJson(), + }); } catch (e, s) { print('Error in addCardVoice: $e\n$s'); @@ -776,14 +790,13 @@ class AdminCardsApiV2 { details = 'A voice with this URL may already exist for this card.'; } - return Response.internalServerError( - body: json.encode({ - 'error': 'Internal server error', - 'message': 'Failed to add voice to card', - 'details': details, - 'success': false, - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Internal server error', + message: 'Failed to add voice to card', + details: details, + ).toJson(), + statusCode: 500, ); } } @@ -799,40 +812,40 @@ class AdminCardsApiV2 { } if (cardId.isEmpty || voiceId.isEmpty) { - return Response.badRequest( - body: json.encode({ - 'error': 'Invalid parameters', - 'message': 'Card ID and Voice ID cannot be empty', - 'field': cardId.isEmpty ? 'cardId' : 'voiceId', - 'details': 'Please provide valid card ID and voice ID to remove the voice.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Invalid parameters', + message: 'Card ID and Voice ID cannot be empty', + field: cardId.isEmpty ? 'cardId' : 'voiceId', + details: 'Please provide valid card ID and voice ID to remove the voice.', + ).toJson(), + statusCode: 400, ); } // Verify card exists final card = await _db.packDao.getCardById(cardId); if (card == null) { - return Response.notFound( - json.encode({ - 'error': 'Card not found', - 'message': 'The card you are trying to remove a voice from does not exist', - 'details': 'Card with ID "$cardId" was not found. Cannot remove voice from a non-existent card.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Card not found', + message: 'The card you are trying to remove a voice from does not exist', + details: 'Card with ID "$cardId" was not found. Cannot remove voice from a non-existent card.', + ).toJson(), + statusCode: 404, ); } // Verify voice exists final voice = await _db.packDao.getVoiceById(voiceId); if (voice == null) { - return Response.notFound( - json.encode({ - 'error': 'Voice not found', - 'message': 'The voice you are trying to remove does not exist', - 'details': 'Voice with ID "$voiceId" was not found. It may have already been deleted.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Voice not found', + message: 'The voice you are trying to remove does not exist', + details: 'Voice with ID "$voiceId" was not found. It may have already been deleted.', + ).toJson(), + statusCode: 404, ); } @@ -840,13 +853,13 @@ class AdminCardsApiV2 { final cardVoices = await _db.packDao.getCardVoices(cardId); final voiceBelongsToCard = cardVoices.any((v) => v.id == voiceId); if (!voiceBelongsToCard) { - return Response.badRequest( - body: json.encode({ - 'error': 'Voice not associated with card', - 'message': 'The voice is not associated with this card', - 'details': 'Voice with ID "$voiceId" is not linked to card "$cardId". Cannot remove a voice that is not associated with this card.', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Voice not associated with card', + message: 'The voice is not associated with this card', + details: 'Voice with ID "$voiceId" is not linked to card "$cardId". Cannot remove a voice that is not associated with this card.', + ).toJson(), + statusCode: 400, ); } @@ -856,23 +869,21 @@ class AdminCardsApiV2 { // Delete voice model (cascade will handle CardVoices relations) await _db.packDao.deleteVoice(voiceId); - return Response.ok( - json.encode({ - 'success': true, - 'message': 'Voice removed successfully', - }), - headers: {'Content-Type': 'application/json'}, + return _json( + SuccessResponse( + success: true, + message: 'Voice removed successfully', + ).toJson(), ); } catch (e, s) { print('Error in removeCardVoice: $e\n$s'); - return Response.internalServerError( - body: json.encode({ - 'error': 'Internal server error', - 'message': 'Failed to remove voice from card', - 'details': 'An unexpected error occurred while removing the voice. Please try again later.', - 'success': false, - }), - headers: {'Content-Type': 'application/json'}, + return _json( + ErrorResponse( + error: 'Internal server error', + message: 'Failed to remove voice from card', + details: 'An unexpected error occurred while removing the voice. Please try again later.', + ).toJson(), + statusCode: 500, ); } } diff --git a/mnemo_cards_backend/lib/api/v2/admin_packs_api_v2.dart b/mnemo_cards_backend/lib/api/v2/admin_packs_api_v2.dart index 0f6b9c8..3ce3b7f 100644 --- a/mnemo_cards_backend/lib/api/v2/admin_packs_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/admin_packs_api_v2.dart @@ -182,6 +182,10 @@ class AdminPacksApiV2 { final previewCards = await _db.packDao.getPreviewCards(packId); final previewCardIds = previewCards.map((c) => c.id).toList(); + // Get tests for this pack + final packTests = await _db.testDao.getTestsByPackId(packId); + final testIds = packTests.map((t) => t.id).toList(); + // Create EditCardPackDto final editDto = EditCardPackDto( id: pack.id, @@ -199,7 +203,7 @@ class AdminPacksApiV2 { version: pack.version, order: pack.order, addCardIds: cards.map((c) => c.id).toList(), - addTestIds: null, + addTestIds: testIds, removeCardIds: null, removeTestIds: null, cardsOrder: cardsOrder, @@ -350,6 +354,49 @@ class AdminPacksApiV2 { } } + // Handle test associations if provided + if (editDto.addTestIds != null && editDto.addTestIds!.isNotEmpty) { + try { + for (final testId in editDto.addTestIds!) { + // Verify test exists before adding + final test = await _db.testDao.getTestById(testId); + if (test == null) { + return _json( + { + 'error': 'Test not found', + 'message': 'One of the tests you are trying to add does not exist', + 'field': 'addTestIds', + 'details': 'Test with ID "$testId" was not found. Please verify all test IDs before adding them to the pack.', + }, + statusCode: 404, + ); + } + await _db.testDao.linkTestToPack(testId, packId); + } + } catch (e) { + // Handle duplicate or constraint errors + if (e.toString().toLowerCase().contains('unique') || + e.toString().toLowerCase().contains('constraint')) { + return _json( + { + 'error': 'Duplicate test', + 'message': 'One or more tests are already in this pack', + 'field': 'addTestIds', + 'details': 'Some tests you are trying to add are already associated with this pack. Please remove duplicates and try again.', + }, + statusCode: 409, + ); + } + rethrow; + } + } + + if (editDto.removeTestIds != null && editDto.removeTestIds!.isNotEmpty) { + for (final testId in editDto.removeTestIds!) { + await _db.testDao.unlinkTestFromPack(testId, packId); + } + } + // Handle cards order if provided if (editDto.cardsOrder != null && editDto.cardsOrder!.isNotEmpty) { await _db.packDao.updatePackCardsOrder( @@ -379,6 +426,10 @@ class AdminPacksApiV2 { final cardsOrder = updatedCards.map((c) => c.id).toList(); final previewCards = await _db.packDao.getPreviewCards(packId); final previewCardIds = previewCards.map((c) => c.id).toList(); + + // Get updated tests for this pack + final updatedPackTests = await _db.testDao.getTestsByPackId(packId); + final updatedTestIds = updatedPackTests.map((t) => t.id).toList(); final updatedDto = EditCardPackDto( id: updatedPack.id, @@ -396,7 +447,7 @@ class AdminPacksApiV2 { version: updatedPack.version, order: updatedPack.order, addCardIds: updatedCards.map((c) => c.id).toList(), - addTestIds: null, + addTestIds: updatedTestIds, removeCardIds: null, removeTestIds: null, cardsOrder: cardsOrder, diff --git a/mnemo_cards_backend/lib/api/v2/admin_tests_api_v2.dart b/mnemo_cards_backend/lib/api/v2/admin_tests_api_v2.dart index ab3d80c..abb119c 100644 --- a/mnemo_cards_backend/lib/api/v2/admin_tests_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/admin_tests_api_v2.dart @@ -119,6 +119,20 @@ class AdminTestsApiV2 { final testDtos = >[]; for (final test in allTests) { final questions = await _db.testDao.getTestQuestions(test.id); + + // Get pack information for this test + final packIds = await _db.testDao.getPackIdsForTest(test.id); + final packs = >[]; + for (final packId in packIds) { + final pack = await _db.packDao.getPackById(packId); + if (pack != null) { + packs.add({ + 'id': pack.id, + 'title': pack.title, + }); + } + } + testDtos.add({ 'id': test.id, 'name': test.name, @@ -128,6 +142,7 @@ class AdminTestsApiV2 { 'time': test.time, 'timeSubtitle': test.timeSubtitle, 'questions': questions.length, + 'packs': packs, }); } @@ -201,8 +216,21 @@ class AdminTestsApiV2 { ); } - // Get packId for the test to build image URLs + // Get packId for the test to build image URLs (use first pack if multiple) final packId = await _db.testDao.getPackIdForTest(testId); + + // Get all pack information for this test + final packIds = await _db.testDao.getPackIdsForTest(testId); + final packs = >[]; + for (final packIdItem in packIds) { + final pack = await _db.packDao.getPackById(packIdItem); + if (pack != null) { + packs.add({ + 'id': pack.id, + 'title': pack.title, + }); + } + } // Get questions final questions = await _db.testDao.getTestQuestions(testId); @@ -319,6 +347,7 @@ class AdminTestsApiV2 { 'time': test.time, 'timeSubtitle': test.timeSubtitle, 'questions': questionsWithUrls, + 'packs': packs, }); } catch (e, s) { print('Error in getTest: $e\n$s'); diff --git a/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart b/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart index 86d1569..2180fae 100644 --- a/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart @@ -678,12 +678,6 @@ class PacksApiV2 { return _notFound('Pack not found'); } - // Get cards for pack - final cards = await _packManager.getCards(packId); - if (cards.isEmpty) { - return _notFound('Pack is empty'); - } - // Get tests for pack - fetchPackTests needs CardPackModel, but we can create a minimal one // or modify TestManager to work with Drift CardPack // For now, let's get tests directly from database diff --git a/mnemo_cards_backend/lib/database/daos/test_dao.dart b/mnemo_cards_backend/lib/database/daos/test_dao.dart index 9dbb184..1cd61f8 100644 --- a/mnemo_cards_backend/lib/database/daos/test_dao.dart +++ b/mnemo_cards_backend/lib/database/daos/test_dao.dart @@ -71,6 +71,13 @@ class TestDao extends DatabaseAccessor with _$TestDaoMixin { ); } + /// Удалить связь теста с паком + Future unlinkTestFromPack(String testId, String packId) async { + await (delete(testPackRelations) + ..where((tpr) => tpr.testId.equals(testId) & tpr.packId.equals(packId)) + ).go(); + } + /// Получить packId для теста Future getPackIdForTest(String testId) async { final relation = await (select(testPackRelations) @@ -79,6 +86,14 @@ class TestDao extends DatabaseAccessor with _$TestDaoMixin { ).getSingleOrNull(); return relation?.packId; } + + /// Получить все packId для теста + Future> getPackIdsForTest(String testId) async { + final relations = await (select(testPackRelations) + ..where((tpr) => tpr.testId.equals(testId)) + ).get(); + return relations.map((r) => r.packId).toList(); + } // ==================== TestQuestions ==================== diff --git a/mnemo_cards_common/lib/mnemo_cards_common.dart b/mnemo_cards_common/lib/mnemo_cards_common.dart index c033df8..c97ccfd 100644 --- a/mnemo_cards_common/lib/mnemo_cards_common.dart +++ b/mnemo_cards_common/lib/mnemo_cards_common.dart @@ -50,6 +50,18 @@ export 'src/dtos/game_tests/test_question_type.dart'; export 'src/dtos/items/items.dart'; +// Admin API models +export 'src/dtos/admin/create_card_request.dart'; +export 'src/dtos/admin/create_card_response.dart'; +export 'src/dtos/admin/add_voice_request.dart'; +export 'src/dtos/admin/admin_voice_response.dart'; +export 'src/dtos/admin/voice_list_response.dart'; +export 'src/dtos/admin/success_response.dart'; + +// Common models +export 'src/dtos/common/paginated_response.dart'; +export 'src/dtos/common/error_response.dart'; + export 'src/utils/utils.dart'; export 'src/utils/iterable_helper.dart'; export 'src/utils/token_generator.dart'; diff --git a/mnemo_cards_common/lib/src/dtos/admin/add_voice_request.dart b/mnemo_cards_common/lib/src/dtos/admin/add_voice_request.dart new file mode 100644 index 0000000..b73ca41 --- /dev/null +++ b/mnemo_cards_common/lib/src/dtos/admin/add_voice_request.dart @@ -0,0 +1,25 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:copy_with_extension/copy_with_extension.dart'; + +part 'add_voice_request.g.dart'; + +/// Запрос для добавления голоса к карточке +@JsonSerializable() +@CopyWith() +class AddVoiceRequest { + /// URL голоса (base64 encoded audio data) + final String voiceUrl; + + /// Язык голоса (по умолчанию 'en') + final String? language; + + const AddVoiceRequest({ + required this.voiceUrl, + this.language, + }); + + factory AddVoiceRequest.fromJson(Map json) => + _$AddVoiceRequestFromJson(json); + + Map toJson() => _$AddVoiceRequestToJson(this); +} diff --git a/mnemo_cards_common/lib/src/dtos/admin/add_voice_request.g.dart b/mnemo_cards_common/lib/src/dtos/admin/add_voice_request.g.dart new file mode 100644 index 0000000..3f8820f --- /dev/null +++ b/mnemo_cards_common/lib/src/dtos/admin/add_voice_request.g.dart @@ -0,0 +1,83 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'add_voice_request.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$AddVoiceRequestCWProxy { + AddVoiceRequest voiceUrl(String voiceUrl); + + AddVoiceRequest language(String? language); + + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `AddVoiceRequest(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// AddVoiceRequest(...).copyWith(id: 12, name: "My name") + /// ``` + AddVoiceRequest call({String voiceUrl, String? language}); +} + +/// Callable proxy for `copyWith` functionality. +/// Use as `instanceOfAddVoiceRequest.copyWith(...)` or call `instanceOfAddVoiceRequest.copyWith.fieldName(value)` for a single field. +class _$AddVoiceRequestCWProxyImpl implements _$AddVoiceRequestCWProxy { + const _$AddVoiceRequestCWProxyImpl(this._value); + + final AddVoiceRequest _value; + + @override + AddVoiceRequest voiceUrl(String voiceUrl) => call(voiceUrl: voiceUrl); + + @override + AddVoiceRequest language(String? language) => call(language: language); + + @override + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `AddVoiceRequest(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// AddVoiceRequest(...).copyWith(id: 12, name: "My name") + /// ``` + AddVoiceRequest call({ + Object? voiceUrl = const $CopyWithPlaceholder(), + Object? language = const $CopyWithPlaceholder(), + }) { + return AddVoiceRequest( + voiceUrl: voiceUrl == const $CopyWithPlaceholder() || voiceUrl == null + ? _value.voiceUrl + // ignore: cast_nullable_to_non_nullable + : voiceUrl as String, + language: language == const $CopyWithPlaceholder() + ? _value.language + // ignore: cast_nullable_to_non_nullable + : language as String?, + ); + } +} + +extension $AddVoiceRequestCopyWith on AddVoiceRequest { + /// Returns a callable class used to build a new instance with modified fields. + /// Example: `instanceOfAddVoiceRequest.copyWith(...)` or `instanceOfAddVoiceRequest.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$AddVoiceRequestCWProxy get copyWith => _$AddVoiceRequestCWProxyImpl(this); +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AddVoiceRequest _$AddVoiceRequestFromJson(Map json) => + AddVoiceRequest( + voiceUrl: json['voiceUrl'] as String, + language: json['language'] as String?, + ); + +Map _$AddVoiceRequestToJson(AddVoiceRequest instance) => + { + 'voiceUrl': instance.voiceUrl, + 'language': instance.language, + }; diff --git a/mnemo_cards_common/lib/src/dtos/admin/admin_voice_response.dart b/mnemo_cards_common/lib/src/dtos/admin/admin_voice_response.dart new file mode 100644 index 0000000..75933c1 --- /dev/null +++ b/mnemo_cards_common/lib/src/dtos/admin/admin_voice_response.dart @@ -0,0 +1,28 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:copy_with_extension/copy_with_extension.dart'; + +part 'admin_voice_response.g.dart'; + +/// Ответ с данными голоса для admin API +@JsonSerializable() +@CopyWith() +class AdminVoiceResponse { + final String id; + final String cardId; + final String voiceUrl; + final String language; + final String createdAt; + + const AdminVoiceResponse({ + required this.id, + required this.cardId, + required this.voiceUrl, + required this.language, + required this.createdAt, + }); + + factory AdminVoiceResponse.fromJson(Map json) => + _$AdminVoiceResponseFromJson(json); + + Map toJson() => _$AdminVoiceResponseToJson(this); +} diff --git a/mnemo_cards_common/lib/src/dtos/admin/admin_voice_response.g.dart b/mnemo_cards_common/lib/src/dtos/admin/admin_voice_response.g.dart new file mode 100644 index 0000000..e7b9650 --- /dev/null +++ b/mnemo_cards_common/lib/src/dtos/admin/admin_voice_response.g.dart @@ -0,0 +1,126 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'admin_voice_response.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$AdminVoiceResponseCWProxy { + AdminVoiceResponse id(String id); + + AdminVoiceResponse cardId(String cardId); + + AdminVoiceResponse voiceUrl(String voiceUrl); + + AdminVoiceResponse language(String language); + + AdminVoiceResponse createdAt(String createdAt); + + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `AdminVoiceResponse(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// AdminVoiceResponse(...).copyWith(id: 12, name: "My name") + /// ``` + AdminVoiceResponse call({ + String id, + String cardId, + String voiceUrl, + String language, + String createdAt, + }); +} + +/// Callable proxy for `copyWith` functionality. +/// Use as `instanceOfAdminVoiceResponse.copyWith(...)` or call `instanceOfAdminVoiceResponse.copyWith.fieldName(value)` for a single field. +class _$AdminVoiceResponseCWProxyImpl implements _$AdminVoiceResponseCWProxy { + const _$AdminVoiceResponseCWProxyImpl(this._value); + + final AdminVoiceResponse _value; + + @override + AdminVoiceResponse id(String id) => call(id: id); + + @override + AdminVoiceResponse cardId(String cardId) => call(cardId: cardId); + + @override + AdminVoiceResponse voiceUrl(String voiceUrl) => call(voiceUrl: voiceUrl); + + @override + AdminVoiceResponse language(String language) => call(language: language); + + @override + AdminVoiceResponse createdAt(String createdAt) => call(createdAt: createdAt); + + @override + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `AdminVoiceResponse(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// AdminVoiceResponse(...).copyWith(id: 12, name: "My name") + /// ``` + AdminVoiceResponse call({ + Object? id = const $CopyWithPlaceholder(), + Object? cardId = const $CopyWithPlaceholder(), + Object? voiceUrl = const $CopyWithPlaceholder(), + Object? language = const $CopyWithPlaceholder(), + Object? createdAt = const $CopyWithPlaceholder(), + }) { + return AdminVoiceResponse( + id: id == const $CopyWithPlaceholder() || id == null + ? _value.id + // ignore: cast_nullable_to_non_nullable + : id as String, + cardId: cardId == const $CopyWithPlaceholder() || cardId == null + ? _value.cardId + // ignore: cast_nullable_to_non_nullable + : cardId as String, + voiceUrl: voiceUrl == const $CopyWithPlaceholder() || voiceUrl == null + ? _value.voiceUrl + // ignore: cast_nullable_to_non_nullable + : voiceUrl as String, + language: language == const $CopyWithPlaceholder() || language == null + ? _value.language + // ignore: cast_nullable_to_non_nullable + : language as String, + createdAt: createdAt == const $CopyWithPlaceholder() || createdAt == null + ? _value.createdAt + // ignore: cast_nullable_to_non_nullable + : createdAt as String, + ); + } +} + +extension $AdminVoiceResponseCopyWith on AdminVoiceResponse { + /// Returns a callable class used to build a new instance with modified fields. + /// Example: `instanceOfAdminVoiceResponse.copyWith(...)` or `instanceOfAdminVoiceResponse.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$AdminVoiceResponseCWProxy get copyWith => + _$AdminVoiceResponseCWProxyImpl(this); +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AdminVoiceResponse _$AdminVoiceResponseFromJson(Map json) => + AdminVoiceResponse( + id: json['id'] as String, + cardId: json['cardId'] as String, + voiceUrl: json['voiceUrl'] as String, + language: json['language'] as String, + createdAt: json['createdAt'] as String, + ); + +Map _$AdminVoiceResponseToJson(AdminVoiceResponse instance) => + { + 'id': instance.id, + 'cardId': instance.cardId, + 'voiceUrl': instance.voiceUrl, + 'language': instance.language, + 'createdAt': instance.createdAt, + }; diff --git a/mnemo_cards_common/lib/src/dtos/admin/create_card_request.dart b/mnemo_cards_common/lib/src/dtos/admin/create_card_request.dart new file mode 100644 index 0000000..c886989 --- /dev/null +++ b/mnemo_cards_common/lib/src/dtos/admin/create_card_request.dart @@ -0,0 +1,58 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:copy_with_extension/copy_with_extension.dart'; + +part 'create_card_request.g.dart'; + +/// Запрос для создания или обновления карточки +/// Все поля optional для поддержки частичного обновления +@JsonSerializable() +@CopyWith() +class CreateCardRequest { + /// ID карточки. Если указан - это обновление существующей карточки + final String? id; + + /// ID пака для связи карточки с паком + final String? packId; + + /// Оригинальный текст (слово на иностранном языке) + final String? original; + + /// Перевод + final String? translation; + + /// Мнемоническая подсказка + final String? mnemo; + + /// Изображение (base64 или URL) + final String? image; + + /// Изображение на обратной стороне (base64 или URL) + final String? imageBack; + + /// Дополнительный текст на обратной стороне + final String? back; + + /// Транскрипция + final String? transcription; + + /// Транскрипция с мнемоникой + final String? transcriptionMnemo; + + const CreateCardRequest({ + this.id, + this.packId, + this.original, + this.translation, + this.mnemo, + this.image, + this.imageBack, + this.back, + this.transcription, + this.transcriptionMnemo, + }); + + factory CreateCardRequest.fromJson(Map json) => + _$CreateCardRequestFromJson(json); + + Map toJson() => _$CreateCardRequestToJson(this); +} diff --git a/mnemo_cards_common/lib/src/dtos/admin/create_card_request.g.dart b/mnemo_cards_common/lib/src/dtos/admin/create_card_request.g.dart new file mode 100644 index 0000000..2c46bc9 --- /dev/null +++ b/mnemo_cards_common/lib/src/dtos/admin/create_card_request.g.dart @@ -0,0 +1,194 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'create_card_request.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$CreateCardRequestCWProxy { + CreateCardRequest id(String? id); + + CreateCardRequest packId(String? packId); + + CreateCardRequest original(String? original); + + CreateCardRequest translation(String? translation); + + CreateCardRequest mnemo(String? mnemo); + + CreateCardRequest image(String? image); + + CreateCardRequest imageBack(String? imageBack); + + CreateCardRequest back(String? back); + + CreateCardRequest transcription(String? transcription); + + CreateCardRequest transcriptionMnemo(String? transcriptionMnemo); + + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `CreateCardRequest(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// CreateCardRequest(...).copyWith(id: 12, name: "My name") + /// ``` + CreateCardRequest call({ + String? id, + String? packId, + String? original, + String? translation, + String? mnemo, + String? image, + String? imageBack, + String? back, + String? transcription, + String? transcriptionMnemo, + }); +} + +/// Callable proxy for `copyWith` functionality. +/// Use as `instanceOfCreateCardRequest.copyWith(...)` or call `instanceOfCreateCardRequest.copyWith.fieldName(value)` for a single field. +class _$CreateCardRequestCWProxyImpl implements _$CreateCardRequestCWProxy { + const _$CreateCardRequestCWProxyImpl(this._value); + + final CreateCardRequest _value; + + @override + CreateCardRequest id(String? id) => call(id: id); + + @override + CreateCardRequest packId(String? packId) => call(packId: packId); + + @override + CreateCardRequest original(String? original) => call(original: original); + + @override + CreateCardRequest translation(String? translation) => + call(translation: translation); + + @override + CreateCardRequest mnemo(String? mnemo) => call(mnemo: mnemo); + + @override + CreateCardRequest image(String? image) => call(image: image); + + @override + CreateCardRequest imageBack(String? imageBack) => call(imageBack: imageBack); + + @override + CreateCardRequest back(String? back) => call(back: back); + + @override + CreateCardRequest transcription(String? transcription) => + call(transcription: transcription); + + @override + CreateCardRequest transcriptionMnemo(String? transcriptionMnemo) => + call(transcriptionMnemo: transcriptionMnemo); + + @override + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `CreateCardRequest(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// CreateCardRequest(...).copyWith(id: 12, name: "My name") + /// ``` + CreateCardRequest call({ + Object? id = const $CopyWithPlaceholder(), + Object? packId = const $CopyWithPlaceholder(), + Object? original = const $CopyWithPlaceholder(), + Object? translation = const $CopyWithPlaceholder(), + Object? mnemo = const $CopyWithPlaceholder(), + Object? image = const $CopyWithPlaceholder(), + Object? imageBack = const $CopyWithPlaceholder(), + Object? back = const $CopyWithPlaceholder(), + Object? transcription = const $CopyWithPlaceholder(), + Object? transcriptionMnemo = const $CopyWithPlaceholder(), + }) { + return CreateCardRequest( + id: id == const $CopyWithPlaceholder() + ? _value.id + // ignore: cast_nullable_to_non_nullable + : id as String?, + packId: packId == const $CopyWithPlaceholder() + ? _value.packId + // ignore: cast_nullable_to_non_nullable + : packId as String?, + original: original == const $CopyWithPlaceholder() + ? _value.original + // ignore: cast_nullable_to_non_nullable + : original as String?, + translation: translation == const $CopyWithPlaceholder() + ? _value.translation + // ignore: cast_nullable_to_non_nullable + : translation as String?, + mnemo: mnemo == const $CopyWithPlaceholder() + ? _value.mnemo + // ignore: cast_nullable_to_non_nullable + : mnemo as String?, + image: image == const $CopyWithPlaceholder() + ? _value.image + // ignore: cast_nullable_to_non_nullable + : image as String?, + imageBack: imageBack == const $CopyWithPlaceholder() + ? _value.imageBack + // ignore: cast_nullable_to_non_nullable + : imageBack as String?, + back: back == const $CopyWithPlaceholder() + ? _value.back + // ignore: cast_nullable_to_non_nullable + : back as String?, + transcription: transcription == const $CopyWithPlaceholder() + ? _value.transcription + // ignore: cast_nullable_to_non_nullable + : transcription as String?, + transcriptionMnemo: transcriptionMnemo == const $CopyWithPlaceholder() + ? _value.transcriptionMnemo + // ignore: cast_nullable_to_non_nullable + : transcriptionMnemo as String?, + ); + } +} + +extension $CreateCardRequestCopyWith on CreateCardRequest { + /// Returns a callable class used to build a new instance with modified fields. + /// Example: `instanceOfCreateCardRequest.copyWith(...)` or `instanceOfCreateCardRequest.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$CreateCardRequestCWProxy get copyWith => + _$CreateCardRequestCWProxyImpl(this); +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CreateCardRequest _$CreateCardRequestFromJson(Map json) => + CreateCardRequest( + id: json['id'] as String?, + packId: json['packId'] as String?, + original: json['original'] as String?, + translation: json['translation'] as String?, + mnemo: json['mnemo'] as String?, + image: json['image'] as String?, + imageBack: json['imageBack'] as String?, + back: json['back'] as String?, + transcription: json['transcription'] as String?, + transcriptionMnemo: json['transcriptionMnemo'] as String?, + ); + +Map _$CreateCardRequestToJson(CreateCardRequest instance) => + { + 'id': instance.id, + 'packId': instance.packId, + 'original': instance.original, + 'translation': instance.translation, + 'mnemo': instance.mnemo, + 'image': instance.image, + 'imageBack': instance.imageBack, + 'back': instance.back, + 'transcription': instance.transcription, + 'transcriptionMnemo': instance.transcriptionMnemo, + }; diff --git a/mnemo_cards_common/lib/src/dtos/admin/create_card_response.dart b/mnemo_cards_common/lib/src/dtos/admin/create_card_response.dart new file mode 100644 index 0000000..caf0704 --- /dev/null +++ b/mnemo_cards_common/lib/src/dtos/admin/create_card_response.dart @@ -0,0 +1,27 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:mnemo_cards_common/src/dtos/game_card_dto.dart'; + +part 'create_card_response.g.dart'; + +/// Ответ с созданной/обновленной карточкой +@JsonSerializable(explicitToJson: true) +@CopyWith() +class CreateCardResponse { + final bool success; + final GameCardDto card; + + /// ID пака (первый из связанных паков, если есть) + final String? packId; + + const CreateCardResponse({ + required this.success, + required this.card, + this.packId, + }); + + factory CreateCardResponse.fromJson(Map json) => + _$CreateCardResponseFromJson(json); + + Map toJson() => _$CreateCardResponseToJson(this); +} diff --git a/mnemo_cards_common/lib/src/dtos/admin/create_card_response.g.dart b/mnemo_cards_common/lib/src/dtos/admin/create_card_response.g.dart new file mode 100644 index 0000000..1bf6ced --- /dev/null +++ b/mnemo_cards_common/lib/src/dtos/admin/create_card_response.g.dart @@ -0,0 +1,96 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'create_card_response.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$CreateCardResponseCWProxy { + CreateCardResponse success(bool success); + + CreateCardResponse card(GameCardDto card); + + CreateCardResponse packId(String? packId); + + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `CreateCardResponse(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// CreateCardResponse(...).copyWith(id: 12, name: "My name") + /// ``` + CreateCardResponse call({bool success, GameCardDto card, String? packId}); +} + +/// Callable proxy for `copyWith` functionality. +/// Use as `instanceOfCreateCardResponse.copyWith(...)` or call `instanceOfCreateCardResponse.copyWith.fieldName(value)` for a single field. +class _$CreateCardResponseCWProxyImpl implements _$CreateCardResponseCWProxy { + const _$CreateCardResponseCWProxyImpl(this._value); + + final CreateCardResponse _value; + + @override + CreateCardResponse success(bool success) => call(success: success); + + @override + CreateCardResponse card(GameCardDto card) => call(card: card); + + @override + CreateCardResponse packId(String? packId) => call(packId: packId); + + @override + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `CreateCardResponse(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// CreateCardResponse(...).copyWith(id: 12, name: "My name") + /// ``` + CreateCardResponse call({ + Object? success = const $CopyWithPlaceholder(), + Object? card = const $CopyWithPlaceholder(), + Object? packId = const $CopyWithPlaceholder(), + }) { + return CreateCardResponse( + success: success == const $CopyWithPlaceholder() || success == null + ? _value.success + // ignore: cast_nullable_to_non_nullable + : success as bool, + card: card == const $CopyWithPlaceholder() || card == null + ? _value.card + // ignore: cast_nullable_to_non_nullable + : card as GameCardDto, + packId: packId == const $CopyWithPlaceholder() + ? _value.packId + // ignore: cast_nullable_to_non_nullable + : packId as String?, + ); + } +} + +extension $CreateCardResponseCopyWith on CreateCardResponse { + /// Returns a callable class used to build a new instance with modified fields. + /// Example: `instanceOfCreateCardResponse.copyWith(...)` or `instanceOfCreateCardResponse.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$CreateCardResponseCWProxy get copyWith => + _$CreateCardResponseCWProxyImpl(this); +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CreateCardResponse _$CreateCardResponseFromJson(Map json) => + CreateCardResponse( + success: json['success'] as bool, + card: GameCardDto.fromJson(json['card'] as Map), + packId: json['packId'] as String?, + ); + +Map _$CreateCardResponseToJson(CreateCardResponse instance) => + { + 'success': instance.success, + 'card': instance.card.toJson(), + 'packId': instance.packId, + }; diff --git a/mnemo_cards_common/lib/src/dtos/admin/success_response.dart b/mnemo_cards_common/lib/src/dtos/admin/success_response.dart new file mode 100644 index 0000000..ee7190b --- /dev/null +++ b/mnemo_cards_common/lib/src/dtos/admin/success_response.dart @@ -0,0 +1,22 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:copy_with_extension/copy_with_extension.dart'; + +part 'success_response.g.dart'; + +/// Простой успешный ответ +@JsonSerializable() +@CopyWith() +class SuccessResponse { + final bool success; + final String? message; + + const SuccessResponse({ + required this.success, + this.message, + }); + + factory SuccessResponse.fromJson(Map json) => + _$SuccessResponseFromJson(json); + + Map toJson() => _$SuccessResponseToJson(this); +} diff --git a/mnemo_cards_common/lib/src/dtos/admin/success_response.g.dart b/mnemo_cards_common/lib/src/dtos/admin/success_response.g.dart new file mode 100644 index 0000000..6d4b097 --- /dev/null +++ b/mnemo_cards_common/lib/src/dtos/admin/success_response.g.dart @@ -0,0 +1,80 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'success_response.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$SuccessResponseCWProxy { + SuccessResponse success(bool success); + + SuccessResponse message(String? message); + + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `SuccessResponse(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// SuccessResponse(...).copyWith(id: 12, name: "My name") + /// ``` + SuccessResponse call({bool success, String? message}); +} + +/// Callable proxy for `copyWith` functionality. +/// Use as `instanceOfSuccessResponse.copyWith(...)` or call `instanceOfSuccessResponse.copyWith.fieldName(value)` for a single field. +class _$SuccessResponseCWProxyImpl implements _$SuccessResponseCWProxy { + const _$SuccessResponseCWProxyImpl(this._value); + + final SuccessResponse _value; + + @override + SuccessResponse success(bool success) => call(success: success); + + @override + SuccessResponse message(String? message) => call(message: message); + + @override + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `SuccessResponse(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// SuccessResponse(...).copyWith(id: 12, name: "My name") + /// ``` + SuccessResponse call({ + Object? success = const $CopyWithPlaceholder(), + Object? message = const $CopyWithPlaceholder(), + }) { + return SuccessResponse( + success: success == const $CopyWithPlaceholder() || success == null + ? _value.success + // ignore: cast_nullable_to_non_nullable + : success as bool, + message: message == const $CopyWithPlaceholder() + ? _value.message + // ignore: cast_nullable_to_non_nullable + : message as String?, + ); + } +} + +extension $SuccessResponseCopyWith on SuccessResponse { + /// Returns a callable class used to build a new instance with modified fields. + /// Example: `instanceOfSuccessResponse.copyWith(...)` or `instanceOfSuccessResponse.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$SuccessResponseCWProxy get copyWith => _$SuccessResponseCWProxyImpl(this); +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SuccessResponse _$SuccessResponseFromJson(Map json) => + SuccessResponse( + success: json['success'] as bool, + message: json['message'] as String?, + ); + +Map _$SuccessResponseToJson(SuccessResponse instance) => + {'success': instance.success, 'message': instance.message}; diff --git a/mnemo_cards_common/lib/src/dtos/admin/voice_list_response.dart b/mnemo_cards_common/lib/src/dtos/admin/voice_list_response.dart new file mode 100644 index 0000000..c236437 --- /dev/null +++ b/mnemo_cards_common/lib/src/dtos/admin/voice_list_response.dart @@ -0,0 +1,21 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:mnemo_cards_common/src/dtos/admin/admin_voice_response.dart'; + +part 'voice_list_response.g.dart'; + +/// Ответ со списком голосов +@JsonSerializable(explicitToJson: true) +@CopyWith() +class VoiceListResponse { + final List items; + + const VoiceListResponse({ + required this.items, + }); + + factory VoiceListResponse.fromJson(Map json) => + _$VoiceListResponseFromJson(json); + + Map toJson() => _$VoiceListResponseToJson(this); +} diff --git a/mnemo_cards_common/lib/src/dtos/admin/voice_list_response.g.dart b/mnemo_cards_common/lib/src/dtos/admin/voice_list_response.g.dart new file mode 100644 index 0000000..c0a7332 --- /dev/null +++ b/mnemo_cards_common/lib/src/dtos/admin/voice_list_response.g.dart @@ -0,0 +1,70 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'voice_list_response.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$VoiceListResponseCWProxy { + VoiceListResponse items(List items); + + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `VoiceListResponse(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// VoiceListResponse(...).copyWith(id: 12, name: "My name") + /// ``` + VoiceListResponse call({List items}); +} + +/// Callable proxy for `copyWith` functionality. +/// Use as `instanceOfVoiceListResponse.copyWith(...)` or call `instanceOfVoiceListResponse.copyWith.fieldName(value)` for a single field. +class _$VoiceListResponseCWProxyImpl implements _$VoiceListResponseCWProxy { + const _$VoiceListResponseCWProxyImpl(this._value); + + final VoiceListResponse _value; + + @override + VoiceListResponse items(List items) => call(items: items); + + @override + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `VoiceListResponse(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// VoiceListResponse(...).copyWith(id: 12, name: "My name") + /// ``` + VoiceListResponse call({Object? items = const $CopyWithPlaceholder()}) { + return VoiceListResponse( + items: items == const $CopyWithPlaceholder() || items == null + ? _value.items + // ignore: cast_nullable_to_non_nullable + : items as List, + ); + } +} + +extension $VoiceListResponseCopyWith on VoiceListResponse { + /// Returns a callable class used to build a new instance with modified fields. + /// Example: `instanceOfVoiceListResponse.copyWith(...)` or `instanceOfVoiceListResponse.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$VoiceListResponseCWProxy get copyWith => + _$VoiceListResponseCWProxyImpl(this); +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +VoiceListResponse _$VoiceListResponseFromJson(Map json) => + VoiceListResponse( + items: (json['items'] as List) + .map((e) => AdminVoiceResponse.fromJson(e as Map)) + .toList(), + ); + +Map _$VoiceListResponseToJson(VoiceListResponse instance) => + {'items': instance.items.map((e) => e.toJson()).toList()}; diff --git a/mnemo_cards_common/lib/src/dtos/common/error_response.dart b/mnemo_cards_common/lib/src/dtos/common/error_response.dart new file mode 100644 index 0000000..17815a0 --- /dev/null +++ b/mnemo_cards_common/lib/src/dtos/common/error_response.dart @@ -0,0 +1,26 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:copy_with_extension/copy_with_extension.dart'; + +part 'error_response.g.dart'; + +/// Стандартизированный ответ об ошибке +@JsonSerializable() +@CopyWith() +class ErrorResponse { + final String error; + final String message; + final String? field; + final String? details; + + const ErrorResponse({ + required this.error, + required this.message, + this.field, + this.details, + }); + + factory ErrorResponse.fromJson(Map json) => + _$ErrorResponseFromJson(json); + + Map toJson() => _$ErrorResponseToJson(this); +} diff --git a/mnemo_cards_common/lib/src/dtos/common/error_response.g.dart b/mnemo_cards_common/lib/src/dtos/common/error_response.g.dart new file mode 100644 index 0000000..88ad9da --- /dev/null +++ b/mnemo_cards_common/lib/src/dtos/common/error_response.g.dart @@ -0,0 +1,112 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'error_response.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$ErrorResponseCWProxy { + ErrorResponse error(String error); + + ErrorResponse message(String message); + + ErrorResponse field(String? field); + + ErrorResponse details(String? details); + + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `ErrorResponse(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// ErrorResponse(...).copyWith(id: 12, name: "My name") + /// ``` + ErrorResponse call({ + String error, + String message, + String? field, + String? details, + }); +} + +/// Callable proxy for `copyWith` functionality. +/// Use as `instanceOfErrorResponse.copyWith(...)` or call `instanceOfErrorResponse.copyWith.fieldName(value)` for a single field. +class _$ErrorResponseCWProxyImpl implements _$ErrorResponseCWProxy { + const _$ErrorResponseCWProxyImpl(this._value); + + final ErrorResponse _value; + + @override + ErrorResponse error(String error) => call(error: error); + + @override + ErrorResponse message(String message) => call(message: message); + + @override + ErrorResponse field(String? field) => call(field: field); + + @override + ErrorResponse details(String? details) => call(details: details); + + @override + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `ErrorResponse(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// ErrorResponse(...).copyWith(id: 12, name: "My name") + /// ``` + ErrorResponse call({ + Object? error = const $CopyWithPlaceholder(), + Object? message = const $CopyWithPlaceholder(), + Object? field = const $CopyWithPlaceholder(), + Object? details = const $CopyWithPlaceholder(), + }) { + return ErrorResponse( + error: error == const $CopyWithPlaceholder() || error == null + ? _value.error + // ignore: cast_nullable_to_non_nullable + : error as String, + message: message == const $CopyWithPlaceholder() || message == null + ? _value.message + // ignore: cast_nullable_to_non_nullable + : message as String, + field: field == const $CopyWithPlaceholder() + ? _value.field + // ignore: cast_nullable_to_non_nullable + : field as String?, + details: details == const $CopyWithPlaceholder() + ? _value.details + // ignore: cast_nullable_to_non_nullable + : details as String?, + ); + } +} + +extension $ErrorResponseCopyWith on ErrorResponse { + /// Returns a callable class used to build a new instance with modified fields. + /// Example: `instanceOfErrorResponse.copyWith(...)` or `instanceOfErrorResponse.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$ErrorResponseCWProxy get copyWith => _$ErrorResponseCWProxyImpl(this); +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ErrorResponse _$ErrorResponseFromJson(Map json) => + ErrorResponse( + error: json['error'] as String, + message: json['message'] as String, + field: json['field'] as String?, + details: json['details'] as String?, + ); + +Map _$ErrorResponseToJson(ErrorResponse instance) => + { + 'error': instance.error, + 'message': instance.message, + 'field': instance.field, + 'details': instance.details, + }; diff --git a/mnemo_cards_common/lib/src/dtos/common/paginated_response.dart b/mnemo_cards_common/lib/src/dtos/common/paginated_response.dart new file mode 100644 index 0000000..020b347 --- /dev/null +++ b/mnemo_cards_common/lib/src/dtos/common/paginated_response.dart @@ -0,0 +1,32 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:copy_with_extension/copy_with_extension.dart'; + +part 'paginated_response.g.dart'; + +/// Универсальный ответ с пагинацией +@JsonSerializable(genericArgumentFactories: true) +@CopyWith() +class PaginatedResponse { + final List items; + final int total; + final int page; + final int limit; + final int totalPages; + + const PaginatedResponse({ + required this.items, + required this.total, + required this.page, + required this.limit, + required this.totalPages, + }); + + factory PaginatedResponse.fromJson( + Map json, + T Function(Object?) fromJsonT, + ) => + _$PaginatedResponseFromJson(json, fromJsonT); + + Map toJson(Object? Function(T) toJsonT) => + _$PaginatedResponseToJson(this, toJsonT); +} diff --git a/mnemo_cards_common/lib/src/dtos/common/paginated_response.g.dart b/mnemo_cards_common/lib/src/dtos/common/paginated_response.g.dart new file mode 100644 index 0000000..e1d575f --- /dev/null +++ b/mnemo_cards_common/lib/src/dtos/common/paginated_response.g.dart @@ -0,0 +1,133 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'paginated_response.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$PaginatedResponseCWProxy { + PaginatedResponse items(List items); + + PaginatedResponse total(int total); + + PaginatedResponse page(int page); + + PaginatedResponse limit(int limit); + + PaginatedResponse totalPages(int totalPages); + + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `PaginatedResponse(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// PaginatedResponse(...).copyWith(id: 12, name: "My name") + /// ``` + PaginatedResponse call({ + List items, + int total, + int page, + int limit, + int totalPages, + }); +} + +/// Callable proxy for `copyWith` functionality. +/// Use as `instanceOfPaginatedResponse.copyWith(...)` or call `instanceOfPaginatedResponse.copyWith.fieldName(value)` for a single field. +class _$PaginatedResponseCWProxyImpl + implements _$PaginatedResponseCWProxy { + const _$PaginatedResponseCWProxyImpl(this._value); + + final PaginatedResponse _value; + + @override + PaginatedResponse items(List items) => call(items: items); + + @override + PaginatedResponse total(int total) => call(total: total); + + @override + PaginatedResponse page(int page) => call(page: page); + + @override + PaginatedResponse limit(int limit) => call(limit: limit); + + @override + PaginatedResponse totalPages(int totalPages) => + call(totalPages: totalPages); + + @override + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `PaginatedResponse(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// PaginatedResponse(...).copyWith(id: 12, name: "My name") + /// ``` + PaginatedResponse call({ + Object? items = const $CopyWithPlaceholder(), + Object? total = const $CopyWithPlaceholder(), + Object? page = const $CopyWithPlaceholder(), + Object? limit = const $CopyWithPlaceholder(), + Object? totalPages = const $CopyWithPlaceholder(), + }) { + return PaginatedResponse( + items: items == const $CopyWithPlaceholder() || items == null + ? _value.items + // ignore: cast_nullable_to_non_nullable + : items as List, + total: total == const $CopyWithPlaceholder() || total == null + ? _value.total + // ignore: cast_nullable_to_non_nullable + : total as int, + page: page == const $CopyWithPlaceholder() || page == null + ? _value.page + // ignore: cast_nullable_to_non_nullable + : page as int, + limit: limit == const $CopyWithPlaceholder() || limit == null + ? _value.limit + // ignore: cast_nullable_to_non_nullable + : limit as int, + totalPages: + totalPages == const $CopyWithPlaceholder() || totalPages == null + ? _value.totalPages + // ignore: cast_nullable_to_non_nullable + : totalPages as int, + ); + } +} + +extension $PaginatedResponseCopyWith on PaginatedResponse { + /// Returns a callable class used to build a new instance with modified fields. + /// Example: `instanceOfPaginatedResponse.copyWith(...)` or `instanceOfPaginatedResponse.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$PaginatedResponseCWProxy get copyWith => + _$PaginatedResponseCWProxyImpl(this); +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PaginatedResponse _$PaginatedResponseFromJson( + Map json, + T Function(Object? json) fromJsonT, +) => PaginatedResponse( + items: (json['items'] as List).map(fromJsonT).toList(), + total: (json['total'] as num).toInt(), + page: (json['page'] as num).toInt(), + limit: (json['limit'] as num).toInt(), + totalPages: (json['totalPages'] as num).toInt(), +); + +Map _$PaginatedResponseToJson( + PaginatedResponse instance, + Object? Function(T value) toJsonT, +) => { + 'items': instance.items.map(toJsonT).toList(), + 'total': instance.total, + 'page': instance.page, + 'limit': instance.limit, + 'totalPages': instance.totalPages, +}; diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/card_flipper/card_flipper.dart b/mnemo_cards_web_v2/lib/presentation/widgets/card_flipper/card_flipper.dart index 32f5b81..6a37614 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/card_flipper/card_flipper.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/card_flipper/card_flipper.dart @@ -738,20 +738,6 @@ class _CardSide extends StatelessWidget { return Image.network( imageUrl, fit: BoxFit.cover, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) { - return child; - } - return Center( - child: CircularProgressIndicator( - value: loadingProgress.expectedTotalBytes != null - ? loadingProgress.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : null, - color: packColor, - ), - ); - }, errorBuilder: (context, error, stackTrace) { return Container( color: packColor.withOpacity(0.1),