stuff
Some checks failed
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Deploy Admin Panel / Deploy Admin Panel (push) Waiting to run
Deploy Admin Panel / Admin Panel Verification (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
Mobile App CI / test (push) Has been cancelled
Web App CI / test (push) Has been cancelled
Deploy Telegram Bot / Deploy Telegram Bot (push) Has been cancelled
Mobile App CI / build-android (push) Has been cancelled
Mobile App CI / build-ios (push) Has been cancelled
Web App CI / build (push) Has been cancelled

This commit is contained in:
Dmitry 2025-12-17 22:54:48 +03:00
parent b4ad3598ee
commit f6d68fb1fc
29 changed files with 1944 additions and 439 deletions

View file

@ -112,7 +112,7 @@ export const cardsApi = {
}, },
// Delete a card by ID // Delete a card by ID
deleteCard: async (cardId: string): Promise<{ success: boolean; message: string }> => { deleteCard: async (cardId: string): Promise<{ success: boolean; message?: string }> => {
try { try {
const response = await adminApiClient.delete(`/api/v2/admin/cards/${cardId}`) const response = await adminApiClient.delete(`/api/v2/admin/cards/${cardId}`)
return response.data return response.data

View file

@ -36,7 +36,7 @@ export const voicesApi = {
removeCardVoice: async ( removeCardVoice: async (
cardId: string, cardId: string,
voiceId: string voiceId: string
): Promise<{ success: boolean }> => { ): Promise<{ success: boolean; message?: string }> => {
const response = await adminApiClient.delete( const response = await adminApiClient.delete(
`/api/v2/admin/cards/${cardId}/voices/${voiceId}` `/api/v2/admin/cards/${cardId}/voices/${voiceId}`
) )

View file

@ -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<Set<string>>(new Set())
const [removedTests, setRemovedTests] = useState<Set<string>>(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<string>(
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 (
<div className="space-y-4">
<div className="space-y-2">
<Label>Manage Tests in Pack</Label>
<div className="flex items-center space-x-2">
<Search className="h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search tests by name or ID..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="max-w-sm"
disabled={disabled}
/>
</div>
<p className="text-sm text-muted-foreground">
Selected tests: {selectedTests.size - removedTests.size} / {allTests.length}
</p>
</div>
{isLoading ? (
<div className="text-center py-4">Loading tests...</div>
) : (
<div className="border rounded-lg max-h-[400px] overflow-y-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12"></TableHead>
<TableHead>ID</TableHead>
<TableHead>Name</TableHead>
<TableHead>Questions</TableHead>
<TableHead>Version</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredTests.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground">
No tests found
</TableCell>
</TableRow>
) : (
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 (
<TableRow
key={test.id}
className={selected ? 'bg-muted/50' : ''}
onClick={() => handleToggleTest(testIdStr)}
>
<TableCell>
{selected ? (
<Check className="h-4 w-4 text-primary" />
) : (
<div className="h-4 w-4 border rounded" />
)}
</TableCell>
<TableCell className="font-mono text-sm">{test.id || '-'}</TableCell>
<TableCell className="font-medium">{test.name}</TableCell>
<TableCell>
{typeof test.questions === 'number'
? test.questions
: test.questions?.length || 0}
</TableCell>
<TableCell>{test.version || 'N/A'}</TableCell>
<TableCell>
{newlyRemoved ? (
<Badge variant="destructive">Removed</Badge>
) : selected && inCurrentPack ? (
<Badge variant="default">In Pack</Badge>
) : selected ? (
<Badge variant="secondary">Selected</Badge>
) : inCurrentPack ? (
<Badge variant="outline">In Pack</Badge>
) : null}
</TableCell>
</TableRow>
)
})
)}
</TableBody>
</Table>
</div>
)}
</div>
)
}

View file

@ -38,6 +38,7 @@ import { Textarea } from '@/components/ui/textarea'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import { ImageUpload } from '@/components/ui/image-upload' import { ImageUpload } from '@/components/ui/image-upload'
import { PackCardsManager } from '@/components/PackCardsManager' import { PackCardsManager } from '@/components/PackCardsManager'
import { PackTestsManager } from '@/components/PackTestsManager'
import { Plus, Search, Edit, Trash2, ChevronLeft, ChevronRight } from 'lucide-react' import { Plus, Search, Edit, Trash2, ChevronLeft, ChevronRight } from 'lucide-react'
export default function PacksPage() { export default function PacksPage() {
@ -72,6 +73,11 @@ export default function PacksPage() {
const [cardsToAdd, setCardsToAdd] = useState<string[]>([]) const [cardsToAdd, setCardsToAdd] = useState<string[]>([])
const [cardsToRemove, setCardsToRemove] = useState<string[]>([]) const [cardsToRemove, setCardsToRemove] = useState<string[]>([])
// Test management state
const [currentTestIds, setCurrentTestIds] = useState<string[]>([])
const [testsToAdd, setTestsToAdd] = useState<string[]>([])
const [testsToRemove, setTestsToRemove] = useState<string[]>([])
const limit = 20 const limit = 20
// Fetch packs // Fetch packs
@ -157,6 +163,9 @@ export default function PacksPage() {
setCurrentCardIds([]) setCurrentCardIds([])
setCardsToAdd([]) setCardsToAdd([])
setCardsToRemove([]) setCardsToRemove([])
setCurrentTestIds([])
setTestsToAdd([])
setTestsToRemove([])
setIsDialogOpen(true) setIsDialogOpen(true)
} }
@ -184,6 +193,13 @@ export default function PacksPage() {
setCurrentCardIds(cardIds) setCurrentCardIds(cardIds)
setCardsToAdd([]) setCardsToAdd([])
setCardsToRemove([]) 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) setIsDialogOpen(true)
} catch (error) { } catch (error) {
const errorMessage = isPacksApiError(error) const errorMessage = isPacksApiError(error)
@ -200,6 +216,9 @@ export default function PacksPage() {
setCurrentCardIds([]) setCurrentCardIds([])
setCardsToAdd([]) setCardsToAdd([])
setCardsToRemove([]) setCardsToRemove([])
setCurrentTestIds([])
setTestsToAdd([])
setTestsToRemove([])
} }
const handleCardsChange = (addIds: string[], removeIds: string[]) => { const handleCardsChange = (addIds: string[], removeIds: string[]) => {
@ -207,6 +226,11 @@ export default function PacksPage() {
setCardsToRemove(removeIds) setCardsToRemove(removeIds)
} }
const handleTestsChange = (addIds: string[], removeIds: string[]) => {
setTestsToAdd(addIds)
setTestsToRemove(removeIds)
}
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
@ -233,6 +257,8 @@ export default function PacksPage() {
cover: formData.cover || undefined, cover: formData.cover || undefined,
addCardIds: cardsToAdd.length > 0 ? cardsToAdd : undefined, addCardIds: cardsToAdd.length > 0 ? cardsToAdd : undefined,
removeCardIds: cardsToRemove.length > 0 ? cardsToRemove : undefined, removeCardIds: cardsToRemove.length > 0 ? cardsToRemove : undefined,
addTestIds: testsToAdd.length > 0 ? testsToAdd : undefined,
removeTestIds: testsToRemove.length > 0 ? testsToRemove : undefined,
} }
if (selectedPack) { if (selectedPack) {
@ -589,13 +615,22 @@ export default function PacksPage() {
</div> </div>
{selectedPack && ( {selectedPack && (
<div className="space-y-2"> <>
<PackCardsManager <div className="space-y-2">
currentCardIds={currentCardIds} <PackCardsManager
onCardsChange={handleCardsChange} currentCardIds={currentCardIds}
disabled={createMutation.isPending || updateMutation.isPending} onCardsChange={handleCardsChange}
/> disabled={createMutation.isPending || updateMutation.isPending}
</div> />
</div>
<div className="space-y-2">
<PackTestsManager
currentTestIds={currentTestIds}
onTestsChange={handleTestsChange}
disabled={createMutation.isPending || updateMutation.isPending}
/>
</div>
</>
)} )}
</div> </div>
<DialogFooter> <DialogFooter>

View file

@ -312,6 +312,7 @@ export default function TestsPage() {
<TableRow> <TableRow>
<TableHead>ID</TableHead> <TableHead>ID</TableHead>
<TableHead>Name</TableHead> <TableHead>Name</TableHead>
<TableHead>Packs</TableHead>
<TableHead>Questions</TableHead> <TableHead>Questions</TableHead>
<TableHead>Version</TableHead> <TableHead>Version</TableHead>
<TableHead>Time</TableHead> <TableHead>Time</TableHead>
@ -323,6 +324,22 @@ export default function TestsPage() {
<TableRow key={test.id || Math.random()}> <TableRow key={test.id || Math.random()}>
<TableCell className="font-mono text-sm">{test.id || 'N/A'}</TableCell> <TableCell className="font-mono text-sm">{test.id || 'N/A'}</TableCell>
<TableCell className="font-medium">{test.name}</TableCell> <TableCell className="font-medium">{test.name}</TableCell>
<TableCell>
{test.packs && test.packs.length > 0 ? (
<div className="flex flex-wrap gap-1">
{test.packs.map((pack) => (
<span
key={pack.id}
className="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
>
{pack.title}
</span>
))}
</div>
) : (
<span className="text-muted-foreground text-sm">No packs</span>
)}
</TableCell>
<TableCell> <TableCell>
{typeof test.questions === 'number' {typeof test.questions === 'number'
? test.questions ? test.questions

View file

@ -11,6 +11,8 @@ export interface GameCardDto {
transcriptionMnemo?: string transcriptionMnemo?: string
imageBack?: string imageBack?: string
back?: string back?: string
createdAt?: string
updatedAt?: string
} }
export interface EditCardPackDto { export interface EditCardPackDto {
@ -173,6 +175,11 @@ import type { Question } from './questions'
export type TestQuestion = Question export type TestQuestion = Question
export interface TestPackInfo {
id: string
title: string
}
export interface TestDto { export interface TestDto {
id?: string id?: string
name: string name: string
@ -183,4 +190,5 @@ export interface TestDto {
timeSubtitle?: string timeSubtitle?: string
questions: Question[] questions: Question[]
statistics?: unknown statistics?: unknown
packs?: TestPackInfo[]
} }

File diff suppressed because it is too large Load diff

View file

@ -182,6 +182,10 @@ class AdminPacksApiV2 {
final previewCards = await _db.packDao.getPreviewCards(packId); final previewCards = await _db.packDao.getPreviewCards(packId);
final previewCardIds = previewCards.map((c) => c.id).toList(); 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 // Create EditCardPackDto
final editDto = EditCardPackDto( final editDto = EditCardPackDto(
id: pack.id, id: pack.id,
@ -199,7 +203,7 @@ class AdminPacksApiV2 {
version: pack.version, version: pack.version,
order: pack.order, order: pack.order,
addCardIds: cards.map((c) => c.id).toList(), addCardIds: cards.map((c) => c.id).toList(),
addTestIds: null, addTestIds: testIds,
removeCardIds: null, removeCardIds: null,
removeTestIds: null, removeTestIds: null,
cardsOrder: cardsOrder, 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 // Handle cards order if provided
if (editDto.cardsOrder != null && editDto.cardsOrder!.isNotEmpty) { if (editDto.cardsOrder != null && editDto.cardsOrder!.isNotEmpty) {
await _db.packDao.updatePackCardsOrder( await _db.packDao.updatePackCardsOrder(
@ -379,6 +426,10 @@ class AdminPacksApiV2 {
final cardsOrder = updatedCards.map((c) => c.id).toList(); final cardsOrder = updatedCards.map((c) => c.id).toList();
final previewCards = await _db.packDao.getPreviewCards(packId); final previewCards = await _db.packDao.getPreviewCards(packId);
final previewCardIds = previewCards.map((c) => c.id).toList(); 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( final updatedDto = EditCardPackDto(
id: updatedPack.id, id: updatedPack.id,
@ -396,7 +447,7 @@ class AdminPacksApiV2 {
version: updatedPack.version, version: updatedPack.version,
order: updatedPack.order, order: updatedPack.order,
addCardIds: updatedCards.map((c) => c.id).toList(), addCardIds: updatedCards.map((c) => c.id).toList(),
addTestIds: null, addTestIds: updatedTestIds,
removeCardIds: null, removeCardIds: null,
removeTestIds: null, removeTestIds: null,
cardsOrder: cardsOrder, cardsOrder: cardsOrder,

View file

@ -119,6 +119,20 @@ class AdminTestsApiV2 {
final testDtos = <Map<String, dynamic>>[]; final testDtos = <Map<String, dynamic>>[];
for (final test in allTests) { for (final test in allTests) {
final questions = await _db.testDao.getTestQuestions(test.id); final questions = await _db.testDao.getTestQuestions(test.id);
// Get pack information for this test
final packIds = await _db.testDao.getPackIdsForTest(test.id);
final packs = <Map<String, dynamic>>[];
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({ testDtos.add({
'id': test.id, 'id': test.id,
'name': test.name, 'name': test.name,
@ -128,6 +142,7 @@ class AdminTestsApiV2 {
'time': test.time, 'time': test.time,
'timeSubtitle': test.timeSubtitle, 'timeSubtitle': test.timeSubtitle,
'questions': questions.length, '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); final packId = await _db.testDao.getPackIdForTest(testId);
// Get all pack information for this test
final packIds = await _db.testDao.getPackIdsForTest(testId);
final packs = <Map<String, dynamic>>[];
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 // Get questions
final questions = await _db.testDao.getTestQuestions(testId); final questions = await _db.testDao.getTestQuestions(testId);
@ -319,6 +347,7 @@ class AdminTestsApiV2 {
'time': test.time, 'time': test.time,
'timeSubtitle': test.timeSubtitle, 'timeSubtitle': test.timeSubtitle,
'questions': questionsWithUrls, 'questions': questionsWithUrls,
'packs': packs,
}); });
} catch (e, s) { } catch (e, s) {
print('Error in getTest: $e\n$s'); print('Error in getTest: $e\n$s');

View file

@ -678,12 +678,6 @@ class PacksApiV2 {
return _notFound('Pack not found'); 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 // Get tests for pack - fetchPackTests needs CardPackModel, but we can create a minimal one
// or modify TestManager to work with Drift CardPack // or modify TestManager to work with Drift CardPack
// For now, let's get tests directly from database // For now, let's get tests directly from database

View file

@ -71,6 +71,13 @@ class TestDao extends DatabaseAccessor<AppDatabase> with _$TestDaoMixin {
); );
} }
/// Удалить связь теста с паком
Future<void> unlinkTestFromPack(String testId, String packId) async {
await (delete(testPackRelations)
..where((tpr) => tpr.testId.equals(testId) & tpr.packId.equals(packId))
).go();
}
/// Получить packId для теста /// Получить packId для теста
Future<String?> getPackIdForTest(String testId) async { Future<String?> getPackIdForTest(String testId) async {
final relation = await (select(testPackRelations) final relation = await (select(testPackRelations)
@ -79,6 +86,14 @@ class TestDao extends DatabaseAccessor<AppDatabase> with _$TestDaoMixin {
).getSingleOrNull(); ).getSingleOrNull();
return relation?.packId; return relation?.packId;
} }
/// Получить все packId для теста
Future<List<String>> getPackIdsForTest(String testId) async {
final relations = await (select(testPackRelations)
..where((tpr) => tpr.testId.equals(testId))
).get();
return relations.map((r) => r.packId).toList();
}
// ==================== TestQuestions ==================== // ==================== TestQuestions ====================

View file

@ -50,6 +50,18 @@ export 'src/dtos/game_tests/test_question_type.dart';
export 'src/dtos/items/items.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/utils.dart';
export 'src/utils/iterable_helper.dart'; export 'src/utils/iterable_helper.dart';
export 'src/utils/token_generator.dart'; export 'src/utils/token_generator.dart';

View file

@ -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<String, dynamic> json) =>
_$AddVoiceRequestFromJson(json);
Map<String, Object?> toJson() => _$AddVoiceRequestToJson(this);
}

View file

@ -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<String, dynamic> json) =>
AddVoiceRequest(
voiceUrl: json['voiceUrl'] as String,
language: json['language'] as String?,
);
Map<String, dynamic> _$AddVoiceRequestToJson(AddVoiceRequest instance) =>
<String, dynamic>{
'voiceUrl': instance.voiceUrl,
'language': instance.language,
};

View file

@ -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<String, dynamic> json) =>
_$AdminVoiceResponseFromJson(json);
Map<String, Object?> toJson() => _$AdminVoiceResponseToJson(this);
}

View file

@ -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<String, dynamic> 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<String, dynamic> _$AdminVoiceResponseToJson(AdminVoiceResponse instance) =>
<String, dynamic>{
'id': instance.id,
'cardId': instance.cardId,
'voiceUrl': instance.voiceUrl,
'language': instance.language,
'createdAt': instance.createdAt,
};

View file

@ -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<String, dynamic> json) =>
_$CreateCardRequestFromJson(json);
Map<String, Object?> toJson() => _$CreateCardRequestToJson(this);
}

View file

@ -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<String, dynamic> 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<String, dynamic> _$CreateCardRequestToJson(CreateCardRequest instance) =>
<String, dynamic>{
'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,
};

View file

@ -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<String, dynamic> json) =>
_$CreateCardResponseFromJson(json);
Map<String, Object?> toJson() => _$CreateCardResponseToJson(this);
}

View file

@ -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<String, dynamic> json) =>
CreateCardResponse(
success: json['success'] as bool,
card: GameCardDto.fromJson(json['card'] as Map<String, dynamic>),
packId: json['packId'] as String?,
);
Map<String, dynamic> _$CreateCardResponseToJson(CreateCardResponse instance) =>
<String, dynamic>{
'success': instance.success,
'card': instance.card.toJson(),
'packId': instance.packId,
};

View file

@ -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<String, dynamic> json) =>
_$SuccessResponseFromJson(json);
Map<String, Object?> toJson() => _$SuccessResponseToJson(this);
}

View file

@ -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<String, dynamic> json) =>
SuccessResponse(
success: json['success'] as bool,
message: json['message'] as String?,
);
Map<String, dynamic> _$SuccessResponseToJson(SuccessResponse instance) =>
<String, dynamic>{'success': instance.success, 'message': instance.message};

View file

@ -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<AdminVoiceResponse> items;
const VoiceListResponse({
required this.items,
});
factory VoiceListResponse.fromJson(Map<String, dynamic> json) =>
_$VoiceListResponseFromJson(json);
Map<String, Object?> toJson() => _$VoiceListResponseToJson(this);
}

View file

@ -0,0 +1,70 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'voice_list_response.dart';
// **************************************************************************
// CopyWithGenerator
// **************************************************************************
abstract class _$VoiceListResponseCWProxy {
VoiceListResponse items(List<AdminVoiceResponse> 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<AdminVoiceResponse> 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<AdminVoiceResponse> 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<AdminVoiceResponse>,
);
}
}
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<String, dynamic> json) =>
VoiceListResponse(
items: (json['items'] as List<dynamic>)
.map((e) => AdminVoiceResponse.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$VoiceListResponseToJson(VoiceListResponse instance) =>
<String, dynamic>{'items': instance.items.map((e) => e.toJson()).toList()};

View file

@ -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<String, dynamic> json) =>
_$ErrorResponseFromJson(json);
Map<String, Object?> toJson() => _$ErrorResponseToJson(this);
}

View file

@ -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<String, dynamic> json) =>
ErrorResponse(
error: json['error'] as String,
message: json['message'] as String,
field: json['field'] as String?,
details: json['details'] as String?,
);
Map<String, dynamic> _$ErrorResponseToJson(ErrorResponse instance) =>
<String, dynamic>{
'error': instance.error,
'message': instance.message,
'field': instance.field,
'details': instance.details,
};

View file

@ -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<T> {
final List<T> 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<String, dynamic> json,
T Function(Object?) fromJsonT,
) =>
_$PaginatedResponseFromJson(json, fromJsonT);
Map<String, dynamic> toJson(Object? Function(T) toJsonT) =>
_$PaginatedResponseToJson(this, toJsonT);
}

View file

@ -0,0 +1,133 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'paginated_response.dart';
// **************************************************************************
// CopyWithGenerator
// **************************************************************************
abstract class _$PaginatedResponseCWProxy<T> {
PaginatedResponse<T> items(List<T> items);
PaginatedResponse<T> total(int total);
PaginatedResponse<T> page(int page);
PaginatedResponse<T> limit(int limit);
PaginatedResponse<T> 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<T>(...).copyWith.fieldName(value)`.
///
/// Example:
/// ```dart
/// PaginatedResponse<T>(...).copyWith(id: 12, name: "My name")
/// ```
PaginatedResponse<T> call({
List<T> 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<T>
implements _$PaginatedResponseCWProxy<T> {
const _$PaginatedResponseCWProxyImpl(this._value);
final PaginatedResponse<T> _value;
@override
PaginatedResponse<T> items(List<T> items) => call(items: items);
@override
PaginatedResponse<T> total(int total) => call(total: total);
@override
PaginatedResponse<T> page(int page) => call(page: page);
@override
PaginatedResponse<T> limit(int limit) => call(limit: limit);
@override
PaginatedResponse<T> 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<T>(...).copyWith.fieldName(value)`.
///
/// Example:
/// ```dart
/// PaginatedResponse<T>(...).copyWith(id: 12, name: "My name")
/// ```
PaginatedResponse<T> call({
Object? items = const $CopyWithPlaceholder(),
Object? total = const $CopyWithPlaceholder(),
Object? page = const $CopyWithPlaceholder(),
Object? limit = const $CopyWithPlaceholder(),
Object? totalPages = const $CopyWithPlaceholder(),
}) {
return PaginatedResponse<T>(
items: items == const $CopyWithPlaceholder() || items == null
? _value.items
// ignore: cast_nullable_to_non_nullable
: items as List<T>,
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<T> on PaginatedResponse<T> {
/// 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<T> get copyWith =>
_$PaginatedResponseCWProxyImpl<T>(this);
}
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PaginatedResponse<T> _$PaginatedResponseFromJson<T>(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) => PaginatedResponse<T>(
items: (json['items'] as List<dynamic>).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<String, dynamic> _$PaginatedResponseToJson<T>(
PaginatedResponse<T> instance,
Object? Function(T value) toJsonT,
) => <String, dynamic>{
'items': instance.items.map(toJsonT).toList(),
'total': instance.total,
'page': instance.page,
'limit': instance.limit,
'totalPages': instance.totalPages,
};

View file

@ -738,20 +738,6 @@ class _CardSide extends StatelessWidget {
return Image.network( return Image.network(
imageUrl, imageUrl,
fit: BoxFit.cover, 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) { errorBuilder: (context, error, stackTrace) {
return Container( return Container(
color: packColor.withOpacity(0.1), color: packColor.withOpacity(0.1),