Reubencf Claude Fable 5 commited on
Commit
2992997
·
0 Parent(s):

Deploy PraxaLing: component refactor, bug fixes, unified neo-brutal design, Qwen3.5 everywhere

Browse files

Squashed deploy snapshot of main (19712cf); large binaries stored via LFS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .claude/scheduled_tasks.lock +1 -0
  2. .claude/settings.local.json +56 -0
  3. .dockerignore +24 -0
  4. .gitattributes +2 -0
  5. .gitignore +42 -0
  6. .idea/.gitignore +3 -0
  7. .idea/caches/deviceStreaming.xml +1857 -0
  8. .idea/deviceManager.xml +13 -0
  9. .idea/language-learner.iml +15 -0
  10. .idea/libraries/Dart_Packages.xml +972 -0
  11. .idea/libraries/Dart_SDK.xml +31 -0
  12. .idea/markdown.xml +8 -0
  13. .idea/misc.xml +5 -0
  14. .idea/modules.xml +8 -0
  15. .idea/vcs.xml +6 -0
  16. .mcp.json +11 -0
  17. AGENTS.md +5 -0
  18. CLAUDE.md +1 -0
  19. Dockerfile +48 -0
  20. README.md +109 -0
  21. app/(app)/BottomNavLink.tsx +24 -0
  22. app/(app)/camera/AnalysisResults.tsx +78 -0
  23. app/(app)/camera/CameraClient.tsx +153 -0
  24. app/(app)/camera/PhotoMarkers.tsx +82 -0
  25. app/(app)/camera/UploadDropzone.tsx +29 -0
  26. app/(app)/camera/page.tsx +36 -0
  27. app/(app)/camera/types.ts +25 -0
  28. app/(app)/error.tsx +17 -0
  29. app/(app)/layout.tsx +42 -0
  30. app/(app)/phrases/PhraseResult/EmptyState.tsx +35 -0
  31. app/(app)/phrases/PhraseResult/HeaderCard.tsx +33 -0
  32. app/(app)/phrases/PhraseResult/KanjiCard.tsx +39 -0
  33. app/(app)/phrases/PhraseResult/RelatedWordCard.tsx +25 -0
  34. app/(app)/phrases/PhraseResult/Section.tsx +19 -0
  35. app/(app)/phrases/PhraseResult/SentenceCard.tsx +20 -0
  36. app/(app)/phrases/PhraseResult/index.tsx +78 -0
  37. app/(app)/phrases/PhrasesClient.tsx +100 -0
  38. app/(app)/phrases/page.tsx +36 -0
  39. app/(app)/practice/CustomTopicCard.tsx +72 -0
  40. app/(app)/practice/DialoguePracticeClient.tsx +84 -0
  41. app/(app)/practice/DialogueResultView.tsx +30 -0
  42. app/(app)/practice/GeneratingDialogueProgress.tsx +111 -0
  43. app/(app)/practice/ScenarioCard.tsx +50 -0
  44. app/(app)/practice/ScenarioLibrary.tsx +37 -0
  45. app/(app)/practice/StageItem.tsx +64 -0
  46. app/(app)/practice/[storyId]/StoryLoader.tsx +17 -0
  47. app/(app)/practice/[storyId]/StoryQuiz.tsx +162 -0
  48. app/(app)/practice/[storyId]/StoryReader.tsx +86 -0
  49. app/(app)/practice/[storyId]/StoryText.tsx +43 -0
  50. app/(app)/practice/[storyId]/VocabList.tsx +18 -0
.claude/scheduled_tasks.lock ADDED
@@ -0,0 +1 @@
 
 
1
+ {"sessionId":"6e5f9b33-6766-4a70-8673-bd559c8270cb","pid":16372,"acquiredAt":1778656145073}
.claude/settings.local.json ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebFetch(domain:api.anthropic.com)",
5
+ "Bash(tar -xf bundle)",
6
+ "Bash(npx tsc *)",
7
+ "Bash(echo \"EXIT=$?\")",
8
+ "Bash(npx next *)",
9
+ "Bash(npx eslint *)",
10
+ "Bash(git restore *)",
11
+ "Bash(git reset *)",
12
+ "Bash(Get-ChildItem -Path \"E:\\\\language-learner\\\\mobile\" -Recurse -Include \"pubspec.yaml\", \"*.yaml\", \"*.yml\")",
13
+ "Bash(Select-Object FullName)",
14
+ "Bash(Get-ChildItem -Path \"E:\\\\language-learner\\\\node_modules\\\\next\\\\dist\\\\docs\" -ErrorAction SilentlyContinue)",
15
+ "Bash(Select-Object -First 20)",
16
+ "Bash(npm install *)",
17
+ "Bash(Get-Content \"E:\\\\language-learner\\\\mobile\\\\lib\\\\features\\\\home\\\\home_shell.dart\")",
18
+ "Bash(Select-Object -First 3)",
19
+ "Bash(npm run *)",
20
+ "mcp__puppeteer__puppeteer_navigate",
21
+ "mcp__puppeteer__puppeteer_screenshot",
22
+ "Bash(taskkill /PID 24984 /F)",
23
+ "PowerShell(Stop-Process *)",
24
+ "PowerShell(Get-Process *)",
25
+ "Read(//tmp/**)",
26
+ "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:3000/)",
27
+ "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:3000/onboarding)",
28
+ "Bash(curl -s -o /dev/null -w \" | /camera: %{http_code}\" http://localhost:3000/camera)",
29
+ "Bash(git add *)",
30
+ "Bash(git commit -m ' *)",
31
+ "Bash(git push *)",
32
+ "PowerShell(git push *)",
33
+ "Bash(taskkill *)",
34
+ "Bash(git *)",
35
+ "Bash(npm ls *)",
36
+ "Bash(findstr /i \"huggingface inference\")",
37
+ "mcp__claude_ai_Hugging_Face__hub_repo_details",
38
+ "mcp__claude_ai_Hugging_Face__hub_repo_search",
39
+ "Bash(curl -s -o NUL -w \"practice %{http_code}\\\\n\" http://localhost:3000/practice)",
40
+ "Bash(curl -s -o NUL -w \"phrases %{http_code}\\\\n\" http://localhost:3000/phrases)",
41
+ "Bash(curl -s -o NUL -w \"camera %{http_code}\\\\n\" http://localhost:3000/camera)",
42
+ "Bash(npx --no -- tsc --noEmit --jsx preserve --esModuleInterop --skipLibCheck app/onboarding/OnboardingForm.tsx)",
43
+ "Bash(Get-ChildItem -Path \"E:\\\\language-learner\\\\mobile\\\\lib\" -Recurse -File)",
44
+ "Bash(Sort-Object FullName)",
45
+ "Bash(Get-ChildItem -Path \"E:\\\\language-learner\\\\app\" -Recurse -Directory)",
46
+ "Bash(Select-Object -First 30 FullName)",
47
+ "Bash(grep -E \"\\\\.\\(tsx?|json|mjs\\)$\")",
48
+ "Bash(xargs wc -l)",
49
+ "Bash(flutter analyze *)",
50
+ "Bash(ls *)",
51
+ "Bash(xargs grep -l \"novita\")",
52
+ "Bash(echo \"EXIT_CODE=$?\")"
53
+ ]
54
+ },
55
+ "outputStyle": "default"
56
+ }
.dockerignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .gitignore
3
+ .github
4
+ .next
5
+ node_modules
6
+ npm-debug.log
7
+ .env
8
+ .env.local
9
+ .env.*.local
10
+ .vscode
11
+ .idea
12
+ .DS_Store
13
+
14
+ # Mobile app — not part of the web build
15
+ mobile
16
+
17
+ # Claude / local working dirs
18
+ .claude
19
+ docs
20
+
21
+ # Build artifacts you don't need in the container
22
+ tsconfig.tsbuildinfo
23
+ response.html
24
+ *.log
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ logo.png filter=lfs diff=lfs merge=lfs -text
2
+ mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+ !.env.example
36
+
37
+ # vercel
38
+ .vercel
39
+
40
+ # typescript
41
+ *.tsbuildinfo
42
+ next-env.d.ts
.idea/.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
.idea/caches/deviceStreaming.xml ADDED
@@ -0,0 +1,1857 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="DeviceStreaming">
4
+ <option name="deviceSelectionList">
5
+ <list>
6
+ <PersistentDeviceSelectionData>
7
+ <option name="api" value="34" />
8
+ <option name="brand" value="Sony" />
9
+ <option name="codename" value="A402SO" />
10
+ <option name="id" value="A402SO" />
11
+ <option name="labId" value="google" />
12
+ <option name="manufacturer" value="Sony" />
13
+ <option name="name" value="Xperia 10 VI" />
14
+ <option name="screenDensity" value="450" />
15
+ <option name="screenX" value="1080" />
16
+ <option name="screenY" value="2520" />
17
+ </PersistentDeviceSelectionData>
18
+ <PersistentDeviceSelectionData>
19
+ <option name="api" value="27" />
20
+ <option name="brand" value="DOCOMO" />
21
+ <option name="codename" value="F01L" />
22
+ <option name="id" value="F01L" />
23
+ <option name="labId" value="google" />
24
+ <option name="manufacturer" value="FUJITSU" />
25
+ <option name="name" value="F-01L" />
26
+ <option name="screenDensity" value="360" />
27
+ <option name="screenX" value="720" />
28
+ <option name="screenY" value="1280" />
29
+ </PersistentDeviceSelectionData>
30
+ <PersistentDeviceSelectionData>
31
+ <option name="api" value="34" />
32
+ <option name="brand" value="OnePlus" />
33
+ <option name="codename" value="OP535DL1" />
34
+ <option name="id" value="OP535DL1" />
35
+ <option name="labId" value="google" />
36
+ <option name="manufacturer" value="OnePlus" />
37
+ <option name="name" value="Nord CE 2 Lite 5G" />
38
+ <option name="screenDensity" value="401" />
39
+ <option name="screenX" value="1080" />
40
+ <option name="screenY" value="2412" />
41
+ </PersistentDeviceSelectionData>
42
+ <PersistentDeviceSelectionData>
43
+ <option name="api" value="34" />
44
+ <option name="brand" value="OnePlus" />
45
+ <option name="codename" value="OP5552L1" />
46
+ <option name="id" value="OP5552L1" />
47
+ <option name="labId" value="google" />
48
+ <option name="manufacturer" value="OnePlus" />
49
+ <option name="name" value="10T 5G" />
50
+ <option name="screenDensity" value="480" />
51
+ <option name="screenX" value="1080" />
52
+ <option name="screenY" value="2412" />
53
+ </PersistentDeviceSelectionData>
54
+ <PersistentDeviceSelectionData>
55
+ <option name="api" value="34" />
56
+ <option name="brand" value="OPPO" />
57
+ <option name="codename" value="OP573DL1" />
58
+ <option name="id" value="OP573DL1" />
59
+ <option name="labId" value="google" />
60
+ <option name="manufacturer" value="OPPO" />
61
+ <option name="name" value="A79 5G" />
62
+ <option name="screenDensity" value="480" />
63
+ <option name="screenX" value="1080" />
64
+ <option name="screenY" value="2400" />
65
+ </PersistentDeviceSelectionData>
66
+ <PersistentDeviceSelectionData>
67
+ <option name="api" value="34" />
68
+ <option name="brand" value="OPPO" />
69
+ <option name="codename" value="OP5759L1" />
70
+ <option name="id" value="OP5759L1" />
71
+ <option name="labId" value="google" />
72
+ <option name="manufacturer" value="OPPO" />
73
+ <option name="name" value="A38" />
74
+ <option name="screenDensity" value="320" />
75
+ <option name="screenX" value="720" />
76
+ <option name="screenY" value="1612" />
77
+ </PersistentDeviceSelectionData>
78
+ <PersistentDeviceSelectionData>
79
+ <option name="api" value="35" />
80
+ <option name="brand" value="realme" />
81
+ <option name="codename" value="RE58C2" />
82
+ <option name="id" value="RE58C2" />
83
+ <option name="labId" value="google" />
84
+ <option name="manufacturer" value="realme" />
85
+ <option name="name" value="C53" />
86
+ <option name="screenDensity" value="320" />
87
+ <option name="screenX" value="720" />
88
+ <option name="screenY" value="1600" />
89
+ </PersistentDeviceSelectionData>
90
+ <PersistentDeviceSelectionData>
91
+ <option name="api" value="30" />
92
+ <option name="brand" value="realme" />
93
+ <option name="codename" value="RMX3231" />
94
+ <option name="id" value="RMX3231" />
95
+ <option name="labId" value="google" />
96
+ <option name="manufacturer" value="realme" />
97
+ <option name="name" value="RMX3231" />
98
+ <option name="screenDensity" value="320" />
99
+ <option name="screenX" value="720" />
100
+ <option name="screenY" value="1600" />
101
+ </PersistentDeviceSelectionData>
102
+ <PersistentDeviceSelectionData>
103
+ <option name="api" value="36" />
104
+ <option name="brand" value="samsung" />
105
+ <option name="codename" value="SC-53C" />
106
+ <option name="id" value="SC-53C" />
107
+ <option name="labId" value="google" />
108
+ <option name="manufacturer" value="Samsung" />
109
+ <option name="name" value="Galaxy A53 5G" />
110
+ <option name="screenDensity" value="450" />
111
+ <option name="screenX" value="1080" />
112
+ <option name="screenY" value="2400" />
113
+ </PersistentDeviceSelectionData>
114
+ <PersistentDeviceSelectionData>
115
+ <option name="api" value="28" />
116
+ <option name="brand" value="DOCOMO" />
117
+ <option name="codename" value="SH-01L" />
118
+ <option name="id" value="SH-01L" />
119
+ <option name="labId" value="google" />
120
+ <option name="manufacturer" value="SHARP" />
121
+ <option name="name" value="AQUOS sense2 SH-01L" />
122
+ <option name="screenDensity" value="480" />
123
+ <option name="screenX" value="1080" />
124
+ <option name="screenY" value="2160" />
125
+ </PersistentDeviceSelectionData>
126
+ <PersistentDeviceSelectionData>
127
+ <option name="api" value="35" />
128
+ <option name="brand" value="Lenovo" />
129
+ <option name="codename" value="TB330FU" />
130
+ <option name="formFactor" value="Tablet" />
131
+ <option name="id" value="TB330FU" />
132
+ <option name="labId" value="google" />
133
+ <option name="manufacturer" value="Lenovo" />
134
+ <option name="name" value="Tab M11" />
135
+ <option name="screenDensity" value="240" />
136
+ <option name="screenX" value="1200" />
137
+ <option name="screenY" value="1920" />
138
+ </PersistentDeviceSelectionData>
139
+ <PersistentDeviceSelectionData>
140
+ <option name="api" value="33" />
141
+ <option name="brand" value="samsung" />
142
+ <option name="codename" value="a03su" />
143
+ <option name="id" value="a03su" />
144
+ <option name="labId" value="google" />
145
+ <option name="manufacturer" value="Samsung" />
146
+ <option name="name" value="Galaxy A03s" />
147
+ <option name="screenDensity" value="280" />
148
+ <option name="screenX" value="720" />
149
+ <option name="screenY" value="1600" />
150
+ </PersistentDeviceSelectionData>
151
+ <PersistentDeviceSelectionData>
152
+ <option name="api" value="35" />
153
+ <option name="brand" value="samsung" />
154
+ <option name="codename" value="a05s" />
155
+ <option name="id" value="a05s" />
156
+ <option name="labId" value="google" />
157
+ <option name="manufacturer" value="Samsung" />
158
+ <option name="name" value="Galaxy A05s" />
159
+ <option name="screenDensity" value="450" />
160
+ <option name="screenX" value="1080" />
161
+ <option name="screenY" value="2400" />
162
+ </PersistentDeviceSelectionData>
163
+ <PersistentDeviceSelectionData>
164
+ <option name="api" value="35" />
165
+ <option name="brand" value="samsung" />
166
+ <option name="codename" value="a06" />
167
+ <option name="id" value="a06" />
168
+ <option name="labId" value="google" />
169
+ <option name="manufacturer" value="Samsung" />
170
+ <option name="name" value="Galaxy A06" />
171
+ <option name="screenDensity" value="300" />
172
+ <option name="screenX" value="720" />
173
+ <option name="screenY" value="1600" />
174
+ </PersistentDeviceSelectionData>
175
+ <PersistentDeviceSelectionData>
176
+ <option name="api" value="34" />
177
+ <option name="brand" value="samsung" />
178
+ <option name="codename" value="a13" />
179
+ <option name="id" value="a13" />
180
+ <option name="labId" value="google" />
181
+ <option name="manufacturer" value="Samsung" />
182
+ <option name="name" value="Galaxy A13" />
183
+ <option name="screenDensity" value="450" />
184
+ <option name="screenX" value="1080" />
185
+ <option name="screenY" value="2408" />
186
+ </PersistentDeviceSelectionData>
187
+ <PersistentDeviceSelectionData>
188
+ <option name="api" value="33" />
189
+ <option name="brand" value="samsung" />
190
+ <option name="codename" value="a13x" />
191
+ <option name="id" value="a13x" />
192
+ <option name="labId" value="google" />
193
+ <option name="manufacturer" value="Samsung" />
194
+ <option name="name" value="Galaxy A13 5G" />
195
+ <option name="screenDensity" value="300" />
196
+ <option name="screenX" value="720" />
197
+ <option name="screenY" value="1600" />
198
+ </PersistentDeviceSelectionData>
199
+ <PersistentDeviceSelectionData>
200
+ <option name="api" value="34" />
201
+ <option name="brand" value="samsung" />
202
+ <option name="codename" value="a14m" />
203
+ <option name="id" value="a14m" />
204
+ <option name="labId" value="google" />
205
+ <option name="manufacturer" value="Samsung" />
206
+ <option name="name" value="Galaxy A14" />
207
+ <option name="screenDensity" value="450" />
208
+ <option name="screenX" value="1080" />
209
+ <option name="screenY" value="2408" />
210
+ </PersistentDeviceSelectionData>
211
+ <PersistentDeviceSelectionData>
212
+ <option name="api" value="35" />
213
+ <option name="brand" value="samsung" />
214
+ <option name="codename" value="a14xmsq" />
215
+ <option name="id" value="a14xmsq" />
216
+ <option name="labId" value="google" />
217
+ <option name="manufacturer" value="Samsung" />
218
+ <option name="name" value="Galaxy A14 5G" />
219
+ <option name="screenDensity" value="450" />
220
+ <option name="screenX" value="1080" />
221
+ <option name="screenY" value="2408" />
222
+ </PersistentDeviceSelectionData>
223
+ <PersistentDeviceSelectionData>
224
+ <option name="api" value="35" />
225
+ <option name="brand" value="samsung" />
226
+ <option name="codename" value="a14xmtfn" />
227
+ <option name="id" value="a14xmtfn" />
228
+ <option name="labId" value="google" />
229
+ <option name="manufacturer" value="Samsung" />
230
+ <option name="name" value="Galaxy A14 5G" />
231
+ <option name="screenDensity" value="450" />
232
+ <option name="screenX" value="1080" />
233
+ <option name="screenY" value="2408" />
234
+ </PersistentDeviceSelectionData>
235
+ <PersistentDeviceSelectionData>
236
+ <option name="api" value="34" />
237
+ <option name="brand" value="samsung" />
238
+ <option name="codename" value="a15" />
239
+ <option name="id" value="a15" />
240
+ <option name="labId" value="google" />
241
+ <option name="manufacturer" value="Samsung" />
242
+ <option name="name" value="Galaxy A15" />
243
+ <option name="screenDensity" value="450" />
244
+ <option name="screenX" value="1080" />
245
+ <option name="screenY" value="2340" />
246
+ </PersistentDeviceSelectionData>
247
+ <PersistentDeviceSelectionData>
248
+ <option name="api" value="34" />
249
+ <option name="brand" value="samsung" />
250
+ <option name="codename" value="a15x" />
251
+ <option name="id" value="a15x" />
252
+ <option name="labId" value="google" />
253
+ <option name="manufacturer" value="Samsung" />
254
+ <option name="name" value="Galaxy A15 5G" />
255
+ <option name="screenDensity" value="450" />
256
+ <option name="screenX" value="1080" />
257
+ <option name="screenY" value="2340" />
258
+ </PersistentDeviceSelectionData>
259
+ <PersistentDeviceSelectionData>
260
+ <option name="api" value="36" />
261
+ <option name="brand" value="samsung" />
262
+ <option name="codename" value="a15x" />
263
+ <option name="id" value="a15x" />
264
+ <option name="labId" value="google" />
265
+ <option name="manufacturer" value="Samsung" />
266
+ <option name="name" value="Galaxy A15 5G" />
267
+ <option name="screenDensity" value="450" />
268
+ <option name="screenX" value="1080" />
269
+ <option name="screenY" value="2340" />
270
+ </PersistentDeviceSelectionData>
271
+ <PersistentDeviceSelectionData>
272
+ <option name="api" value="36" />
273
+ <option name="brand" value="samsung" />
274
+ <option name="codename" value="a15xtfn" />
275
+ <option name="id" value="a15xtfn" />
276
+ <option name="labId" value="google" />
277
+ <option name="manufacturer" value="Samsung" />
278
+ <option name="name" value="Galaxy A15 5G" />
279
+ <option name="screenDensity" value="450" />
280
+ <option name="screenX" value="1080" />
281
+ <option name="screenY" value="2340" />
282
+ </PersistentDeviceSelectionData>
283
+ <PersistentDeviceSelectionData>
284
+ <option name="api" value="35" />
285
+ <option name="brand" value="samsung" />
286
+ <option name="codename" value="a16" />
287
+ <option name="id" value="a16" />
288
+ <option name="labId" value="google" />
289
+ <option name="manufacturer" value="Samsung" />
290
+ <option name="name" value="Galaxy A16" />
291
+ <option name="screenDensity" value="450" />
292
+ <option name="screenX" value="1080" />
293
+ <option name="screenY" value="2340" />
294
+ </PersistentDeviceSelectionData>
295
+ <PersistentDeviceSelectionData>
296
+ <option name="api" value="34" />
297
+ <option name="brand" value="samsung" />
298
+ <option name="codename" value="a16x" />
299
+ <option name="id" value="a16x" />
300
+ <option name="labId" value="google" />
301
+ <option name="manufacturer" value="Samsung" />
302
+ <option name="name" value="Galaxy A16 5G" />
303
+ <option name="screenDensity" value="450" />
304
+ <option name="screenX" value="1080" />
305
+ <option name="screenY" value="2340" />
306
+ </PersistentDeviceSelectionData>
307
+ <PersistentDeviceSelectionData>
308
+ <option name="api" value="36" />
309
+ <option name="brand" value="samsung" />
310
+ <option name="codename" value="a16x" />
311
+ <option name="id" value="a16x" />
312
+ <option name="labId" value="google" />
313
+ <option name="manufacturer" value="Samsung" />
314
+ <option name="name" value="Galaxy A16 5G" />
315
+ <option name="screenDensity" value="450" />
316
+ <option name="screenX" value="1080" />
317
+ <option name="screenY" value="2340" />
318
+ </PersistentDeviceSelectionData>
319
+ <PersistentDeviceSelectionData>
320
+ <option name="api" value="35" />
321
+ <option name="brand" value="samsung" />
322
+ <option name="codename" value="a16xeea" />
323
+ <option name="id" value="a16xeea" />
324
+ <option name="labId" value="google" />
325
+ <option name="manufacturer" value="Samsung" />
326
+ <option name="name" value="Galaxy A16 5G" />
327
+ <option name="screenDensity" value="450" />
328
+ <option name="screenX" value="1080" />
329
+ <option name="screenY" value="2340" />
330
+ </PersistentDeviceSelectionData>
331
+ <PersistentDeviceSelectionData>
332
+ <option name="api" value="31" />
333
+ <option name="brand" value="samsung" />
334
+ <option name="codename" value="a21" />
335
+ <option name="id" value="a21" />
336
+ <option name="labId" value="google" />
337
+ <option name="manufacturer" value="Samsung" />
338
+ <option name="name" value="Galaxy A21" />
339
+ <option name="screenDensity" value="300" />
340
+ <option name="screenX" value="720" />
341
+ <option name="screenY" value="1600" />
342
+ </PersistentDeviceSelectionData>
343
+ <PersistentDeviceSelectionData>
344
+ <option name="api" value="36" />
345
+ <option name="brand" value="samsung" />
346
+ <option name="codename" value="a26x" />
347
+ <option name="id" value="a26x" />
348
+ <option name="labId" value="google" />
349
+ <option name="manufacturer" value="Samsung" />
350
+ <option name="name" value="Galaxy A26 5G" />
351
+ <option name="screenDensity" value="450" />
352
+ <option name="screenX" value="1080" />
353
+ <option name="screenY" value="2340" />
354
+ </PersistentDeviceSelectionData>
355
+ <PersistentDeviceSelectionData>
356
+ <option name="api" value="33" />
357
+ <option name="brand" value="samsung" />
358
+ <option name="codename" value="a32" />
359
+ <option name="id" value="a32" />
360
+ <option name="labId" value="google" />
361
+ <option name="manufacturer" value="Samsung" />
362
+ <option name="name" value="Galaxy A32" />
363
+ <option name="screenDensity" value="420" />
364
+ <option name="screenX" value="1080" />
365
+ <option name="screenY" value="2400" />
366
+ </PersistentDeviceSelectionData>
367
+ <PersistentDeviceSelectionData>
368
+ <option name="api" value="36" />
369
+ <option name="brand" value="samsung" />
370
+ <option name="codename" value="a34x" />
371
+ <option name="id" value="a34x" />
372
+ <option name="labId" value="google" />
373
+ <option name="manufacturer" value="Samsung" />
374
+ <option name="name" value="Galaxy A34 5G" />
375
+ <option name="screenDensity" value="450" />
376
+ <option name="screenX" value="1080" />
377
+ <option name="screenY" value="2340" />
378
+ </PersistentDeviceSelectionData>
379
+ <PersistentDeviceSelectionData>
380
+ <option name="api" value="34" />
381
+ <option name="brand" value="samsung" />
382
+ <option name="codename" value="a35x" />
383
+ <option name="id" value="a35x" />
384
+ <option name="labId" value="google" />
385
+ <option name="manufacturer" value="Samsung" />
386
+ <option name="name" value="Galaxy A35 5G" />
387
+ <option name="screenDensity" value="450" />
388
+ <option name="screenX" value="1080" />
389
+ <option name="screenY" value="2340" />
390
+ </PersistentDeviceSelectionData>
391
+ <PersistentDeviceSelectionData>
392
+ <option name="api" value="35" />
393
+ <option name="brand" value="samsung" />
394
+ <option name="codename" value="a35x" />
395
+ <option name="id" value="a35x" />
396
+ <option name="labId" value="google" />
397
+ <option name="manufacturer" value="Samsung" />
398
+ <option name="name" value="Galaxy A35 5G" />
399
+ <option name="screenDensity" value="450" />
400
+ <option name="screenX" value="1080" />
401
+ <option name="screenY" value="2340" />
402
+ </PersistentDeviceSelectionData>
403
+ <PersistentDeviceSelectionData>
404
+ <option name="api" value="36" />
405
+ <option name="brand" value="samsung" />
406
+ <option name="codename" value="a35x" />
407
+ <option name="id" value="a35x" />
408
+ <option name="labId" value="google" />
409
+ <option name="manufacturer" value="Samsung" />
410
+ <option name="name" value="Galaxy A35 5G" />
411
+ <option name="screenDensity" value="450" />
412
+ <option name="screenX" value="1080" />
413
+ <option name="screenY" value="2340" />
414
+ </PersistentDeviceSelectionData>
415
+ <PersistentDeviceSelectionData>
416
+ <option name="api" value="35" />
417
+ <option name="brand" value="samsung" />
418
+ <option name="codename" value="a36xq" />
419
+ <option name="id" value="a36xq" />
420
+ <option name="labId" value="google" />
421
+ <option name="manufacturer" value="Samsung" />
422
+ <option name="name" value="Galaxy A36 5G" />
423
+ <option name="screenDensity" value="450" />
424
+ <option name="screenX" value="1080" />
425
+ <option name="screenY" value="2340" />
426
+ </PersistentDeviceSelectionData>
427
+ <PersistentDeviceSelectionData>
428
+ <option name="api" value="36" />
429
+ <option name="brand" value="samsung" />
430
+ <option name="codename" value="a36xq" />
431
+ <option name="id" value="a36xq" />
432
+ <option name="labId" value="google" />
433
+ <option name="manufacturer" value="Samsung" />
434
+ <option name="name" value="Galaxy A36 5G" />
435
+ <option name="screenDensity" value="450" />
436
+ <option name="screenX" value="1080" />
437
+ <option name="screenY" value="2340" />
438
+ </PersistentDeviceSelectionData>
439
+ <PersistentDeviceSelectionData>
440
+ <option name="api" value="35" />
441
+ <option name="brand" value="samsung" />
442
+ <option name="codename" value="a56x" />
443
+ <option name="id" value="a56x" />
444
+ <option name="labId" value="google" />
445
+ <option name="manufacturer" value="Samsung" />
446
+ <option name="name" value="Galaxy A56 5G" />
447
+ <option name="screenDensity" value="450" />
448
+ <option name="screenX" value="1080" />
449
+ <option name="screenY" value="2340" />
450
+ </PersistentDeviceSelectionData>
451
+ <PersistentDeviceSelectionData>
452
+ <option name="api" value="34" />
453
+ <option name="brand" value="google" />
454
+ <option name="codename" value="akita" />
455
+ <option name="id" value="akita" />
456
+ <option name="labId" value="google" />
457
+ <option name="manufacturer" value="Google" />
458
+ <option name="name" value="Pixel 8a" />
459
+ <option name="screenDensity" value="420" />
460
+ <option name="screenX" value="1080" />
461
+ <option name="screenY" value="2400" />
462
+ </PersistentDeviceSelectionData>
463
+ <PersistentDeviceSelectionData>
464
+ <option name="api" value="35" />
465
+ <option name="brand" value="google" />
466
+ <option name="codename" value="akita" />
467
+ <option name="id" value="akita" />
468
+ <option name="labId" value="google" />
469
+ <option name="manufacturer" value="Google" />
470
+ <option name="name" value="Pixel 8a" />
471
+ <option name="screenDensity" value="420" />
472
+ <option name="screenX" value="1080" />
473
+ <option name="screenY" value="2400" />
474
+ </PersistentDeviceSelectionData>
475
+ <PersistentDeviceSelectionData>
476
+ <option name="api" value="34" />
477
+ <option name="brand" value="motorola" />
478
+ <option name="codename" value="arcfox" />
479
+ <option name="id" value="arcfox" />
480
+ <option name="labId" value="google" />
481
+ <option name="manufacturer" value="Motorola" />
482
+ <option name="name" value="razr plus 2024" />
483
+ <option name="screenDensity" value="360" />
484
+ <option name="screenX" value="1080" />
485
+ <option name="screenY" value="1272" />
486
+ </PersistentDeviceSelectionData>
487
+ <PersistentDeviceSelectionData>
488
+ <option name="api" value="30" />
489
+ <option name="brand" value="motorola" />
490
+ <option name="codename" value="aruba" />
491
+ <option name="id" value="aruba" />
492
+ <option name="labId" value="google" />
493
+ <option name="manufacturer" value="Motorola" />
494
+ <option name="name" value="moto e20" />
495
+ <option name="screenDensity" value="280" />
496
+ <option name="screenX" value="720" />
497
+ <option name="screenY" value="1600" />
498
+ </PersistentDeviceSelectionData>
499
+ <PersistentDeviceSelectionData>
500
+ <option name="api" value="33" />
501
+ <option name="brand" value="motorola" />
502
+ <option name="codename" value="austin" />
503
+ <option name="id" value="austin" />
504
+ <option name="labId" value="google" />
505
+ <option name="manufacturer" value="Motorola" />
506
+ <option name="name" value="moto g 5G (2022)" />
507
+ <option name="screenDensity" value="280" />
508
+ <option name="screenX" value="720" />
509
+ <option name="screenY" value="1600" />
510
+ </PersistentDeviceSelectionData>
511
+ <PersistentDeviceSelectionData>
512
+ <option name="api" value="33" />
513
+ <option name="brand" value="samsung" />
514
+ <option name="codename" value="b0q" />
515
+ <option name="id" value="b0q" />
516
+ <option name="labId" value="google" />
517
+ <option name="manufacturer" value="Samsung" />
518
+ <option name="name" value="Galaxy S22 Ultra" />
519
+ <option name="screenDensity" value="600" />
520
+ <option name="screenX" value="1440" />
521
+ <option name="screenY" value="3088" />
522
+ </PersistentDeviceSelectionData>
523
+ <PersistentDeviceSelectionData>
524
+ <option name="api" value="36" />
525
+ <option name="brand" value="samsung" />
526
+ <option name="codename" value="b0q" />
527
+ <option name="id" value="b0q" />
528
+ <option name="labId" value="google" />
529
+ <option name="manufacturer" value="Samsung" />
530
+ <option name="name" value="Galaxy S22 Ultra" />
531
+ <option name="screenDensity" value="600" />
532
+ <option name="screenX" value="1440" />
533
+ <option name="screenY" value="3088" />
534
+ </PersistentDeviceSelectionData>
535
+ <PersistentDeviceSelectionData>
536
+ <option name="api" value="36" />
537
+ <option name="brand" value="samsung" />
538
+ <option name="codename" value="b5qsqw" />
539
+ <option name="id" value="b5qsqw" />
540
+ <option name="labId" value="google" />
541
+ <option name="manufacturer" value="Samsung" />
542
+ <option name="name" value="Galaxy Z Flip5" />
543
+ <option name="screenDensity" value="340" />
544
+ <option name="screenX" value="748" />
545
+ <option name="screenY" value="720" />
546
+ </PersistentDeviceSelectionData>
547
+ <PersistentDeviceSelectionData>
548
+ <option name="api" value="34" />
549
+ <option name="brand" value="samsung" />
550
+ <option name="codename" value="b6q" />
551
+ <option name="id" value="b6q" />
552
+ <option name="labId" value="google" />
553
+ <option name="manufacturer" value="Samsung" />
554
+ <option name="name" value="Galaxy Z Flip6" />
555
+ <option name="screenDensity" value="340" />
556
+ <option name="screenX" value="1080" />
557
+ <option name="screenY" value="2640" />
558
+ </PersistentDeviceSelectionData>
559
+ <PersistentDeviceSelectionData>
560
+ <option name="api" value="36" />
561
+ <option name="brand" value="samsung" />
562
+ <option name="codename" value="b6qsqw" />
563
+ <option name="id" value="b6qsqw" />
564
+ <option name="labId" value="google" />
565
+ <option name="manufacturer" value="Samsung" />
566
+ <option name="name" value="Galaxy Z Flip6" />
567
+ <option name="screenDensity" value="480" />
568
+ <option name="screenX" value="1080" />
569
+ <option name="screenY" value="2640" />
570
+ </PersistentDeviceSelectionData>
571
+ <PersistentDeviceSelectionData>
572
+ <option name="api" value="36" />
573
+ <option name="brand" value="google" />
574
+ <option name="codename" value="blazer" />
575
+ <option name="default" value="true" />
576
+ <option name="id" value="blazer" />
577
+ <option name="labId" value="google" />
578
+ <option name="manufacturer" value="Google" />
579
+ <option name="name" value="Pixel 10 Pro" />
580
+ <option name="screenDensity" value="420" />
581
+ <option name="screenX" value="1080" />
582
+ <option name="screenY" value="2410" />
583
+ <option name="tags">
584
+ <list>
585
+ <option value="dda-default" />
586
+ </list>
587
+ </option>
588
+ </PersistentDeviceSelectionData>
589
+ <PersistentDeviceSelectionData>
590
+ <option name="api" value="32" />
591
+ <option name="brand" value="google" />
592
+ <option name="codename" value="bluejay" />
593
+ <option name="id" value="bluejay" />
594
+ <option name="labId" value="google" />
595
+ <option name="manufacturer" value="Google" />
596
+ <option name="name" value="Pixel 6a" />
597
+ <option name="screenDensity" value="420" />
598
+ <option name="screenX" value="1080" />
599
+ <option name="screenY" value="2400" />
600
+ </PersistentDeviceSelectionData>
601
+ <PersistentDeviceSelectionData>
602
+ <option name="api" value="33" />
603
+ <option name="brand" value="samsung" />
604
+ <option name="codename" value="c1q" />
605
+ <option name="id" value="c1q" />
606
+ <option name="labId" value="google" />
607
+ <option name="manufacturer" value="Samsung" />
608
+ <option name="name" value="Galaxy Note20 5G" />
609
+ <option name="screenDensity" value="450" />
610
+ <option name="screenX" value="1080" />
611
+ <option name="screenY" value="2400" />
612
+ </PersistentDeviceSelectionData>
613
+ <PersistentDeviceSelectionData>
614
+ <option name="api" value="33" />
615
+ <option name="brand" value="samsung" />
616
+ <option name="codename" value="c1qksw" />
617
+ <option name="id" value="c1qksw" />
618
+ <option name="labId" value="google" />
619
+ <option name="manufacturer" value="Samsung" />
620
+ <option name="name" value="Galaxy Note20 5G" />
621
+ <option name="screenDensity" value="450" />
622
+ <option name="screenX" value="1080" />
623
+ <option name="screenY" value="2400" />
624
+ </PersistentDeviceSelectionData>
625
+ <PersistentDeviceSelectionData>
626
+ <option name="api" value="33" />
627
+ <option name="brand" value="samsung" />
628
+ <option name="codename" value="c2q" />
629
+ <option name="id" value="c2q" />
630
+ <option name="labId" value="google" />
631
+ <option name="manufacturer" value="Samsung" />
632
+ <option name="name" value="Galaxy Note20 Ultra 5G" />
633
+ <option name="screenDensity" value="560" />
634
+ <option name="screenX" value="1440" />
635
+ <option name="screenY" value="3088" />
636
+ </PersistentDeviceSelectionData>
637
+ <PersistentDeviceSelectionData>
638
+ <option name="api" value="34" />
639
+ <option name="brand" value="google" />
640
+ <option name="codename" value="caiman" />
641
+ <option name="id" value="caiman" />
642
+ <option name="labId" value="google" />
643
+ <option name="manufacturer" value="Google" />
644
+ <option name="name" value="Pixel 9 Pro" />
645
+ <option name="screenDensity" value="360" />
646
+ <option name="screenX" value="960" />
647
+ <option name="screenY" value="2142" />
648
+ </PersistentDeviceSelectionData>
649
+ <PersistentDeviceSelectionData>
650
+ <option name="api" value="35" />
651
+ <option name="brand" value="google" />
652
+ <option name="codename" value="caiman" />
653
+ <option name="id" value="caiman" />
654
+ <option name="labId" value="google" />
655
+ <option name="manufacturer" value="Google" />
656
+ <option name="name" value="Pixel 9 Pro" />
657
+ <option name="screenDensity" value="360" />
658
+ <option name="screenX" value="960" />
659
+ <option name="screenY" value="2142" />
660
+ </PersistentDeviceSelectionData>
661
+ <PersistentDeviceSelectionData>
662
+ <option name="api" value="34" />
663
+ <option name="brand" value="google" />
664
+ <option name="codename" value="comet" />
665
+ <option name="id" value="comet" />
666
+ <option name="labId" value="google" />
667
+ <option name="manufacturer" value="Google" />
668
+ <option name="name" value="Pixel 9 Pro Fold" />
669
+ <option name="screenDensity" value="390" />
670
+ <option name="screenX" value="2076" />
671
+ <option name="screenY" value="2152" />
672
+ </PersistentDeviceSelectionData>
673
+ <PersistentDeviceSelectionData>
674
+ <option name="api" value="35" />
675
+ <option name="brand" value="google" />
676
+ <option name="codename" value="comet" />
677
+ <option name="id" value="comet" />
678
+ <option name="labId" value="google" />
679
+ <option name="manufacturer" value="Google" />
680
+ <option name="name" value="Pixel 9 Pro Fold" />
681
+ <option name="screenDensity" value="390" />
682
+ <option name="screenX" value="2076" />
683
+ <option name="screenY" value="2152" />
684
+ </PersistentDeviceSelectionData>
685
+ <PersistentDeviceSelectionData>
686
+ <option name="api" value="36" />
687
+ <option name="brand" value="google" />
688
+ <option name="codename" value="comet" />
689
+ <option name="id" value="comet" />
690
+ <option name="labId" value="google" />
691
+ <option name="manufacturer" value="Google" />
692
+ <option name="name" value="Pixel 9 Pro Fold" />
693
+ <option name="screenDensity" value="390" />
694
+ <option name="screenX" value="2076" />
695
+ <option name="screenY" value="2152" />
696
+ </PersistentDeviceSelectionData>
697
+ <PersistentDeviceSelectionData>
698
+ <option name="api" value="35" />
699
+ <option name="brand" value="motorola" />
700
+ <option name="codename" value="cuscoi" />
701
+ <option name="id" value="cuscoi" />
702
+ <option name="labId" value="google" />
703
+ <option name="manufacturer" value="Motorola" />
704
+ <option name="name" value="moto g96 5G" />
705
+ <option name="screenDensity" value="400" />
706
+ <option name="screenX" value="1080" />
707
+ <option name="screenY" value="2400" />
708
+ </PersistentDeviceSelectionData>
709
+ <PersistentDeviceSelectionData>
710
+ <option name="api" value="35" />
711
+ <option name="brand" value="samsung" />
712
+ <option name="codename" value="dm1q" />
713
+ <option name="id" value="dm1q" />
714
+ <option name="labId" value="google" />
715
+ <option name="manufacturer" value="Samsung" />
716
+ <option name="name" value="Galaxy S23" />
717
+ <option name="screenDensity" value="480" />
718
+ <option name="screenX" value="1080" />
719
+ <option name="screenY" value="2340" />
720
+ </PersistentDeviceSelectionData>
721
+ <PersistentDeviceSelectionData>
722
+ <option name="api" value="36" />
723
+ <option name="brand" value="samsung" />
724
+ <option name="codename" value="dm1q-SM-S911U" />
725
+ <option name="id" value="dm1q-SM-S911U" />
726
+ <option name="labId" value="google" />
727
+ <option name="manufacturer" value="Samsung" />
728
+ <option name="name" value="Galaxy S23" />
729
+ <option name="screenDensity" value="480" />
730
+ <option name="screenX" value="1080" />
731
+ <option name="screenY" value="2340" />
732
+ </PersistentDeviceSelectionData>
733
+ <PersistentDeviceSelectionData>
734
+ <option name="api" value="36" />
735
+ <option name="brand" value="samsung" />
736
+ <option name="codename" value="dm1qcsx" />
737
+ <option name="id" value="dm1qcsx" />
738
+ <option name="labId" value="google" />
739
+ <option name="manufacturer" value="Samsung" />
740
+ <option name="name" value="Galaxy S23" />
741
+ <option name="screenDensity" value="480" />
742
+ <option name="screenX" value="1080" />
743
+ <option name="screenY" value="2340" />
744
+ </PersistentDeviceSelectionData>
745
+ <PersistentDeviceSelectionData>
746
+ <option name="api" value="34" />
747
+ <option name="brand" value="samsung" />
748
+ <option name="codename" value="dm2q" />
749
+ <option name="id" value="dm2q" />
750
+ <option name="labId" value="google" />
751
+ <option name="manufacturer" value="Samsung" />
752
+ <option name="name" value="Galaxy S23+" />
753
+ <option name="screenDensity" value="450" />
754
+ <option name="screenX" value="1080" />
755
+ <option name="screenY" value="2340" />
756
+ </PersistentDeviceSelectionData>
757
+ <PersistentDeviceSelectionData>
758
+ <option name="api" value="34" />
759
+ <option name="brand" value="samsung" />
760
+ <option name="codename" value="dm3q" />
761
+ <option name="id" value="dm3q" />
762
+ <option name="labId" value="google" />
763
+ <option name="manufacturer" value="Samsung" />
764
+ <option name="name" value="Galaxy S23 Ultra" />
765
+ <option name="screenDensity" value="600" />
766
+ <option name="screenX" value="1440" />
767
+ <option name="screenY" value="3088" />
768
+ </PersistentDeviceSelectionData>
769
+ <PersistentDeviceSelectionData>
770
+ <option name="api" value="34" />
771
+ <option name="brand" value="motorola" />
772
+ <option name="codename" value="dubai" />
773
+ <option name="id" value="dubai" />
774
+ <option name="labId" value="google" />
775
+ <option name="manufacturer" value="Motorola" />
776
+ <option name="name" value="edge 30" />
777
+ <option name="screenDensity" value="405" />
778
+ <option name="screenX" value="1080" />
779
+ <option name="screenY" value="2400" />
780
+ </PersistentDeviceSelectionData>
781
+ <PersistentDeviceSelectionData>
782
+ <option name="api" value="34" />
783
+ <option name="brand" value="samsung" />
784
+ <option name="codename" value="e1q" />
785
+ <option name="id" value="e1q" />
786
+ <option name="labId" value="google" />
787
+ <option name="manufacturer" value="Samsung" />
788
+ <option name="name" value="Galaxy S24" />
789
+ <option name="screenDensity" value="480" />
790
+ <option name="screenX" value="1080" />
791
+ <option name="screenY" value="2340" />
792
+ </PersistentDeviceSelectionData>
793
+ <PersistentDeviceSelectionData>
794
+ <option name="api" value="36" />
795
+ <option name="brand" value="samsung" />
796
+ <option name="codename" value="e1q" />
797
+ <option name="id" value="e1q" />
798
+ <option name="labId" value="google" />
799
+ <option name="manufacturer" value="Samsung" />
800
+ <option name="name" value="Galaxy S24" />
801
+ <option name="screenDensity" value="480" />
802
+ <option name="screenX" value="1080" />
803
+ <option name="screenY" value="2340" />
804
+ </PersistentDeviceSelectionData>
805
+ <PersistentDeviceSelectionData>
806
+ <option name="api" value="36" />
807
+ <option name="brand" value="samsung" />
808
+ <option name="codename" value="e1s" />
809
+ <option name="id" value="e1s" />
810
+ <option name="labId" value="google" />
811
+ <option name="manufacturer" value="Samsung" />
812
+ <option name="name" value="Galaxy S24" />
813
+ <option name="screenDensity" value="480" />
814
+ <option name="screenX" value="1080" />
815
+ <option name="screenY" value="2340" />
816
+ </PersistentDeviceSelectionData>
817
+ <PersistentDeviceSelectionData>
818
+ <option name="api" value="36" />
819
+ <option name="brand" value="samsung" />
820
+ <option name="codename" value="e2q" />
821
+ <option name="id" value="e2q" />
822
+ <option name="labId" value="google" />
823
+ <option name="manufacturer" value="Samsung" />
824
+ <option name="name" value="Galaxy S24+" />
825
+ <option name="screenDensity" value="450" />
826
+ <option name="screenX" value="1080" />
827
+ <option name="screenY" value="2340" />
828
+ </PersistentDeviceSelectionData>
829
+ <PersistentDeviceSelectionData>
830
+ <option name="api" value="36" />
831
+ <option name="brand" value="samsung" />
832
+ <option name="codename" value="e2s" />
833
+ <option name="id" value="e2s" />
834
+ <option name="labId" value="google" />
835
+ <option name="manufacturer" value="Samsung" />
836
+ <option name="name" value="Galaxy S24+" />
837
+ <option name="screenDensity" value="450" />
838
+ <option name="screenX" value="1080" />
839
+ <option name="screenY" value="2340" />
840
+ </PersistentDeviceSelectionData>
841
+ <PersistentDeviceSelectionData>
842
+ <option name="api" value="34" />
843
+ <option name="brand" value="samsung" />
844
+ <option name="codename" value="e3q" />
845
+ <option name="id" value="e3q" />
846
+ <option name="labId" value="google" />
847
+ <option name="manufacturer" value="Samsung" />
848
+ <option name="name" value="Galaxy S24 Ultra" />
849
+ <option name="screenDensity" value="450" />
850
+ <option name="screenX" value="1440" />
851
+ <option name="screenY" value="3120" />
852
+ </PersistentDeviceSelectionData>
853
+ <PersistentDeviceSelectionData>
854
+ <option name="api" value="36" />
855
+ <option name="brand" value="samsung" />
856
+ <option name="codename" value="e3q" />
857
+ <option name="id" value="e3q" />
858
+ <option name="labId" value="google" />
859
+ <option name="manufacturer" value="Samsung" />
860
+ <option name="name" value="Galaxy S24 Ultra" />
861
+ <option name="screenDensity" value="450" />
862
+ <option name="screenX" value="1440" />
863
+ <option name="screenY" value="3120" />
864
+ </PersistentDeviceSelectionData>
865
+ <PersistentDeviceSelectionData>
866
+ <option name="api" value="36" />
867
+ <option name="brand" value="samsung" />
868
+ <option name="codename" value="e3qcsx" />
869
+ <option name="id" value="e3qcsx" />
870
+ <option name="labId" value="google" />
871
+ <option name="manufacturer" value="Samsung" />
872
+ <option name="name" value="Galaxy S24 Ultra" />
873
+ <option name="screenDensity" value="450" />
874
+ <option name="screenX" value="1440" />
875
+ <option name="screenY" value="3120" />
876
+ </PersistentDeviceSelectionData>
877
+ <PersistentDeviceSelectionData>
878
+ <option name="api" value="36" />
879
+ <option name="brand" value="samsung" />
880
+ <option name="codename" value="e3qksx" />
881
+ <option name="id" value="e3qksx" />
882
+ <option name="labId" value="google" />
883
+ <option name="manufacturer" value="Samsung" />
884
+ <option name="name" value="Galaxy S24 Ultra" />
885
+ <option name="screenDensity" value="450" />
886
+ <option name="screenX" value="1440" />
887
+ <option name="screenY" value="3120" />
888
+ </PersistentDeviceSelectionData>
889
+ <PersistentDeviceSelectionData>
890
+ <option name="api" value="33" />
891
+ <option name="brand" value="google" />
892
+ <option name="codename" value="eos" />
893
+ <option name="id" value="eos" />
894
+ <option name="labId" value="google" />
895
+ <option name="manufacturer" value="Google" />
896
+ <option name="name" value="Eos" />
897
+ <option name="screenDensity" value="320" />
898
+ <option name="screenX" value="384" />
899
+ <option name="screenY" value="384" />
900
+ </PersistentDeviceSelectionData>
901
+ <PersistentDeviceSelectionData>
902
+ <option name="api" value="33" />
903
+ <option name="brand" value="google" />
904
+ <option name="codename" value="felix" />
905
+ <option name="id" value="felix" />
906
+ <option name="labId" value="google" />
907
+ <option name="manufacturer" value="Google" />
908
+ <option name="name" value="Pixel Fold" />
909
+ <option name="screenDensity" value="420" />
910
+ <option name="screenX" value="2208" />
911
+ <option name="screenY" value="1840" />
912
+ </PersistentDeviceSelectionData>
913
+ <PersistentDeviceSelectionData>
914
+ <option name="api" value="34" />
915
+ <option name="brand" value="google" />
916
+ <option name="codename" value="felix" />
917
+ <option name="id" value="felix" />
918
+ <option name="labId" value="google" />
919
+ <option name="manufacturer" value="Google" />
920
+ <option name="name" value="Pixel Fold" />
921
+ <option name="screenDensity" value="420" />
922
+ <option name="screenX" value="2208" />
923
+ <option name="screenY" value="1840" />
924
+ </PersistentDeviceSelectionData>
925
+ <PersistentDeviceSelectionData>
926
+ <option name="api" value="36" />
927
+ <option name="brand" value="google" />
928
+ <option name="codename" value="felix" />
929
+ <option name="id" value="felix" />
930
+ <option name="labId" value="google" />
931
+ <option name="manufacturer" value="Google" />
932
+ <option name="name" value="Pixel Fold" />
933
+ <option name="screenDensity" value="420" />
934
+ <option name="screenX" value="2208" />
935
+ <option name="screenY" value="1840" />
936
+ </PersistentDeviceSelectionData>
937
+ <PersistentDeviceSelectionData>
938
+ <option name="api" value="33" />
939
+ <option name="brand" value="google" />
940
+ <option name="codename" value="felix_camera" />
941
+ <option name="id" value="felix_camera" />
942
+ <option name="labId" value="google" />
943
+ <option name="manufacturer" value="Google" />
944
+ <option name="name" value="Pixel Fold (Camera-enabled)" />
945
+ <option name="screenDensity" value="420" />
946
+ <option name="screenX" value="2208" />
947
+ <option name="screenY" value="1840" />
948
+ </PersistentDeviceSelectionData>
949
+ <PersistentDeviceSelectionData>
950
+ <option name="api" value="34" />
951
+ <option name="brand" value="motorola" />
952
+ <option name="codename" value="fogona" />
953
+ <option name="id" value="fogona" />
954
+ <option name="labId" value="google" />
955
+ <option name="manufacturer" value="Motorola" />
956
+ <option name="name" value="moto g play - 2024" />
957
+ <option name="screenDensity" value="280" />
958
+ <option name="screenX" value="720" />
959
+ <option name="screenY" value="1600" />
960
+ </PersistentDeviceSelectionData>
961
+ <PersistentDeviceSelectionData>
962
+ <option name="api" value="34" />
963
+ <option name="brand" value="motorola" />
964
+ <option name="codename" value="fogorow" />
965
+ <option name="id" value="fogorow" />
966
+ <option name="labId" value="google" />
967
+ <option name="manufacturer" value="Motorola" />
968
+ <option name="name" value="moto g24" />
969
+ <option name="screenDensity" value="280" />
970
+ <option name="screenX" value="720" />
971
+ <option name="screenY" value="1612" />
972
+ </PersistentDeviceSelectionData>
973
+ <PersistentDeviceSelectionData>
974
+ <option name="api" value="34" />
975
+ <option name="brand" value="motorola" />
976
+ <option name="codename" value="fogos" />
977
+ <option name="id" value="fogos" />
978
+ <option name="labId" value="google" />
979
+ <option name="manufacturer" value="Motorola" />
980
+ <option name="name" value="moto g34 5G" />
981
+ <option name="screenDensity" value="280" />
982
+ <option name="screenX" value="720" />
983
+ <option name="screenY" value="1600" />
984
+ </PersistentDeviceSelectionData>
985
+ <PersistentDeviceSelectionData>
986
+ <option name="api" value="35" />
987
+ <option name="brand" value="motorola" />
988
+ <option name="codename" value="fogos" />
989
+ <option name="id" value="fogos" />
990
+ <option name="labId" value="google" />
991
+ <option name="manufacturer" value="Motorola" />
992
+ <option name="name" value="moto g34 5G" />
993
+ <option name="screenDensity" value="280" />
994
+ <option name="screenX" value="720" />
995
+ <option name="screenY" value="1600" />
996
+ </PersistentDeviceSelectionData>
997
+ <PersistentDeviceSelectionData>
998
+ <option name="api" value="36" />
999
+ <option name="brand" value="google" />
1000
+ <option name="codename" value="frankel" />
1001
+ <option name="default" value="true" />
1002
+ <option name="id" value="frankel" />
1003
+ <option name="labId" value="google" />
1004
+ <option name="manufacturer" value="Google" />
1005
+ <option name="name" value="Pixel 10" />
1006
+ <option name="screenDensity" value="420" />
1007
+ <option name="screenX" value="1080" />
1008
+ <option name="screenY" value="2424" />
1009
+ <option name="tags">
1010
+ <list>
1011
+ <option value="dda-default" />
1012
+ </list>
1013
+ </option>
1014
+ </PersistentDeviceSelectionData>
1015
+ <PersistentDeviceSelectionData>
1016
+ <option name="api" value="34" />
1017
+ <option name="brand" value="samsung" />
1018
+ <option name="codename" value="g0q" />
1019
+ <option name="id" value="g0q" />
1020
+ <option name="labId" value="google" />
1021
+ <option name="manufacturer" value="Samsung" />
1022
+ <option name="name" value="Galaxy S22+" />
1023
+ <option name="screenDensity" value="450" />
1024
+ <option name="screenX" value="1080" />
1025
+ <option name="screenY" value="2340" />
1026
+ </PersistentDeviceSelectionData>
1027
+ <PersistentDeviceSelectionData>
1028
+ <option name="api" value="36" />
1029
+ <option name="brand" value="samsung" />
1030
+ <option name="codename" value="g0q" />
1031
+ <option name="id" value="g0q" />
1032
+ <option name="labId" value="google" />
1033
+ <option name="manufacturer" value="Samsung" />
1034
+ <option name="name" value="Galaxy S22+" />
1035
+ <option name="screenDensity" value="450" />
1036
+ <option name="screenX" value="1080" />
1037
+ <option name="screenY" value="2340" />
1038
+ </PersistentDeviceSelectionData>
1039
+ <PersistentDeviceSelectionData>
1040
+ <option name="api" value="33" />
1041
+ <option name="brand" value="motorola" />
1042
+ <option name="codename" value="gnevan" />
1043
+ <option name="id" value="gnevan" />
1044
+ <option name="labId" value="google" />
1045
+ <option name="manufacturer" value="Motorola" />
1046
+ <option name="name" value="moto g stylus (2023)" />
1047
+ <option name="screenDensity" value="280" />
1048
+ <option name="screenX" value="720" />
1049
+ <option name="screenY" value="1600" />
1050
+ </PersistentDeviceSelectionData>
1051
+ <PersistentDeviceSelectionData>
1052
+ <option name="api" value="34" />
1053
+ <option name="brand" value="samsung" />
1054
+ <option name="codename" value="gta9pwifi" />
1055
+ <option name="id" value="gta9pwifi" />
1056
+ <option name="labId" value="google" />
1057
+ <option name="manufacturer" value="Samsung" />
1058
+ <option name="name" value="Galaxy Tab A9+" />
1059
+ <option name="screenDensity" value="240" />
1060
+ <option name="screenX" value="1200" />
1061
+ <option name="screenY" value="1920" />
1062
+ </PersistentDeviceSelectionData>
1063
+ <PersistentDeviceSelectionData>
1064
+ <option name="api" value="33" />
1065
+ <option name="brand" value="samsung" />
1066
+ <option name="codename" value="gts7lwifi" />
1067
+ <option name="id" value="gts7lwifi" />
1068
+ <option name="labId" value="google" />
1069
+ <option name="manufacturer" value="Samsung" />
1070
+ <option name="name" value="Galaxy Tab S7" />
1071
+ <option name="screenDensity" value="340" />
1072
+ <option name="screenX" value="1600" />
1073
+ <option name="screenY" value="2560" />
1074
+ </PersistentDeviceSelectionData>
1075
+ <PersistentDeviceSelectionData>
1076
+ <option name="api" value="34" />
1077
+ <option name="brand" value="samsung" />
1078
+ <option name="codename" value="gts7xllite" />
1079
+ <option name="id" value="gts7xllite" />
1080
+ <option name="labId" value="google" />
1081
+ <option name="manufacturer" value="Samsung" />
1082
+ <option name="name" value="Galaxy Tab S7 FE 5G" />
1083
+ <option name="screenDensity" value="340" />
1084
+ <option name="screenX" value="1600" />
1085
+ <option name="screenY" value="2560" />
1086
+ </PersistentDeviceSelectionData>
1087
+ <PersistentDeviceSelectionData>
1088
+ <option name="api" value="33" />
1089
+ <option name="brand" value="samsung" />
1090
+ <option name="codename" value="gts8uwifi" />
1091
+ <option name="formFactor" value="Tablet" />
1092
+ <option name="id" value="gts8uwifi" />
1093
+ <option name="labId" value="google" />
1094
+ <option name="manufacturer" value="Samsung" />
1095
+ <option name="name" value="Galaxy Tab S8 Ultra" />
1096
+ <option name="screenDensity" value="320" />
1097
+ <option name="screenX" value="1848" />
1098
+ <option name="screenY" value="2960" />
1099
+ </PersistentDeviceSelectionData>
1100
+ <PersistentDeviceSelectionData>
1101
+ <option name="api" value="34" />
1102
+ <option name="brand" value="samsung" />
1103
+ <option name="codename" value="gts8wifi" />
1104
+ <option name="formFactor" value="Tablet" />
1105
+ <option name="id" value="gts8wifi" />
1106
+ <option name="labId" value="google" />
1107
+ <option name="manufacturer" value="Samsung" />
1108
+ <option name="name" value="Galaxy Tab S8" />
1109
+ <option name="screenDensity" value="274" />
1110
+ <option name="screenX" value="1600" />
1111
+ <option name="screenY" value="2560" />
1112
+ </PersistentDeviceSelectionData>
1113
+ <PersistentDeviceSelectionData>
1114
+ <option name="api" value="34" />
1115
+ <option name="brand" value="samsung" />
1116
+ <option name="codename" value="gts9fe" />
1117
+ <option name="id" value="gts9fe" />
1118
+ <option name="labId" value="google" />
1119
+ <option name="manufacturer" value="Samsung" />
1120
+ <option name="name" value="Galaxy Tab S9 FE 5G" />
1121
+ <option name="screenDensity" value="280" />
1122
+ <option name="screenX" value="1440" />
1123
+ <option name="screenY" value="2304" />
1124
+ </PersistentDeviceSelectionData>
1125
+ <PersistentDeviceSelectionData>
1126
+ <option name="api" value="34" />
1127
+ <option name="brand" value="samsung" />
1128
+ <option name="codename" value="gts9wifi" />
1129
+ <option name="id" value="gts9wifi" />
1130
+ <option name="labId" value="google" />
1131
+ <option name="manufacturer" value="Samsung" />
1132
+ <option name="name" value="Galaxy Tab S9" />
1133
+ <option name="screenDensity" value="340" />
1134
+ <option name="screenX" value="1600" />
1135
+ <option name="screenY" value="2560" />
1136
+ </PersistentDeviceSelectionData>
1137
+ <PersistentDeviceSelectionData>
1138
+ <option name="api" value="30" />
1139
+ <option name="brand" value="motorola" />
1140
+ <option name="codename" value="guamna" />
1141
+ <option name="id" value="guamna" />
1142
+ <option name="labId" value="google" />
1143
+ <option name="manufacturer" value="Motorola" />
1144
+ <option name="name" value="moto g play (2021)" />
1145
+ <option name="screenDensity" value="280" />
1146
+ <option name="screenX" value="720" />
1147
+ <option name="screenY" value="1600" />
1148
+ </PersistentDeviceSelectionData>
1149
+ <PersistentDeviceSelectionData>
1150
+ <option name="api" value="30" />
1151
+ <option name="brand" value="motorola" />
1152
+ <option name="codename" value="guamp" />
1153
+ <option name="id" value="guamp" />
1154
+ <option name="labId" value="google" />
1155
+ <option name="manufacturer" value="Motorola" />
1156
+ <option name="name" value="moto g(9) play" />
1157
+ <option name="screenDensity" value="280" />
1158
+ <option name="screenX" value="720" />
1159
+ <option name="screenY" value="1600" />
1160
+ </PersistentDeviceSelectionData>
1161
+ <PersistentDeviceSelectionData>
1162
+ <option name="api" value="34" />
1163
+ <option name="brand" value="google" />
1164
+ <option name="codename" value="husky" />
1165
+ <option name="id" value="husky" />
1166
+ <option name="labId" value="google" />
1167
+ <option name="manufacturer" value="Google" />
1168
+ <option name="name" value="Pixel 8 Pro" />
1169
+ <option name="screenDensity" value="390" />
1170
+ <option name="screenX" value="1008" />
1171
+ <option name="screenY" value="2244" />
1172
+ </PersistentDeviceSelectionData>
1173
+ <PersistentDeviceSelectionData>
1174
+ <option name="api" value="35" />
1175
+ <option name="brand" value="google" />
1176
+ <option name="codename" value="husky" />
1177
+ <option name="id" value="husky" />
1178
+ <option name="labId" value="google" />
1179
+ <option name="manufacturer" value="Google" />
1180
+ <option name="name" value="Pixel 8 Pro" />
1181
+ <option name="screenDensity" value="390" />
1182
+ <option name="screenX" value="1008" />
1183
+ <option name="screenY" value="2244" />
1184
+ </PersistentDeviceSelectionData>
1185
+ <PersistentDeviceSelectionData>
1186
+ <option name="api" value="30" />
1187
+ <option name="brand" value="motorola" />
1188
+ <option name="codename" value="java" />
1189
+ <option name="id" value="java" />
1190
+ <option name="labId" value="google" />
1191
+ <option name="manufacturer" value="Motorola" />
1192
+ <option name="name" value="G20" />
1193
+ <option name="screenDensity" value="280" />
1194
+ <option name="screenX" value="720" />
1195
+ <option name="screenY" value="1600" />
1196
+ </PersistentDeviceSelectionData>
1197
+ <PersistentDeviceSelectionData>
1198
+ <option name="api" value="35" />
1199
+ <option name="brand" value="motorola" />
1200
+ <option name="codename" value="kansas" />
1201
+ <option name="id" value="kansas" />
1202
+ <option name="labId" value="google" />
1203
+ <option name="manufacturer" value="Motorola" />
1204
+ <option name="name" value="moto g - 2025" />
1205
+ <option name="screenDensity" value="280" />
1206
+ <option name="screenX" value="720" />
1207
+ <option name="screenY" value="1604" />
1208
+ </PersistentDeviceSelectionData>
1209
+ <PersistentDeviceSelectionData>
1210
+ <option name="api" value="34" />
1211
+ <option name="brand" value="google" />
1212
+ <option name="codename" value="komodo" />
1213
+ <option name="id" value="komodo" />
1214
+ <option name="labId" value="google" />
1215
+ <option name="manufacturer" value="Google" />
1216
+ <option name="name" value="Pixel 9 Pro XL" />
1217
+ <option name="screenDensity" value="360" />
1218
+ <option name="screenX" value="1008" />
1219
+ <option name="screenY" value="2244" />
1220
+ </PersistentDeviceSelectionData>
1221
+ <PersistentDeviceSelectionData>
1222
+ <option name="api" value="35" />
1223
+ <option name="brand" value="google" />
1224
+ <option name="codename" value="komodo" />
1225
+ <option name="id" value="komodo" />
1226
+ <option name="labId" value="google" />
1227
+ <option name="manufacturer" value="Google" />
1228
+ <option name="name" value="Pixel 9 Pro XL" />
1229
+ <option name="screenDensity" value="360" />
1230
+ <option name="screenX" value="1008" />
1231
+ <option name="screenY" value="2244" />
1232
+ </PersistentDeviceSelectionData>
1233
+ <PersistentDeviceSelectionData>
1234
+ <option name="api" value="35" />
1235
+ <option name="brand" value="motorola" />
1236
+ <option name="codename" value="lamul" />
1237
+ <option name="id" value="lamul" />
1238
+ <option name="labId" value="google" />
1239
+ <option name="manufacturer" value="Motorola" />
1240
+ <option name="name" value="moto g05" />
1241
+ <option name="screenDensity" value="280" />
1242
+ <option name="screenX" value="720" />
1243
+ <option name="screenY" value="1604" />
1244
+ </PersistentDeviceSelectionData>
1245
+ <PersistentDeviceSelectionData>
1246
+ <option name="api" value="34" />
1247
+ <option name="brand" value="motorola" />
1248
+ <option name="codename" value="lion" />
1249
+ <option name="id" value="lion" />
1250
+ <option name="labId" value="google" />
1251
+ <option name="manufacturer" value="Motorola" />
1252
+ <option name="name" value="moto g04" />
1253
+ <option name="screenDensity" value="280" />
1254
+ <option name="screenX" value="720" />
1255
+ <option name="screenY" value="1612" />
1256
+ </PersistentDeviceSelectionData>
1257
+ <PersistentDeviceSelectionData>
1258
+ <option name="api" value="33" />
1259
+ <option name="brand" value="google" />
1260
+ <option name="codename" value="lynx" />
1261
+ <option name="id" value="lynx" />
1262
+ <option name="labId" value="google" />
1263
+ <option name="manufacturer" value="Google" />
1264
+ <option name="name" value="Pixel 7a" />
1265
+ <option name="screenDensity" value="420" />
1266
+ <option name="screenX" value="1080" />
1267
+ <option name="screenY" value="2400" />
1268
+ </PersistentDeviceSelectionData>
1269
+ <PersistentDeviceSelectionData>
1270
+ <option name="api" value="34" />
1271
+ <option name="brand" value="motorola" />
1272
+ <option name="codename" value="lyriq" />
1273
+ <option name="id" value="lyriq" />
1274
+ <option name="labId" value="google" />
1275
+ <option name="manufacturer" value="Motorola" />
1276
+ <option name="name" value="edge 40" />
1277
+ <option name="screenDensity" value="400" />
1278
+ <option name="screenX" value="1080" />
1279
+ <option name="screenY" value="2400" />
1280
+ </PersistentDeviceSelectionData>
1281
+ <PersistentDeviceSelectionData>
1282
+ <option name="api" value="36" />
1283
+ <option name="brand" value="samsung" />
1284
+ <option name="codename" value="m1q" />
1285
+ <option name="id" value="m1q" />
1286
+ <option name="labId" value="google" />
1287
+ <option name="manufacturer" value="Samsung" />
1288
+ <option name="name" value="Galaxy S26" />
1289
+ <option name="screenDensity" value="480" />
1290
+ <option name="screenX" value="1080" />
1291
+ <option name="screenY" value="2340" />
1292
+ </PersistentDeviceSelectionData>
1293
+ <PersistentDeviceSelectionData>
1294
+ <option name="api" value="36" />
1295
+ <option name="brand" value="samsung" />
1296
+ <option name="codename" value="m2q" />
1297
+ <option name="id" value="m2q" />
1298
+ <option name="labId" value="google" />
1299
+ <option name="manufacturer" value="Samsung" />
1300
+ <option name="name" value="Galaxy S26+" />
1301
+ <option name="screenDensity" value="450" />
1302
+ <option name="screenX" value="1080" />
1303
+ <option name="screenY" value="2340" />
1304
+ </PersistentDeviceSelectionData>
1305
+ <PersistentDeviceSelectionData>
1306
+ <option name="api" value="34" />
1307
+ <option name="brand" value="motorola" />
1308
+ <option name="codename" value="manaus" />
1309
+ <option name="id" value="manaus" />
1310
+ <option name="labId" value="google" />
1311
+ <option name="manufacturer" value="Motorola" />
1312
+ <option name="name" value="edge 40 neo" />
1313
+ <option name="screenDensity" value="400" />
1314
+ <option name="screenX" value="1080" />
1315
+ <option name="screenY" value="2400" />
1316
+ </PersistentDeviceSelectionData>
1317
+ <PersistentDeviceSelectionData>
1318
+ <option name="api" value="33" />
1319
+ <option name="brand" value="motorola" />
1320
+ <option name="codename" value="maui" />
1321
+ <option name="id" value="maui" />
1322
+ <option name="labId" value="google" />
1323
+ <option name="manufacturer" value="Motorola" />
1324
+ <option name="name" value="moto g play - 2023" />
1325
+ <option name="screenDensity" value="280" />
1326
+ <option name="screenX" value="720" />
1327
+ <option name="screenY" value="1600" />
1328
+ </PersistentDeviceSelectionData>
1329
+ <PersistentDeviceSelectionData>
1330
+ <option name="api" value="36" />
1331
+ <option name="brand" value="google" />
1332
+ <option name="codename" value="mustang" />
1333
+ <option name="default" value="true" />
1334
+ <option name="id" value="mustang" />
1335
+ <option name="labId" value="google" />
1336
+ <option name="manufacturer" value="Google" />
1337
+ <option name="name" value="Pixel 10 Pro XL" />
1338
+ <option name="screenDensity" value="390" />
1339
+ <option name="screenX" value="1080" />
1340
+ <option name="screenY" value="2404" />
1341
+ <option name="tags">
1342
+ <list>
1343
+ <option value="dda-default" />
1344
+ </list>
1345
+ </option>
1346
+ </PersistentDeviceSelectionData>
1347
+ <PersistentDeviceSelectionData>
1348
+ <option name="api" value="34" />
1349
+ <option name="brand" value="samsung" />
1350
+ <option name="codename" value="o1q" />
1351
+ <option name="id" value="o1q" />
1352
+ <option name="labId" value="google" />
1353
+ <option name="manufacturer" value="Samsung" />
1354
+ <option name="name" value="Galaxy S21 5G" />
1355
+ <option name="screenDensity" value="421" />
1356
+ <option name="screenX" value="1080" />
1357
+ <option name="screenY" value="2400" />
1358
+ </PersistentDeviceSelectionData>
1359
+ <PersistentDeviceSelectionData>
1360
+ <option name="api" value="35" />
1361
+ <option name="brand" value="samsung" />
1362
+ <option name="codename" value="o1q" />
1363
+ <option name="id" value="o1q" />
1364
+ <option name="labId" value="google" />
1365
+ <option name="manufacturer" value="Samsung" />
1366
+ <option name="name" value="Galaxy S21 5G" />
1367
+ <option name="screenDensity" value="421" />
1368
+ <option name="screenX" value="1080" />
1369
+ <option name="screenY" value="2400" />
1370
+ </PersistentDeviceSelectionData>
1371
+ <PersistentDeviceSelectionData>
1372
+ <option name="api" value="31" />
1373
+ <option name="brand" value="google" />
1374
+ <option name="codename" value="oriole" />
1375
+ <option name="id" value="oriole" />
1376
+ <option name="labId" value="google" />
1377
+ <option name="manufacturer" value="Google" />
1378
+ <option name="name" value="Pixel 6" />
1379
+ <option name="screenDensity" value="420" />
1380
+ <option name="screenX" value="1080" />
1381
+ <option name="screenY" value="2400" />
1382
+ </PersistentDeviceSelectionData>
1383
+ <PersistentDeviceSelectionData>
1384
+ <option name="api" value="35" />
1385
+ <option name="brand" value="samsung" />
1386
+ <option name="codename" value="p3q" />
1387
+ <option name="id" value="p3q" />
1388
+ <option name="labId" value="google" />
1389
+ <option name="manufacturer" value="Samsung" />
1390
+ <option name="name" value="Galaxy S21 Ultra 5G" />
1391
+ <option name="screenDensity" value="600" />
1392
+ <option name="screenX" value="1440" />
1393
+ <option name="screenY" value="3200" />
1394
+ </PersistentDeviceSelectionData>
1395
+ <PersistentDeviceSelectionData>
1396
+ <option name="api" value="35" />
1397
+ <option name="brand" value="samsung" />
1398
+ <option name="codename" value="p3s" />
1399
+ <option name="id" value="p3s" />
1400
+ <option name="labId" value="google" />
1401
+ <option name="manufacturer" value="Samsung" />
1402
+ <option name="name" value="Galaxy S21 Ultra 5G" />
1403
+ <option name="screenDensity" value="600" />
1404
+ <option name="screenX" value="1440" />
1405
+ <option name="screenY" value="3200" />
1406
+ </PersistentDeviceSelectionData>
1407
+ <PersistentDeviceSelectionData>
1408
+ <option name="api" value="36" />
1409
+ <option name="brand" value="samsung" />
1410
+ <option name="codename" value="pa2q" />
1411
+ <option name="id" value="pa2q" />
1412
+ <option name="labId" value="google" />
1413
+ <option name="manufacturer" value="Samsung" />
1414
+ <option name="name" value="Galaxy S25+" />
1415
+ <option name="screenDensity" value="450" />
1416
+ <option name="screenX" value="1080" />
1417
+ <option name="screenY" value="2340" />
1418
+ </PersistentDeviceSelectionData>
1419
+ <PersistentDeviceSelectionData>
1420
+ <option name="api" value="36" />
1421
+ <option name="brand" value="samsung" />
1422
+ <option name="codename" value="pa2qxxx" />
1423
+ <option name="id" value="pa2qxxx" />
1424
+ <option name="labId" value="google" />
1425
+ <option name="manufacturer" value="Samsung" />
1426
+ <option name="name" value="Galaxy S25+" />
1427
+ <option name="screenDensity" value="450" />
1428
+ <option name="screenX" value="1080" />
1429
+ <option name="screenY" value="2340" />
1430
+ </PersistentDeviceSelectionData>
1431
+ <PersistentDeviceSelectionData>
1432
+ <option name="api" value="35" />
1433
+ <option name="brand" value="samsung" />
1434
+ <option name="codename" value="pa3q" />
1435
+ <option name="default" value="true" />
1436
+ <option name="id" value="pa3q" />
1437
+ <option name="labId" value="google" />
1438
+ <option name="manufacturer" value="Samsung" />
1439
+ <option name="name" value="Galaxy S25 Ultra" />
1440
+ <option name="screenDensity" value="450" />
1441
+ <option name="screenX" value="1080" />
1442
+ <option name="screenY" value="2340" />
1443
+ <option name="tags">
1444
+ <list>
1445
+ <option value="dda-default" />
1446
+ </list>
1447
+ </option>
1448
+ </PersistentDeviceSelectionData>
1449
+ <PersistentDeviceSelectionData>
1450
+ <option name="api" value="36" />
1451
+ <option name="brand" value="samsung" />
1452
+ <option name="codename" value="pa3q" />
1453
+ <option name="default" value="true" />
1454
+ <option name="id" value="pa3q" />
1455
+ <option name="labId" value="google" />
1456
+ <option name="manufacturer" value="Samsung" />
1457
+ <option name="name" value="Galaxy S25 Ultra" />
1458
+ <option name="screenDensity" value="450" />
1459
+ <option name="screenX" value="1080" />
1460
+ <option name="screenY" value="2340" />
1461
+ <option name="tags">
1462
+ <list>
1463
+ <option value="dda-default" />
1464
+ </list>
1465
+ </option>
1466
+ </PersistentDeviceSelectionData>
1467
+ <PersistentDeviceSelectionData>
1468
+ <option name="api" value="33" />
1469
+ <option name="brand" value="google" />
1470
+ <option name="codename" value="panther" />
1471
+ <option name="id" value="panther" />
1472
+ <option name="labId" value="google" />
1473
+ <option name="manufacturer" value="Google" />
1474
+ <option name="name" value="Pixel 7" />
1475
+ <option name="screenDensity" value="420" />
1476
+ <option name="screenX" value="1080" />
1477
+ <option name="screenY" value="2400" />
1478
+ </PersistentDeviceSelectionData>
1479
+ <PersistentDeviceSelectionData>
1480
+ <option name="api" value="35" />
1481
+ <option name="brand" value="samsung" />
1482
+ <option name="codename" value="psq" />
1483
+ <option name="id" value="psq" />
1484
+ <option name="labId" value="google" />
1485
+ <option name="manufacturer" value="Samsung" />
1486
+ <option name="name" value="Galaxy S25 Edge" />
1487
+ <option name="screenDensity" value="450" />
1488
+ <option name="screenX" value="1440" />
1489
+ <option name="screenY" value="3120" />
1490
+ </PersistentDeviceSelectionData>
1491
+ <PersistentDeviceSelectionData>
1492
+ <option name="api" value="36" />
1493
+ <option name="brand" value="samsung" />
1494
+ <option name="codename" value="q4qksx" />
1495
+ <option name="id" value="q4qksx" />
1496
+ <option name="labId" value="google" />
1497
+ <option name="manufacturer" value="Samsung" />
1498
+ <option name="name" value="Galaxy Z Fold4" />
1499
+ <option name="screenDensity" value="420" />
1500
+ <option name="screenX" value="1812" />
1501
+ <option name="screenY" value="2176" />
1502
+ </PersistentDeviceSelectionData>
1503
+ <PersistentDeviceSelectionData>
1504
+ <option name="api" value="34" />
1505
+ <option name="brand" value="samsung" />
1506
+ <option name="codename" value="q5q" />
1507
+ <option name="id" value="q5q" />
1508
+ <option name="labId" value="google" />
1509
+ <option name="manufacturer" value="Samsung" />
1510
+ <option name="name" value="Galaxy Z Fold5" />
1511
+ <option name="screenDensity" value="420" />
1512
+ <option name="screenX" value="1812" />
1513
+ <option name="screenY" value="2176" />
1514
+ </PersistentDeviceSelectionData>
1515
+ <PersistentDeviceSelectionData>
1516
+ <option name="api" value="36" />
1517
+ <option name="brand" value="samsung" />
1518
+ <option name="codename" value="q5qksx" />
1519
+ <option name="id" value="q5qksx" />
1520
+ <option name="labId" value="google" />
1521
+ <option name="manufacturer" value="Samsung" />
1522
+ <option name="name" value="Galaxy Z Fold5" />
1523
+ <option name="screenDensity" value="420" />
1524
+ <option name="screenX" value="1812" />
1525
+ <option name="screenY" value="2176" />
1526
+ </PersistentDeviceSelectionData>
1527
+ <PersistentDeviceSelectionData>
1528
+ <option name="api" value="34" />
1529
+ <option name="brand" value="samsung" />
1530
+ <option name="codename" value="q6q" />
1531
+ <option name="id" value="q6q" />
1532
+ <option name="labId" value="google" />
1533
+ <option name="manufacturer" value="Samsung" />
1534
+ <option name="name" value="Galaxy Z Fold6" />
1535
+ <option name="screenDensity" value="420" />
1536
+ <option name="screenX" value="1856" />
1537
+ <option name="screenY" value="2160" />
1538
+ </PersistentDeviceSelectionData>
1539
+ <PersistentDeviceSelectionData>
1540
+ <option name="api" value="36" />
1541
+ <option name="brand" value="samsung" />
1542
+ <option name="codename" value="q7mq" />
1543
+ <option name="id" value="q7mq" />
1544
+ <option name="labId" value="google" />
1545
+ <option name="manufacturer" value="Samsung" />
1546
+ <option name="name" value="Galaxy Z TriFold" />
1547
+ <option name="screenDensity" value="320" />
1548
+ <option name="screenX" value="2160" />
1549
+ <option name="screenY" value="1584" />
1550
+ </PersistentDeviceSelectionData>
1551
+ <PersistentDeviceSelectionData>
1552
+ <option name="api" value="36" />
1553
+ <option name="brand" value="samsung" />
1554
+ <option name="codename" value="r0q" />
1555
+ <option name="id" value="r0q" />
1556
+ <option name="labId" value="google" />
1557
+ <option name="manufacturer" value="Samsung" />
1558
+ <option name="name" value="Galaxy S22" />
1559
+ <option name="screenDensity" value="425" />
1560
+ <option name="screenX" value="1080" />
1561
+ <option name="screenY" value="2340" />
1562
+ </PersistentDeviceSelectionData>
1563
+ <PersistentDeviceSelectionData>
1564
+ <option name="api" value="36" />
1565
+ <option name="brand" value="samsung" />
1566
+ <option name="codename" value="r0qcsx" />
1567
+ <option name="id" value="r0qcsx" />
1568
+ <option name="labId" value="google" />
1569
+ <option name="manufacturer" value="Samsung" />
1570
+ <option name="name" value="Galaxy S22" />
1571
+ <option name="screenDensity" value="480" />
1572
+ <option name="screenX" value="1080" />
1573
+ <option name="screenY" value="2340" />
1574
+ </PersistentDeviceSelectionData>
1575
+ <PersistentDeviceSelectionData>
1576
+ <option name="api" value="30" />
1577
+ <option name="brand" value="google" />
1578
+ <option name="codename" value="r11" />
1579
+ <option name="formFactor" value="Wear OS" />
1580
+ <option name="id" value="r11" />
1581
+ <option name="labId" value="google" />
1582
+ <option name="manufacturer" value="Google" />
1583
+ <option name="name" value="Pixel Watch" />
1584
+ <option name="screenDensity" value="320" />
1585
+ <option name="screenX" value="384" />
1586
+ <option name="screenY" value="384" />
1587
+ <option name="type" value="WEAR_OS" />
1588
+ </PersistentDeviceSelectionData>
1589
+ <PersistentDeviceSelectionData>
1590
+ <option name="api" value="34" />
1591
+ <option name="brand" value="samsung" />
1592
+ <option name="codename" value="r11q" />
1593
+ <option name="id" value="r11q" />
1594
+ <option name="labId" value="google" />
1595
+ <option name="manufacturer" value="Samsung" />
1596
+ <option name="name" value="Galaxy S23 FE" />
1597
+ <option name="screenDensity" value="450" />
1598
+ <option name="screenX" value="1080" />
1599
+ <option name="screenY" value="2340" />
1600
+ </PersistentDeviceSelectionData>
1601
+ <PersistentDeviceSelectionData>
1602
+ <option name="api" value="36" />
1603
+ <option name="brand" value="samsung" />
1604
+ <option name="codename" value="r11s" />
1605
+ <option name="id" value="r11s" />
1606
+ <option name="labId" value="google" />
1607
+ <option name="manufacturer" value="Samsung" />
1608
+ <option name="name" value="Galaxy S23 FE" />
1609
+ <option name="screenDensity" value="450" />
1610
+ <option name="screenX" value="1080" />
1611
+ <option name="screenY" value="2340" />
1612
+ </PersistentDeviceSelectionData>
1613
+ <PersistentDeviceSelectionData>
1614
+ <option name="api" value="33" />
1615
+ <option name="brand" value="samsung" />
1616
+ <option name="codename" value="r8q" />
1617
+ <option name="id" value="r8q" />
1618
+ <option name="labId" value="google" />
1619
+ <option name="manufacturer" value="Samsung" />
1620
+ <option name="name" value="Galaxy S20 FE 5G" />
1621
+ <option name="screenDensity" value="480" />
1622
+ <option name="screenX" value="1080" />
1623
+ <option name="screenY" value="2400" />
1624
+ </PersistentDeviceSelectionData>
1625
+ <PersistentDeviceSelectionData>
1626
+ <option name="api" value="33" />
1627
+ <option name="brand" value="samsung" />
1628
+ <option name="codename" value="r8qksx" />
1629
+ <option name="id" value="r8qksx" />
1630
+ <option name="labId" value="google" />
1631
+ <option name="manufacturer" value="Samsung" />
1632
+ <option name="name" value="Galaxy S20 FE 5G" />
1633
+ <option name="screenDensity" value="480" />
1634
+ <option name="screenX" value="1080" />
1635
+ <option name="screenY" value="2400" />
1636
+ </PersistentDeviceSelectionData>
1637
+ <PersistentDeviceSelectionData>
1638
+ <option name="api" value="36" />
1639
+ <option name="brand" value="samsung" />
1640
+ <option name="codename" value="r9q" />
1641
+ <option name="id" value="r9q" />
1642
+ <option name="labId" value="google" />
1643
+ <option name="manufacturer" value="Samsung" />
1644
+ <option name="name" value="Galaxy S21 FE 5G" />
1645
+ <option name="screenDensity" value="480" />
1646
+ <option name="screenX" value="1080" />
1647
+ <option name="screenY" value="2340" />
1648
+ </PersistentDeviceSelectionData>
1649
+ <PersistentDeviceSelectionData>
1650
+ <option name="api" value="36" />
1651
+ <option name="brand" value="samsung" />
1652
+ <option name="codename" value="r9q-SM-G990U" />
1653
+ <option name="id" value="r9q-SM-G990U" />
1654
+ <option name="labId" value="google" />
1655
+ <option name="manufacturer" value="Samsung" />
1656
+ <option name="name" value="Galaxy S21 FE 5G" />
1657
+ <option name="screenDensity" value="480" />
1658
+ <option name="screenX" value="1080" />
1659
+ <option name="screenY" value="2340" />
1660
+ </PersistentDeviceSelectionData>
1661
+ <PersistentDeviceSelectionData>
1662
+ <option name="api" value="36" />
1663
+ <option name="brand" value="google" />
1664
+ <option name="codename" value="rango" />
1665
+ <option name="default" value="true" />
1666
+ <option name="id" value="rango" />
1667
+ <option name="labId" value="google" />
1668
+ <option name="manufacturer" value="Google" />
1669
+ <option name="name" value="Pixel 10 Pro Fold" />
1670
+ <option name="screenDensity" value="390" />
1671
+ <option name="screenX" value="2076" />
1672
+ <option name="screenY" value="2152" />
1673
+ <option name="tags">
1674
+ <list>
1675
+ <option value="dda-default" />
1676
+ </list>
1677
+ </option>
1678
+ </PersistentDeviceSelectionData>
1679
+ <PersistentDeviceSelectionData>
1680
+ <option name="api" value="30" />
1681
+ <option name="brand" value="google" />
1682
+ <option name="codename" value="redfin" />
1683
+ <option name="id" value="redfin" />
1684
+ <option name="labId" value="google" />
1685
+ <option name="manufacturer" value="Google" />
1686
+ <option name="name" value="Pixel 5" />
1687
+ <option name="screenDensity" value="440" />
1688
+ <option name="screenX" value="1080" />
1689
+ <option name="screenY" value="2340" />
1690
+ <option name="tags">
1691
+ <list>
1692
+ <option value="default" />
1693
+ </list>
1694
+ </option>
1695
+ </PersistentDeviceSelectionData>
1696
+ <PersistentDeviceSelectionData>
1697
+ <option name="api" value="34" />
1698
+ <option name="brand" value="google" />
1699
+ <option name="codename" value="shiba" />
1700
+ <option name="id" value="shiba" />
1701
+ <option name="labId" value="google" />
1702
+ <option name="manufacturer" value="Google" />
1703
+ <option name="name" value="Pixel 8" />
1704
+ <option name="screenDensity" value="420" />
1705
+ <option name="screenX" value="1080" />
1706
+ <option name="screenY" value="2400" />
1707
+ </PersistentDeviceSelectionData>
1708
+ <PersistentDeviceSelectionData>
1709
+ <option name="api" value="36" />
1710
+ <option name="brand" value="google" />
1711
+ <option name="codename" value="shiba" />
1712
+ <option name="id" value="shiba" />
1713
+ <option name="labId" value="google" />
1714
+ <option name="manufacturer" value="Google" />
1715
+ <option name="name" value="Pixel 8" />
1716
+ <option name="screenDensity" value="420" />
1717
+ <option name="screenX" value="1080" />
1718
+ <option name="screenY" value="2400" />
1719
+ </PersistentDeviceSelectionData>
1720
+ <PersistentDeviceSelectionData>
1721
+ <option name="api" value="34" />
1722
+ <option name="brand" value="samsung" />
1723
+ <option name="codename" value="t2q" />
1724
+ <option name="id" value="t2q" />
1725
+ <option name="labId" value="google" />
1726
+ <option name="manufacturer" value="Samsung" />
1727
+ <option name="name" value="Galaxy S21+ 5G" />
1728
+ <option name="screenDensity" value="394" />
1729
+ <option name="screenX" value="1080" />
1730
+ <option name="screenY" value="2400" />
1731
+ </PersistentDeviceSelectionData>
1732
+ <PersistentDeviceSelectionData>
1733
+ <option name="api" value="35" />
1734
+ <option name="brand" value="samsung" />
1735
+ <option name="codename" value="t2q" />
1736
+ <option name="id" value="t2q" />
1737
+ <option name="labId" value="google" />
1738
+ <option name="manufacturer" value="Samsung" />
1739
+ <option name="name" value="Galaxy S21+ 5G" />
1740
+ <option name="screenDensity" value="394" />
1741
+ <option name="screenX" value="1080" />
1742
+ <option name="screenY" value="2400" />
1743
+ </PersistentDeviceSelectionData>
1744
+ <PersistentDeviceSelectionData>
1745
+ <option name="api" value="33" />
1746
+ <option name="brand" value="google" />
1747
+ <option name="codename" value="tangorpro" />
1748
+ <option name="formFactor" value="Tablet" />
1749
+ <option name="id" value="tangorpro" />
1750
+ <option name="labId" value="google" />
1751
+ <option name="manufacturer" value="Google" />
1752
+ <option name="name" value="Pixel Tablet" />
1753
+ <option name="screenDensity" value="320" />
1754
+ <option name="screenX" value="1600" />
1755
+ <option name="screenY" value="2560" />
1756
+ </PersistentDeviceSelectionData>
1757
+ <PersistentDeviceSelectionData>
1758
+ <option name="api" value="36" />
1759
+ <option name="brand" value="google" />
1760
+ <option name="codename" value="tangorpro" />
1761
+ <option name="formFactor" value="Tablet" />
1762
+ <option name="id" value="tangorpro" />
1763
+ <option name="labId" value="google" />
1764
+ <option name="manufacturer" value="Google" />
1765
+ <option name="name" value="Pixel Tablet" />
1766
+ <option name="screenDensity" value="320" />
1767
+ <option name="screenX" value="1600" />
1768
+ <option name="screenY" value="2560" />
1769
+ </PersistentDeviceSelectionData>
1770
+ <PersistentDeviceSelectionData>
1771
+ <option name="api" value="35" />
1772
+ <option name="brand" value="google" />
1773
+ <option name="codename" value="tegu" />
1774
+ <option name="id" value="tegu" />
1775
+ <option name="labId" value="google" />
1776
+ <option name="manufacturer" value="Google" />
1777
+ <option name="name" value="Pixel 9a" />
1778
+ <option name="screenDensity" value="420" />
1779
+ <option name="screenX" value="1080" />
1780
+ <option name="screenY" value="2424" />
1781
+ </PersistentDeviceSelectionData>
1782
+ <PersistentDeviceSelectionData>
1783
+ <option name="api" value="34" />
1784
+ <option name="brand" value="google" />
1785
+ <option name="codename" value="tokay" />
1786
+ <option name="id" value="tokay" />
1787
+ <option name="labId" value="google" />
1788
+ <option name="manufacturer" value="Google" />
1789
+ <option name="name" value="Pixel 9" />
1790
+ <option name="screenDensity" value="420" />
1791
+ <option name="screenX" value="1080" />
1792
+ <option name="screenY" value="2424" />
1793
+ </PersistentDeviceSelectionData>
1794
+ <PersistentDeviceSelectionData>
1795
+ <option name="api" value="35" />
1796
+ <option name="brand" value="google" />
1797
+ <option name="codename" value="tokay" />
1798
+ <option name="id" value="tokay" />
1799
+ <option name="labId" value="google" />
1800
+ <option name="manufacturer" value="Google" />
1801
+ <option name="name" value="Pixel 9" />
1802
+ <option name="screenDensity" value="420" />
1803
+ <option name="screenX" value="1080" />
1804
+ <option name="screenY" value="2424" />
1805
+ </PersistentDeviceSelectionData>
1806
+ <PersistentDeviceSelectionData>
1807
+ <option name="api" value="36" />
1808
+ <option name="brand" value="google" />
1809
+ <option name="codename" value="tokay" />
1810
+ <option name="id" value="tokay" />
1811
+ <option name="labId" value="google" />
1812
+ <option name="manufacturer" value="Google" />
1813
+ <option name="name" value="Pixel 9" />
1814
+ <option name="screenDensity" value="420" />
1815
+ <option name="screenX" value="1080" />
1816
+ <option name="screenY" value="2424" />
1817
+ </PersistentDeviceSelectionData>
1818
+ <PersistentDeviceSelectionData>
1819
+ <option name="api" value="34" />
1820
+ <option name="brand" value="samsung" />
1821
+ <option name="codename" value="xcover7" />
1822
+ <option name="id" value="xcover7" />
1823
+ <option name="labId" value="google" />
1824
+ <option name="manufacturer" value="Samsung" />
1825
+ <option name="name" value="Galaxy XCover7" />
1826
+ <option name="screenDensity" value="450" />
1827
+ <option name="screenX" value="1080" />
1828
+ <option name="screenY" value="2408" />
1829
+ </PersistentDeviceSelectionData>
1830
+ <PersistentDeviceSelectionData>
1831
+ <option name="api" value="33" />
1832
+ <option name="brand" value="samsung" />
1833
+ <option name="codename" value="y2q" />
1834
+ <option name="id" value="y2q" />
1835
+ <option name="labId" value="google" />
1836
+ <option name="manufacturer" value="Samsung" />
1837
+ <option name="name" value="Galaxy S20+ 5G" />
1838
+ <option name="screenDensity" value="600" />
1839
+ <option name="screenX" value="1440" />
1840
+ <option name="screenY" value="3200" />
1841
+ </PersistentDeviceSelectionData>
1842
+ <PersistentDeviceSelectionData>
1843
+ <option name="api" value="33" />
1844
+ <option name="brand" value="samsung" />
1845
+ <option name="codename" value="z3q" />
1846
+ <option name="id" value="z3q" />
1847
+ <option name="labId" value="google" />
1848
+ <option name="manufacturer" value="Samsung" />
1849
+ <option name="name" value="Galaxy S20 Ultra 5G" />
1850
+ <option name="screenDensity" value="560" />
1851
+ <option name="screenX" value="1440" />
1852
+ <option name="screenY" value="3200" />
1853
+ </PersistentDeviceSelectionData>
1854
+ </list>
1855
+ </option>
1856
+ </component>
1857
+ </project>
.idea/deviceManager.xml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="DeviceTable">
4
+ <option name="columnSorters">
5
+ <list>
6
+ <ColumnSorterState>
7
+ <option name="column" value="Name" />
8
+ <option name="order" value="ASCENDING" />
9
+ </ColumnSorterState>
10
+ </list>
11
+ </option>
12
+ </component>
13
+ </project>
.idea/language-learner.iml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="JAVA_MODULE" version="4">
3
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
4
+ <exclude-output />
5
+ <content url="file://$MODULE_DIR$">
6
+ <excludeFolder url="file://$MODULE_DIR$/mobile/.dart_tool" />
7
+ <excludeFolder url="file://$MODULE_DIR$/mobile/.pub" />
8
+ <excludeFolder url="file://$MODULE_DIR$/mobile/build" />
9
+ </content>
10
+ <orderEntry type="inheritedJdk" />
11
+ <orderEntry type="sourceFolder" forTests="false" />
12
+ <orderEntry type="library" name="Dart SDK" level="project" />
13
+ <orderEntry type="library" name="Dart Packages" level="project" />
14
+ </component>
15
+ </module>
.idea/libraries/Dart_Packages.xml ADDED
@@ -0,0 +1,972 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <component name="libraryTable">
2
+ <library name="Dart Packages" type="DartPackagesLibraryType">
3
+ <properties>
4
+ <option name="packageNameToDirsMap">
5
+ <entry key="archive">
6
+ <value>
7
+ <list>
8
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/archive-4.0.9/lib" />
9
+ </list>
10
+ </value>
11
+ </entry>
12
+ <entry key="args">
13
+ <value>
14
+ <list>
15
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/args-2.7.0/lib" />
16
+ </list>
17
+ </value>
18
+ </entry>
19
+ <entry key="async">
20
+ <value>
21
+ <list>
22
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/async-2.13.1/lib" />
23
+ </list>
24
+ </value>
25
+ </entry>
26
+ <entry key="boolean_selector">
27
+ <value>
28
+ <list>
29
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/boolean_selector-2.1.2/lib" />
30
+ </list>
31
+ </value>
32
+ </entry>
33
+ <entry key="characters">
34
+ <value>
35
+ <list>
36
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/characters-1.4.1/lib" />
37
+ </list>
38
+ </value>
39
+ </entry>
40
+ <entry key="checked_yaml">
41
+ <value>
42
+ <list>
43
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/checked_yaml-2.0.4/lib" />
44
+ </list>
45
+ </value>
46
+ </entry>
47
+ <entry key="cli_util">
48
+ <value>
49
+ <list>
50
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/cli_util-0.4.2/lib" />
51
+ </list>
52
+ </value>
53
+ </entry>
54
+ <entry key="clock">
55
+ <value>
56
+ <list>
57
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/clock-1.1.2/lib" />
58
+ </list>
59
+ </value>
60
+ </entry>
61
+ <entry key="code_assets">
62
+ <value>
63
+ <list>
64
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/code_assets-1.0.0/lib" />
65
+ </list>
66
+ </value>
67
+ </entry>
68
+ <entry key="collection">
69
+ <value>
70
+ <list>
71
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/collection-1.19.1/lib" />
72
+ </list>
73
+ </value>
74
+ </entry>
75
+ <entry key="convert">
76
+ <value>
77
+ <list>
78
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/convert-3.1.2/lib" />
79
+ </list>
80
+ </value>
81
+ </entry>
82
+ <entry key="country_flags">
83
+ <value>
84
+ <list>
85
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/country_flags-4.1.2/lib" />
86
+ </list>
87
+ </value>
88
+ </entry>
89
+ <entry key="cross_file">
90
+ <value>
91
+ <list>
92
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/cross_file-0.3.5+2/lib" />
93
+ </list>
94
+ </value>
95
+ </entry>
96
+ <entry key="crypto">
97
+ <value>
98
+ <list>
99
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/crypto-3.0.7/lib" />
100
+ </list>
101
+ </value>
102
+ </entry>
103
+ <entry key="csslib">
104
+ <value>
105
+ <list>
106
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/csslib-1.0.2/lib" />
107
+ </list>
108
+ </value>
109
+ </entry>
110
+ <entry key="cupertino_icons">
111
+ <value>
112
+ <list>
113
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/cupertino_icons-1.0.9/lib" />
114
+ </list>
115
+ </value>
116
+ </entry>
117
+ <entry key="desktop_webview_window">
118
+ <value>
119
+ <list>
120
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/desktop_webview_window-0.2.3/lib" />
121
+ </list>
122
+ </value>
123
+ </entry>
124
+ <entry key="dio">
125
+ <value>
126
+ <list>
127
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/dio-5.9.2/lib" />
128
+ </list>
129
+ </value>
130
+ </entry>
131
+ <entry key="dio_web_adapter">
132
+ <value>
133
+ <list>
134
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/dio_web_adapter-2.1.2/lib" />
135
+ </list>
136
+ </value>
137
+ </entry>
138
+ <entry key="fake_async">
139
+ <value>
140
+ <list>
141
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/fake_async-1.3.3/lib" />
142
+ </list>
143
+ </value>
144
+ </entry>
145
+ <entry key="ffi">
146
+ <value>
147
+ <list>
148
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/ffi-2.2.0/lib" />
149
+ </list>
150
+ </value>
151
+ </entry>
152
+ <entry key="file">
153
+ <value>
154
+ <list>
155
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/file-7.0.1/lib" />
156
+ </list>
157
+ </value>
158
+ </entry>
159
+ <entry key="file_selector_linux">
160
+ <value>
161
+ <list>
162
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/file_selector_linux-0.9.4/lib" />
163
+ </list>
164
+ </value>
165
+ </entry>
166
+ <entry key="file_selector_macos">
167
+ <value>
168
+ <list>
169
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/file_selector_macos-0.9.5/lib" />
170
+ </list>
171
+ </value>
172
+ </entry>
173
+ <entry key="file_selector_platform_interface">
174
+ <value>
175
+ <list>
176
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/file_selector_platform_interface-2.7.0/lib" />
177
+ </list>
178
+ </value>
179
+ </entry>
180
+ <entry key="file_selector_windows">
181
+ <value>
182
+ <list>
183
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/file_selector_windows-0.9.3+5/lib" />
184
+ </list>
185
+ </value>
186
+ </entry>
187
+ <entry key="flutter">
188
+ <value>
189
+ <list>
190
+ <option value="$PROJECT_DIR$/../flutter/packages/flutter/lib" />
191
+ </list>
192
+ </value>
193
+ </entry>
194
+ <entry key="flutter_launcher_icons">
195
+ <value>
196
+ <list>
197
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_launcher_icons-0.14.4/lib" />
198
+ </list>
199
+ </value>
200
+ </entry>
201
+ <entry key="flutter_lints">
202
+ <value>
203
+ <list>
204
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_lints-6.0.0/lib" />
205
+ </list>
206
+ </value>
207
+ </entry>
208
+ <entry key="flutter_plugin_android_lifecycle">
209
+ <value>
210
+ <list>
211
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.34/lib" />
212
+ </list>
213
+ </value>
214
+ </entry>
215
+ <entry key="flutter_riverpod">
216
+ <value>
217
+ <list>
218
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_riverpod-2.6.1/lib" />
219
+ </list>
220
+ </value>
221
+ </entry>
222
+ <entry key="flutter_secure_storage">
223
+ <value>
224
+ <list>
225
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_secure_storage-9.2.4/lib" />
226
+ </list>
227
+ </value>
228
+ </entry>
229
+ <entry key="flutter_secure_storage_linux">
230
+ <value>
231
+ <list>
232
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_secure_storage_linux-1.2.3/lib" />
233
+ </list>
234
+ </value>
235
+ </entry>
236
+ <entry key="flutter_secure_storage_macos">
237
+ <value>
238
+ <list>
239
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_secure_storage_macos-3.1.3/lib" />
240
+ </list>
241
+ </value>
242
+ </entry>
243
+ <entry key="flutter_secure_storage_platform_interface">
244
+ <value>
245
+ <list>
246
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_secure_storage_platform_interface-1.1.2/lib" />
247
+ </list>
248
+ </value>
249
+ </entry>
250
+ <entry key="flutter_secure_storage_web">
251
+ <value>
252
+ <list>
253
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_secure_storage_web-1.2.1/lib" />
254
+ </list>
255
+ </value>
256
+ </entry>
257
+ <entry key="flutter_secure_storage_windows">
258
+ <value>
259
+ <list>
260
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_secure_storage_windows-3.1.2/lib" />
261
+ </list>
262
+ </value>
263
+ </entry>
264
+ <entry key="flutter_test">
265
+ <value>
266
+ <list>
267
+ <option value="$PROJECT_DIR$/../flutter/packages/flutter_test/lib" />
268
+ </list>
269
+ </value>
270
+ </entry>
271
+ <entry key="flutter_tts">
272
+ <value>
273
+ <list>
274
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_tts-4.2.5/lib" />
275
+ </list>
276
+ </value>
277
+ </entry>
278
+ <entry key="flutter_web_auth_2">
279
+ <value>
280
+ <list>
281
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_web_auth_2-4.1.0/lib" />
282
+ </list>
283
+ </value>
284
+ </entry>
285
+ <entry key="flutter_web_auth_2_platform_interface">
286
+ <value>
287
+ <list>
288
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_web_auth_2_platform_interface-4.1.0/lib" />
289
+ </list>
290
+ </value>
291
+ </entry>
292
+ <entry key="flutter_web_plugins">
293
+ <value>
294
+ <list>
295
+ <option value="$PROJECT_DIR$/../flutter/packages/flutter_web_plugins/lib" />
296
+ </list>
297
+ </value>
298
+ </entry>
299
+ <entry key="glob">
300
+ <value>
301
+ <list>
302
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/glob-2.1.3/lib" />
303
+ </list>
304
+ </value>
305
+ </entry>
306
+ <entry key="go_router">
307
+ <value>
308
+ <list>
309
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/go_router-14.8.1/lib" />
310
+ </list>
311
+ </value>
312
+ </entry>
313
+ <entry key="google_fonts">
314
+ <value>
315
+ <list>
316
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/google_fonts-6.3.3/lib" />
317
+ </list>
318
+ </value>
319
+ </entry>
320
+ <entry key="hooks">
321
+ <value>
322
+ <list>
323
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/hooks-1.0.3/lib" />
324
+ </list>
325
+ </value>
326
+ </entry>
327
+ <entry key="html">
328
+ <value>
329
+ <list>
330
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/html-0.15.6/lib" />
331
+ </list>
332
+ </value>
333
+ </entry>
334
+ <entry key="http">
335
+ <value>
336
+ <list>
337
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/http-1.6.0/lib" />
338
+ </list>
339
+ </value>
340
+ </entry>
341
+ <entry key="http_parser">
342
+ <value>
343
+ <list>
344
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/http_parser-4.1.2/lib" />
345
+ </list>
346
+ </value>
347
+ </entry>
348
+ <entry key="image">
349
+ <value>
350
+ <list>
351
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/image-4.8.0/lib" />
352
+ </list>
353
+ </value>
354
+ </entry>
355
+ <entry key="image_picker">
356
+ <value>
357
+ <list>
358
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/image_picker-1.2.1/lib" />
359
+ </list>
360
+ </value>
361
+ </entry>
362
+ <entry key="image_picker_android">
363
+ <value>
364
+ <list>
365
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/image_picker_android-0.8.13+16/lib" />
366
+ </list>
367
+ </value>
368
+ </entry>
369
+ <entry key="image_picker_for_web">
370
+ <value>
371
+ <list>
372
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/image_picker_for_web-3.1.1/lib" />
373
+ </list>
374
+ </value>
375
+ </entry>
376
+ <entry key="image_picker_ios">
377
+ <value>
378
+ <list>
379
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/image_picker_ios-0.8.13+6/lib" />
380
+ </list>
381
+ </value>
382
+ </entry>
383
+ <entry key="image_picker_linux">
384
+ <value>
385
+ <list>
386
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/image_picker_linux-0.2.2/lib" />
387
+ </list>
388
+ </value>
389
+ </entry>
390
+ <entry key="image_picker_macos">
391
+ <value>
392
+ <list>
393
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/image_picker_macos-0.2.2+1/lib" />
394
+ </list>
395
+ </value>
396
+ </entry>
397
+ <entry key="image_picker_platform_interface">
398
+ <value>
399
+ <list>
400
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/image_picker_platform_interface-2.11.1/lib" />
401
+ </list>
402
+ </value>
403
+ </entry>
404
+ <entry key="image_picker_windows">
405
+ <value>
406
+ <list>
407
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/image_picker_windows-0.2.2/lib" />
408
+ </list>
409
+ </value>
410
+ </entry>
411
+ <entry key="jni">
412
+ <value>
413
+ <list>
414
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/jni-1.0.0/lib" />
415
+ </list>
416
+ </value>
417
+ </entry>
418
+ <entry key="jni_flutter">
419
+ <value>
420
+ <list>
421
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/jni_flutter-1.0.1/lib" />
422
+ </list>
423
+ </value>
424
+ </entry>
425
+ <entry key="jovial_misc">
426
+ <value>
427
+ <list>
428
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/jovial_misc-0.10.0/lib" />
429
+ </list>
430
+ </value>
431
+ </entry>
432
+ <entry key="jovial_svg">
433
+ <value>
434
+ <list>
435
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/jovial_svg-1.1.29/lib" />
436
+ </list>
437
+ </value>
438
+ </entry>
439
+ <entry key="js">
440
+ <value>
441
+ <list>
442
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/js-0.6.7/lib" />
443
+ </list>
444
+ </value>
445
+ </entry>
446
+ <entry key="json_annotation">
447
+ <value>
448
+ <list>
449
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/json_annotation-4.12.0/lib" />
450
+ </list>
451
+ </value>
452
+ </entry>
453
+ <entry key="leak_tracker">
454
+ <value>
455
+ <list>
456
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/leak_tracker-11.0.2/lib" />
457
+ </list>
458
+ </value>
459
+ </entry>
460
+ <entry key="leak_tracker_flutter_testing">
461
+ <value>
462
+ <list>
463
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/leak_tracker_flutter_testing-3.0.10/lib" />
464
+ </list>
465
+ </value>
466
+ </entry>
467
+ <entry key="leak_tracker_testing">
468
+ <value>
469
+ <list>
470
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/leak_tracker_testing-3.0.2/lib" />
471
+ </list>
472
+ </value>
473
+ </entry>
474
+ <entry key="lints">
475
+ <value>
476
+ <list>
477
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/lints-6.1.0/lib" />
478
+ </list>
479
+ </value>
480
+ </entry>
481
+ <entry key="logging">
482
+ <value>
483
+ <list>
484
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/logging-1.3.0/lib" />
485
+ </list>
486
+ </value>
487
+ </entry>
488
+ <entry key="matcher">
489
+ <value>
490
+ <list>
491
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/matcher-0.12.19/lib" />
492
+ </list>
493
+ </value>
494
+ </entry>
495
+ <entry key="material_color_utilities">
496
+ <value>
497
+ <list>
498
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/material_color_utilities-0.13.0/lib" />
499
+ </list>
500
+ </value>
501
+ </entry>
502
+ <entry key="meta">
503
+ <value>
504
+ <list>
505
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/meta-1.17.0/lib" />
506
+ </list>
507
+ </value>
508
+ </entry>
509
+ <entry key="mime">
510
+ <value>
511
+ <list>
512
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/mime-2.0.0/lib" />
513
+ </list>
514
+ </value>
515
+ </entry>
516
+ <entry key="native_toolchain_c">
517
+ <value>
518
+ <list>
519
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/native_toolchain_c-0.17.6/lib" />
520
+ </list>
521
+ </value>
522
+ </entry>
523
+ <entry key="objective_c">
524
+ <value>
525
+ <list>
526
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/objective_c-9.3.0/lib" />
527
+ </list>
528
+ </value>
529
+ </entry>
530
+ <entry key="package_config">
531
+ <value>
532
+ <list>
533
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/package_config-2.2.0/lib" />
534
+ </list>
535
+ </value>
536
+ </entry>
537
+ <entry key="path">
538
+ <value>
539
+ <list>
540
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/path-1.9.1/lib" />
541
+ </list>
542
+ </value>
543
+ </entry>
544
+ <entry key="path_provider">
545
+ <value>
546
+ <list>
547
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider-2.1.5/lib" />
548
+ </list>
549
+ </value>
550
+ </entry>
551
+ <entry key="path_provider_android">
552
+ <value>
553
+ <list>
554
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_android-2.3.1/lib" />
555
+ </list>
556
+ </value>
557
+ </entry>
558
+ <entry key="path_provider_foundation">
559
+ <value>
560
+ <list>
561
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_foundation-2.6.0/lib" />
562
+ </list>
563
+ </value>
564
+ </entry>
565
+ <entry key="path_provider_linux">
566
+ <value>
567
+ <list>
568
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_linux-2.2.1/lib" />
569
+ </list>
570
+ </value>
571
+ </entry>
572
+ <entry key="path_provider_platform_interface">
573
+ <value>
574
+ <list>
575
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_platform_interface-2.1.2/lib" />
576
+ </list>
577
+ </value>
578
+ </entry>
579
+ <entry key="path_provider_windows">
580
+ <value>
581
+ <list>
582
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_windows-2.3.0/lib" />
583
+ </list>
584
+ </value>
585
+ </entry>
586
+ <entry key="petitparser">
587
+ <value>
588
+ <list>
589
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/petitparser-7.0.2/lib" />
590
+ </list>
591
+ </value>
592
+ </entry>
593
+ <entry key="platform">
594
+ <value>
595
+ <list>
596
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/platform-3.1.6/lib" />
597
+ </list>
598
+ </value>
599
+ </entry>
600
+ <entry key="plugin_platform_interface">
601
+ <value>
602
+ <list>
603
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/plugin_platform_interface-2.1.8/lib" />
604
+ </list>
605
+ </value>
606
+ </entry>
607
+ <entry key="posix">
608
+ <value>
609
+ <list>
610
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/posix-6.5.0/lib" />
611
+ </list>
612
+ </value>
613
+ </entry>
614
+ <entry key="pub_semver">
615
+ <value>
616
+ <list>
617
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/pub_semver-2.2.0/lib" />
618
+ </list>
619
+ </value>
620
+ </entry>
621
+ <entry key="record_use">
622
+ <value>
623
+ <list>
624
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/record_use-0.6.0/lib" />
625
+ </list>
626
+ </value>
627
+ </entry>
628
+ <entry key="riverpod">
629
+ <value>
630
+ <list>
631
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/riverpod-2.6.1/lib" />
632
+ </list>
633
+ </value>
634
+ </entry>
635
+ <entry key="sky_engine">
636
+ <value>
637
+ <list>
638
+ <option value="$PROJECT_DIR$/../flutter/bin/cache/pkg/sky_engine/lib" />
639
+ </list>
640
+ </value>
641
+ </entry>
642
+ <entry key="source_span">
643
+ <value>
644
+ <list>
645
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/source_span-1.10.2/lib" />
646
+ </list>
647
+ </value>
648
+ </entry>
649
+ <entry key="stack_trace">
650
+ <value>
651
+ <list>
652
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/stack_trace-1.12.1/lib" />
653
+ </list>
654
+ </value>
655
+ </entry>
656
+ <entry key="state_notifier">
657
+ <value>
658
+ <list>
659
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/state_notifier-1.0.0/lib" />
660
+ </list>
661
+ </value>
662
+ </entry>
663
+ <entry key="stream_channel">
664
+ <value>
665
+ <list>
666
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/stream_channel-2.1.4/lib" />
667
+ </list>
668
+ </value>
669
+ </entry>
670
+ <entry key="string_scanner">
671
+ <value>
672
+ <list>
673
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/string_scanner-1.4.1/lib" />
674
+ </list>
675
+ </value>
676
+ </entry>
677
+ <entry key="term_glyph">
678
+ <value>
679
+ <list>
680
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/term_glyph-1.2.2/lib" />
681
+ </list>
682
+ </value>
683
+ </entry>
684
+ <entry key="test_api">
685
+ <value>
686
+ <list>
687
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/test_api-0.7.10/lib" />
688
+ </list>
689
+ </value>
690
+ </entry>
691
+ <entry key="typed_data">
692
+ <value>
693
+ <list>
694
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/typed_data-1.4.0/lib" />
695
+ </list>
696
+ </value>
697
+ </entry>
698
+ <entry key="url_launcher">
699
+ <value>
700
+ <list>
701
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/url_launcher-6.3.2/lib" />
702
+ </list>
703
+ </value>
704
+ </entry>
705
+ <entry key="url_launcher_android">
706
+ <value>
707
+ <list>
708
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/url_launcher_android-6.3.29/lib" />
709
+ </list>
710
+ </value>
711
+ </entry>
712
+ <entry key="url_launcher_ios">
713
+ <value>
714
+ <list>
715
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/url_launcher_ios-6.4.1/lib" />
716
+ </list>
717
+ </value>
718
+ </entry>
719
+ <entry key="url_launcher_linux">
720
+ <value>
721
+ <list>
722
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/url_launcher_linux-3.2.2/lib" />
723
+ </list>
724
+ </value>
725
+ </entry>
726
+ <entry key="url_launcher_macos">
727
+ <value>
728
+ <list>
729
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/url_launcher_macos-3.2.5/lib" />
730
+ </list>
731
+ </value>
732
+ </entry>
733
+ <entry key="url_launcher_platform_interface">
734
+ <value>
735
+ <list>
736
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/url_launcher_platform_interface-2.3.2/lib" />
737
+ </list>
738
+ </value>
739
+ </entry>
740
+ <entry key="url_launcher_web">
741
+ <value>
742
+ <list>
743
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/url_launcher_web-2.4.2/lib" />
744
+ </list>
745
+ </value>
746
+ </entry>
747
+ <entry key="url_launcher_windows">
748
+ <value>
749
+ <list>
750
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/url_launcher_windows-3.1.5/lib" />
751
+ </list>
752
+ </value>
753
+ </entry>
754
+ <entry key="vector_math">
755
+ <value>
756
+ <list>
757
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/vector_math-2.2.0/lib" />
758
+ </list>
759
+ </value>
760
+ </entry>
761
+ <entry key="video_player">
762
+ <value>
763
+ <list>
764
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/video_player-2.11.1/lib" />
765
+ </list>
766
+ </value>
767
+ </entry>
768
+ <entry key="video_player_android">
769
+ <value>
770
+ <list>
771
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/video_player_android-2.9.5/lib" />
772
+ </list>
773
+ </value>
774
+ </entry>
775
+ <entry key="video_player_avfoundation">
776
+ <value>
777
+ <list>
778
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/video_player_avfoundation-2.9.6/lib" />
779
+ </list>
780
+ </value>
781
+ </entry>
782
+ <entry key="video_player_platform_interface">
783
+ <value>
784
+ <list>
785
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/video_player_platform_interface-6.7.0/lib" />
786
+ </list>
787
+ </value>
788
+ </entry>
789
+ <entry key="video_player_web">
790
+ <value>
791
+ <list>
792
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/video_player_web-2.4.0/lib" />
793
+ </list>
794
+ </value>
795
+ </entry>
796
+ <entry key="vm_service">
797
+ <value>
798
+ <list>
799
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/vm_service-15.1.0/lib" />
800
+ </list>
801
+ </value>
802
+ </entry>
803
+ <entry key="web">
804
+ <value>
805
+ <list>
806
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/web-1.1.1/lib" />
807
+ </list>
808
+ </value>
809
+ </entry>
810
+ <entry key="win32">
811
+ <value>
812
+ <list>
813
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/win32-5.15.0/lib" />
814
+ </list>
815
+ </value>
816
+ </entry>
817
+ <entry key="window_to_front">
818
+ <value>
819
+ <list>
820
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/window_to_front-0.0.3/lib" />
821
+ </list>
822
+ </value>
823
+ </entry>
824
+ <entry key="xdg_directories">
825
+ <value>
826
+ <list>
827
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/xdg_directories-1.1.0/lib" />
828
+ </list>
829
+ </value>
830
+ </entry>
831
+ <entry key="xml">
832
+ <value>
833
+ <list>
834
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/xml-6.6.1/lib" />
835
+ </list>
836
+ </value>
837
+ </entry>
838
+ <entry key="yaml">
839
+ <value>
840
+ <list>
841
+ <option value="$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/yaml-3.1.3/lib" />
842
+ </list>
843
+ </value>
844
+ </entry>
845
+ </option>
846
+ </properties>
847
+ <CLASSES>
848
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/archive-4.0.9/lib" />
849
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/args-2.7.0/lib" />
850
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/async-2.13.1/lib" />
851
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/boolean_selector-2.1.2/lib" />
852
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/characters-1.4.1/lib" />
853
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/checked_yaml-2.0.4/lib" />
854
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/cli_util-0.4.2/lib" />
855
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/clock-1.1.2/lib" />
856
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/code_assets-1.0.0/lib" />
857
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/collection-1.19.1/lib" />
858
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/convert-3.1.2/lib" />
859
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/country_flags-4.1.2/lib" />
860
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/cross_file-0.3.5+2/lib" />
861
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/crypto-3.0.7/lib" />
862
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/csslib-1.0.2/lib" />
863
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/cupertino_icons-1.0.9/lib" />
864
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/desktop_webview_window-0.2.3/lib" />
865
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/dio-5.9.2/lib" />
866
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/dio_web_adapter-2.1.2/lib" />
867
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/fake_async-1.3.3/lib" />
868
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/ffi-2.2.0/lib" />
869
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/file-7.0.1/lib" />
870
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/file_selector_linux-0.9.4/lib" />
871
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/file_selector_macos-0.9.5/lib" />
872
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/file_selector_platform_interface-2.7.0/lib" />
873
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/file_selector_windows-0.9.3+5/lib" />
874
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_launcher_icons-0.14.4/lib" />
875
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_lints-6.0.0/lib" />
876
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.34/lib" />
877
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_riverpod-2.6.1/lib" />
878
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_secure_storage-9.2.4/lib" />
879
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_secure_storage_linux-1.2.3/lib" />
880
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_secure_storage_macos-3.1.3/lib" />
881
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_secure_storage_platform_interface-1.1.2/lib" />
882
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_secure_storage_web-1.2.1/lib" />
883
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_secure_storage_windows-3.1.2/lib" />
884
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_tts-4.2.5/lib" />
885
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_web_auth_2-4.1.0/lib" />
886
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/flutter_web_auth_2_platform_interface-4.1.0/lib" />
887
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/glob-2.1.3/lib" />
888
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/go_router-14.8.1/lib" />
889
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/google_fonts-6.3.3/lib" />
890
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/hooks-1.0.3/lib" />
891
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/html-0.15.6/lib" />
892
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/http-1.6.0/lib" />
893
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/http_parser-4.1.2/lib" />
894
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/image-4.8.0/lib" />
895
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/image_picker-1.2.1/lib" />
896
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/image_picker_android-0.8.13+16/lib" />
897
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/image_picker_for_web-3.1.1/lib" />
898
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/image_picker_ios-0.8.13+6/lib" />
899
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/image_picker_linux-0.2.2/lib" />
900
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/image_picker_macos-0.2.2+1/lib" />
901
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/image_picker_platform_interface-2.11.1/lib" />
902
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/image_picker_windows-0.2.2/lib" />
903
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/jni-1.0.0/lib" />
904
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/jni_flutter-1.0.1/lib" />
905
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/jovial_misc-0.10.0/lib" />
906
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/jovial_svg-1.1.29/lib" />
907
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/js-0.6.7/lib" />
908
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/json_annotation-4.12.0/lib" />
909
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/leak_tracker-11.0.2/lib" />
910
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/leak_tracker_flutter_testing-3.0.10/lib" />
911
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/leak_tracker_testing-3.0.2/lib" />
912
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/lints-6.1.0/lib" />
913
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/logging-1.3.0/lib" />
914
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/matcher-0.12.19/lib" />
915
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/material_color_utilities-0.13.0/lib" />
916
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/meta-1.17.0/lib" />
917
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/mime-2.0.0/lib" />
918
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/native_toolchain_c-0.17.6/lib" />
919
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/objective_c-9.3.0/lib" />
920
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/package_config-2.2.0/lib" />
921
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/path-1.9.1/lib" />
922
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider-2.1.5/lib" />
923
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_android-2.3.1/lib" />
924
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_foundation-2.6.0/lib" />
925
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_linux-2.2.1/lib" />
926
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_platform_interface-2.1.2/lib" />
927
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_windows-2.3.0/lib" />
928
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/petitparser-7.0.2/lib" />
929
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/platform-3.1.6/lib" />
930
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/plugin_platform_interface-2.1.8/lib" />
931
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/posix-6.5.0/lib" />
932
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/pub_semver-2.2.0/lib" />
933
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/record_use-0.6.0/lib" />
934
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/riverpod-2.6.1/lib" />
935
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/source_span-1.10.2/lib" />
936
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/stack_trace-1.12.1/lib" />
937
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/state_notifier-1.0.0/lib" />
938
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/stream_channel-2.1.4/lib" />
939
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/string_scanner-1.4.1/lib" />
940
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/term_glyph-1.2.2/lib" />
941
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/test_api-0.7.10/lib" />
942
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/typed_data-1.4.0/lib" />
943
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/url_launcher-6.3.2/lib" />
944
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/url_launcher_android-6.3.29/lib" />
945
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/url_launcher_ios-6.4.1/lib" />
946
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/url_launcher_linux-3.2.2/lib" />
947
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/url_launcher_macos-3.2.5/lib" />
948
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/url_launcher_platform_interface-2.3.2/lib" />
949
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/url_launcher_web-2.4.2/lib" />
950
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/url_launcher_windows-3.1.5/lib" />
951
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/vector_math-2.2.0/lib" />
952
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/video_player-2.11.1/lib" />
953
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/video_player_android-2.9.5/lib" />
954
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/video_player_avfoundation-2.9.6/lib" />
955
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/video_player_platform_interface-6.7.0/lib" />
956
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/video_player_web-2.4.0/lib" />
957
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/vm_service-15.1.0/lib" />
958
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/web-1.1.1/lib" />
959
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/win32-5.15.0/lib" />
960
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/window_to_front-0.0.3/lib" />
961
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/xdg_directories-1.1.0/lib" />
962
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/xml-6.6.1/lib" />
963
+ <root url="file://$USER_HOME$/AppData/Local/Pub/Cache/hosted/pub.dev/yaml-3.1.3/lib" />
964
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/pkg/sky_engine/lib" />
965
+ <root url="file://$PROJECT_DIR$/../flutter/packages/flutter/lib" />
966
+ <root url="file://$PROJECT_DIR$/../flutter/packages/flutter_test/lib" />
967
+ <root url="file://$PROJECT_DIR$/../flutter/packages/flutter_web_plugins/lib" />
968
+ </CLASSES>
969
+ <JAVADOC />
970
+ <SOURCES />
971
+ </library>
972
+ </component>
.idea/libraries/Dart_SDK.xml ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <component name="libraryTable">
2
+ <library name="Dart SDK">
3
+ <CLASSES>
4
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/_internal" />
5
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/async" />
6
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/cli" />
7
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/collection" />
8
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/concurrent" />
9
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/convert" />
10
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/core" />
11
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/developer" />
12
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/ffi" />
13
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/html" />
14
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/indexed_db" />
15
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/io" />
16
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/isolate" />
17
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/js" />
18
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/js_interop" />
19
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/js_interop_unsafe" />
20
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/js_util" />
21
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/math" />
22
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/mirrors" />
23
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/svg" />
24
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/typed_data" />
25
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/web_audio" />
26
+ <root url="file://$PROJECT_DIR$/../flutter/bin/cache/dart-sdk/lib/web_gl" />
27
+ </CLASSES>
28
+ <JAVADOC />
29
+ <SOURCES />
30
+ </library>
31
+ </component>
.idea/markdown.xml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="MarkdownSettings">
4
+ <option name="previewPanelProviderInfo">
5
+ <ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
6
+ </option>
7
+ </component>
8
+ </project>
.idea/misc.xml ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ <project version="4">
2
+ <component name="ProjectRootManager" version="2">
3
+ <output url="file://$PROJECT_DIR$/out" />
4
+ </component>
5
+ </project>
.idea/modules.xml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/language-learner.iml" filepath="$PROJECT_DIR$/.idea/language-learner.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ </component>
6
+ </project>
.mcp.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "mcpServers": {
3
+ "shadcn": {
4
+ "command": "npx",
5
+ "args": [
6
+ "shadcn@latest",
7
+ "mcp"
8
+ ]
9
+ }
10
+ }
11
+ }
AGENTS.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ <!-- BEGIN:nextjs-agent-rules -->
2
+ # This is NOT the Next.js you know
3
+
4
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
5
+ <!-- END:nextjs-agent-rules -->
CLAUDE.md ADDED
@@ -0,0 +1 @@
 
 
1
+ @AGENTS.md
Dockerfile ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax=docker/dockerfile:1.7
2
+
3
+ # ---- deps ----
4
+ FROM node:20-alpine AS deps
5
+ WORKDIR /app
6
+ RUN apk add --no-cache libc6-compat
7
+ COPY package.json package-lock.json ./
8
+ RUN npm ci --no-audit --no-fund
9
+
10
+ # ---- builder ----
11
+ FROM node:20-alpine AS builder
12
+ WORKDIR /app
13
+ RUN apk add --no-cache libc6-compat
14
+ COPY --from=deps /app/node_modules ./node_modules
15
+ COPY . .
16
+
17
+ # lib/db/client.ts throws at import time if DATABASE_URL is unset, and
18
+ # `next build` evaluates server modules. Provide a placeholder so the build
19
+ # succeeds. Real value (if any) is injected at runtime via HF Space Secrets.
20
+ ENV DATABASE_URL=postgresql://placeholder:placeholder@127.0.0.1:5432/placeholder
21
+ ENV NEXT_TELEMETRY_DISABLED=1
22
+ ENV NODE_ENV=production
23
+
24
+ RUN npm run build
25
+
26
+ # ---- runner ----
27
+ FROM node:20-alpine AS runner
28
+ WORKDIR /app
29
+ RUN apk add --no-cache libc6-compat \
30
+ && addgroup -g 1001 -S nodejs \
31
+ && adduser -u 1001 -S nextjs -G nodejs
32
+
33
+ ENV NODE_ENV=production
34
+ ENV NEXT_TELEMETRY_DISABLED=1
35
+ # HF Spaces routes traffic to $PORT (defaults to 7860). Bind to 0.0.0.0
36
+ # so the container is reachable from outside.
37
+ ENV PORT=7860
38
+ ENV HOSTNAME=0.0.0.0
39
+
40
+ COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
41
+ COPY --from=builder --chown=nextjs:nodejs /app/public ./public
42
+ COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
43
+ COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json
44
+
45
+ USER nextjs
46
+ EXPOSE 7860
47
+
48
+ CMD ["npx", "next", "start", "-H", "0.0.0.0", "-p", "7860"]
README.md ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: PraxaLing
3
+ emoji: 🦜
4
+ colorFrom: yellow
5
+ colorTo: gray
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ hf_oauth: true
10
+ hf_oauth_scopes:
11
+ - email
12
+ - inference-api
13
+ ---
14
+
15
+ # PraxaLing
16
+
17
+ Learn languages through short stories and the world around you, powered by HuggingFace for identity and inference.
18
+
19
+ Two clients, one backend:
20
+ - **Web** — Next.js 16 App Router (also acts as the shared API).
21
+ - **Mobile** — Flutter (latest stable), calls the same HTTP endpoints.
22
+
23
+ Three flows:
24
+ 1. **Sign in with HuggingFace** (OAuth).
25
+ 2. **Practice** — read short stories matched to your CEFR level and target language; tap any word for a translation. Generate new stories on demand.
26
+ 3. **Camera** — take or upload a photo; the backend runs BLIP captioning, DETR detection, NLLB translation, and Mistral-generated example sentences.
27
+
28
+ ## Stack
29
+
30
+ | Concern | Tech |
31
+ |---|---|
32
+ | Web | Next.js 16.2 · React 19 · Tailwind v4 · Turbopack |
33
+ | Mobile | Flutter 3.41 · Riverpod · go_router · dio |
34
+ | Auth | HuggingFace OAuth (`arctic`) · JWT (`jose`) |
35
+ | DB | Postgres · Drizzle ORM |
36
+ | Storage | Vercel Blob (optional) |
37
+ | Inference | `@huggingface/inference` · Mistral-7B-Instruct · BLIP · DETR · NLLB-200 |
38
+
39
+ ## Getting started (web)
40
+
41
+ ```bash
42
+ # 1. Install deps
43
+ npm install
44
+
45
+ # 2. Bring up Postgres
46
+ docker compose up -d
47
+
48
+ # 3. Configure env — copy and fill in
49
+ cp .env.example .env.local
50
+
51
+ # 4. Apply schema
52
+ npm run db:push
53
+
54
+ # 5. (Optional) seed stories
55
+ npm run db:seed
56
+
57
+ # 6. Run dev server
58
+ npm run dev
59
+ ```
60
+
61
+ Required env:
62
+ - `AUTH_SECRET` — any 32+ char random string (`openssl rand -base64 32`)
63
+ - `HF_OAUTH_CLIENT_ID` / `HF_OAUTH_CLIENT_SECRET` — from https://huggingface.co/settings/applications/new, or use the Space metadata `hf_oauth: true` and the app will read Hugging Face's injected `OAUTH_CLIENT_ID` / `OAUTH_CLIENT_SECRET`
64
+ - `HF_TOKEN` — read token from https://huggingface.co/settings/tokens
65
+ - Optional: `BLOB_READ_WRITE_TOKEN` for Vercel Blob
66
+
67
+ Redirect URIs to register on the HF OAuth app:
68
+ - `http://localhost:3000/api/auth/callback/huggingface`
69
+ - `http://localhost:3000/api/auth/callback/huggingface?client=mobile`
70
+ - `https://<SPACE_HOST>/api/auth/callback/huggingface`
71
+ - `https://<SPACE_HOST>/api/auth/callback/huggingface?client=mobile`
72
+
73
+ For the hosted Space, use the direct `*.hf.space` host, not the `huggingface.co/spaces/...` page, for redirect URIs and mobile `API_BASE_URL`.
74
+
75
+ ## Getting started (mobile)
76
+
77
+ ```bash
78
+ cd mobile
79
+ flutter pub get
80
+
81
+ # iOS simulator — host loopback is 127.0.0.1 by default
82
+ flutter run --dart-define=API_BASE_URL=http://127.0.0.1:3000
83
+
84
+ # Android emulator — host loopback is 10.0.2.2 (default)
85
+ flutter run
86
+
87
+ # Hosted backend / physical device
88
+ flutter run --dart-define=API_BASE_URL=https://<SPACE_HOST>
89
+ ```
90
+
91
+ See `docs/API.md` for the endpoints the app consumes.
92
+
93
+ ## Architecture notes
94
+
95
+ - **Auth**: both clients exchange an HF OAuth code for our own short-lived JWT. Web stores it in an httpOnly cookie; mobile in `flutter_secure_storage`. `lib/auth/session.ts#getSession()` handles both.
96
+ - **Next.js 16 specifics**: `middleware.ts` is now `proxy.ts`; dynamic APIs (`cookies`, `headers`, `params`) are async; Turbopack is default. See `node_modules/next/dist/docs/01-app/02-guides/upgrading/version-16.md`.
97
+ - **Data flow for camera**: multipart → BLIP + DETR in parallel → dedupe labels → translate each to target lang → Mistral generates 3 sentences keyed on caption + objects → persisted in `vision_jobs`.
98
+
99
+ ## Scripts
100
+
101
+ | Command | Purpose |
102
+ |---|---|
103
+ | `npm run dev` | Next.js dev server |
104
+ | `npm run build` | Production build |
105
+ | `npm run db:push` | Apply schema to Postgres |
106
+ | `npm run db:generate` | Generate migration from schema |
107
+ | `npm run db:migrate` | Apply pending migrations |
108
+ | `npm run db:seed` | Insert hand-curated stories |
109
+ | `npm run db:studio` | Drizzle Studio UI |
app/(app)/BottomNavLink.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { usePathname } from "next/navigation";
5
+
6
+ export function BottomNavLink({ href, icon, label }: { href: string; icon: React.ReactNode; label: string }) {
7
+ const pathname = usePathname();
8
+ const isActive = pathname === href || pathname.startsWith(`${href}/`);
9
+
10
+ return (
11
+ <Link
12
+ href={href}
13
+ aria-current={isActive ? "page" : undefined}
14
+ className={`flex min-h-14 flex-col items-center justify-center gap-1 rounded-xl text-xs font-black uppercase tracking-tight transition-all sm:flex-row sm:gap-2 sm:text-sm ${
15
+ isActive
16
+ ? "border-2 border-black bg-[#FFD21E] text-black shadow-[2px_2px_0px_rgba(0,0,0,1)]"
17
+ : "text-gray-500 hover:bg-[#FFF7D6] hover:text-black"
18
+ }`}
19
+ >
20
+ {icon}
21
+ {label}
22
+ </Link>
23
+ );
24
+ }
app/(app)/camera/AnalysisResults.tsx ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { motion } from "framer-motion";
4
+ import { Furi } from "@/components/Furi";
5
+ import { TTSButton } from "@/components/TTSButton";
6
+ import type { Analysis } from "./types";
7
+
8
+ const EASE = [0.16, 1, 0.3, 1] as const;
9
+
10
+ export function AnalysisResults({ analysis, lang }: { analysis: Analysis; lang: string }) {
11
+ return (
12
+ <motion.div
13
+ initial={{ opacity: 0, y: 16 }}
14
+ animate={{ opacity: 1, y: 0 }}
15
+ transition={{ duration: 0.5, ease: EASE }}
16
+ className="grid gap-8 lg:grid-cols-2"
17
+ >
18
+ <ResultCard title="Objects detected">
19
+ {analysis.objects.length > 0 ? (
20
+ <ul className="space-y-4">
21
+ {analysis.objects.map((o, i) => (
22
+ <li key={`${o.translation}-row-${i}`} className="rounded-xl bg-white border-2 border-black flex items-center justify-between gap-4 p-4 transition-all shadow-[4px_4px_0px_rgba(0,0,0,1)] hover:translate-x-[-1px] hover:translate-y-[-1px] hover:shadow-[5px_5px_0px_rgba(0,0,0,1)]">
23
+ <div className="flex items-center gap-3 min-w-0">
24
+ <TTSButton text={o.translation} lang={lang} />
25
+ <div>
26
+ <div className={`text-lg font-black text-black ${o.translationSegments ? "has-furi" : ""}`}>
27
+ <Furi text={o.translation} segments={o.translationSegments} />
28
+ </div>
29
+ {o.romanized ? <div className="text-xs font-bold text-[#3B82F6] mt-0.5">{o.romanized}</div> : null}
30
+ <div className="mt-1.5"><span className="text-[10px] font-black uppercase tracking-wider text-black bg-[#E6FBFA] border border-black rounded px-1.5 py-0.5 shadow-[1px_1px_0px_rgba(0,0,0,1)]">{o.label}</span></div>
31
+ </div>
32
+ </div>
33
+ <div className="rounded-lg border-2 border-black bg-[#FFD21E] px-3 py-1 text-xs font-black text-black shadow-[2px_2px_0px_rgba(0,0,0,1)] shrink-0">
34
+ {Math.round(o.score * 100)}%
35
+ </div>
36
+ </li>
37
+ ))}
38
+ </ul>
39
+ ) : (
40
+ <p className="text-sm font-bold text-gray-500">No objects detected. Try a clearer photo.</p>
41
+ )}
42
+ </ResultCard>
43
+
44
+ <ResultCard title="Practice sentences">
45
+ {analysis.sentences.length > 0 ? (
46
+ <ul className="space-y-4">
47
+ {analysis.sentences.map((s, i) => (
48
+ <li key={`${s.target}-${i}`} className="rounded-xl bg-white border-2 border-black p-4 transition-all shadow-[4px_4px_0px_rgba(0,0,0,1)] hover:translate-x-[-1px] hover:translate-y-[-1px] hover:shadow-[5px_5px_0px_rgba(0,0,0,1)]">
49
+ <div className="flex items-start justify-between gap-3">
50
+ <div className={`text-lg font-black text-black ${s.targetSegments ? "has-furi" : ""}`}>
51
+ <Furi text={s.target} segments={s.targetSegments} />
52
+ </div>
53
+ <TTSButton text={s.target} lang={lang} />
54
+ </div>
55
+ {s.romanized ? <div className="mt-1 text-sm font-bold italic text-[#3B82F6]">{s.romanized}</div> : null}
56
+ <div className="mt-3 text-xs font-black text-black bg-[#FFF7D6] p-2.5 rounded-lg border-2 border-black shadow-[2px_2px_0px_rgba(0,0,0,1)]">{s.gloss}</div>
57
+ </li>
58
+ ))}
59
+ </ul>
60
+ ) : (
61
+ <p className="text-sm font-bold text-gray-500">No sentences generated. Retry the photo.</p>
62
+ )}
63
+ </ResultCard>
64
+ </motion.div>
65
+ );
66
+ }
67
+
68
+ function ResultCard({ title, children }: { title: string; children: React.ReactNode }) {
69
+ return (
70
+ <section className="rounded-2xl bg-white p-6 border-3 border-black shadow-[6px_6px_0px_rgba(0,0,0,1)]">
71
+ <h2 className="text-sm font-black uppercase tracking-wider text-black mb-6 flex items-center gap-2">
72
+ <span className="w-2 h-6 bg-[#FFD21E] border-2 border-black rounded-full" />
73
+ {title}
74
+ </h2>
75
+ {children}
76
+ </section>
77
+ );
78
+ }
app/(app)/camera/CameraClient.tsx ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useRef, useState } from "react";
4
+ import { motion } from "framer-motion";
5
+ import type { Level } from "@/lib/languages";
6
+ import { LevelPicker } from "@/components/LevelPicker";
7
+ import { PhotoMarkers } from "./PhotoMarkers";
8
+ import { UploadDropzone } from "./UploadDropzone";
9
+ import { AnalysisResults } from "./AnalysisResults";
10
+ import type { Analysis } from "./types";
11
+ import { Camera, RotateCcw, Wand2, AlertCircle } from "lucide-react";
12
+
13
+ export function CameraClient({ defaultLevel, lang }: { defaultLevel: Level; lang: string }) {
14
+ const [previewUrl, setPreviewUrl] = useState<string | null>(null);
15
+ const [imageSize, setImageSize] = useState<{ w: number; h: number } | null>(null);
16
+ const [analysis, setAnalysis] = useState<Analysis | null>(null);
17
+ const [isLoading, setIsLoading] = useState(false);
18
+ const [error, setError] = useState<string | null>(null);
19
+ const [level, setLevel] = useState<Level>(defaultLevel);
20
+ const inputRef = useRef<HTMLInputElement>(null);
21
+ // Increments per pick so a slow analysis can't attach to a newer photo.
22
+ const uploadSeq = useRef(0);
23
+
24
+ function pick(file: File) {
25
+ setError(null);
26
+ setAnalysis(null);
27
+ if (previewUrl) URL.revokeObjectURL(previewUrl);
28
+
29
+ const url = URL.createObjectURL(file);
30
+ setPreviewUrl(url);
31
+
32
+ const img = new Image();
33
+ img.onload = () => setImageSize({ w: img.naturalWidth, h: img.naturalHeight });
34
+ img.src = url;
35
+
36
+ void upload(file, ++uploadSeq.current);
37
+ }
38
+
39
+ async function upload(file: File, seq: number) {
40
+ setIsLoading(true);
41
+ try {
42
+ const form = new FormData();
43
+ form.append("image", file);
44
+ form.append("level", level);
45
+ const res = await fetch("/api/vision/analyze", { method: "POST", body: form });
46
+ if (seq !== uploadSeq.current) return;
47
+ if (!res.ok) {
48
+ const j = await res.json().catch(() => ({}));
49
+ setError(j.message || j.error || "Analysis failed.");
50
+ return;
51
+ }
52
+ setAnalysis((await res.json()) as Analysis);
53
+ } catch {
54
+ if (seq === uploadSeq.current) setError("Network error. Please try again.");
55
+ } finally {
56
+ if (seq === uploadSeq.current) setIsLoading(false);
57
+ }
58
+ }
59
+
60
+ function onInputChange(e: React.ChangeEvent<HTMLInputElement>) {
61
+ const file = e.target.files?.[0];
62
+ if (file) pick(file);
63
+ }
64
+
65
+ function clear() {
66
+ uploadSeq.current++;
67
+ if (previewUrl) URL.revokeObjectURL(previewUrl);
68
+ setPreviewUrl(null);
69
+ setAnalysis(null);
70
+ setImageSize(null);
71
+ setError(null);
72
+ setIsLoading(false);
73
+ if (inputRef.current) inputRef.current.value = "";
74
+ }
75
+
76
+ return (
77
+ <div className="space-y-8">
78
+ <input
79
+ ref={inputRef}
80
+ type="file"
81
+ accept="image/*"
82
+ capture="environment"
83
+ onChange={onInputChange}
84
+ className="hidden"
85
+ />
86
+
87
+ <div className="flex flex-col sm:flex-row items-center justify-between gap-4 rounded-2xl bg-white p-5 border-3 border-black shadow-[4px_4px_0px_rgba(0,0,0,1)]">
88
+ <span className="text-sm font-black uppercase tracking-wide text-black">Sentences match level</span>
89
+ <LevelPicker value={level} onChange={setLevel} size="sm" />
90
+ </div>
91
+
92
+ {!previewUrl && <UploadDropzone onPick={() => inputRef.current?.click()} />}
93
+
94
+ {previewUrl && imageSize && (
95
+ <div className="space-y-6">
96
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center">
97
+ <button
98
+ onClick={() => inputRef.current?.click()}
99
+ className="flex items-center justify-center gap-2.5 rounded-xl bg-[#3B82F6] border-3 border-black text-white px-8 py-3 text-base font-black shadow-[4px_4px_0px_rgba(0,0,0,1)] hover:translate-x-[-2px] hover:translate-y-[-2px] hover:shadow-[6px_6px_0px_rgba(0,0,0,1)] active:translate-x-[1px] active:translate-y-[1px] active:shadow-[1px_1px_0px_rgba(0,0,0,1)] transition-all uppercase tracking-wider"
100
+ >
101
+ <Camera size={20} strokeWidth={2.5} />
102
+ New photo
103
+ </button>
104
+ <button
105
+ onClick={clear}
106
+ className="flex items-center justify-center gap-2.5 rounded-xl bg-white border-3 border-black text-black px-8 py-3 text-base font-black shadow-[4px_4px_0px_rgba(0,0,0,1)] hover:translate-x-[-2px] hover:translate-y-[-2px] hover:shadow-[6px_6px_0px_rgba(0,0,0,1)] active:translate-x-[1px] active:translate-y-[1px] active:shadow-[1px_1px_0px_rgba(0,0,0,1)] transition-all uppercase tracking-wider"
107
+ >
108
+ <RotateCcw size={20} strokeWidth={2.5} />
109
+ Clear
110
+ </button>
111
+ </div>
112
+
113
+ <div className="relative inline-block max-w-full overflow-hidden rounded-2xl bg-white p-2 border-3 border-black shadow-[8px_8px_0px_rgba(0,0,0,1)]">
114
+ <div className="relative inline-block max-w-full overflow-hidden rounded-xl bg-black">
115
+ <img src={previewUrl} alt="Selected practice" className="block max-h-[560px] w-auto max-w-full object-contain" />
116
+
117
+ {analysis ? <PhotoMarkers objects={analysis.objects} imageSize={imageSize} /> : null}
118
+
119
+ {isLoading && (
120
+ <div className="absolute inset-0 flex flex-col items-center justify-center gap-4 bg-white/90 backdrop-blur-sm z-20">
121
+ <Wand2 size={48} strokeWidth={2.5} className="animate-bounce text-[#3B82F6]" />
122
+ <span className="text-lg font-black text-black uppercase tracking-wider">Analyzing image...</span>
123
+ </div>
124
+ )}
125
+ </div>
126
+ </div>
127
+
128
+ {analysis?.caption ? (
129
+ <div className="rounded-xl bg-[#FFD21E] border-3 border-black p-5 text-base font-black text-black shadow-[4px_4px_0px_rgba(0,0,0,1)] italic">
130
+ &ldquo;{analysis.caption}&rdquo;
131
+ </div>
132
+ ) : null}
133
+ </div>
134
+ )}
135
+
136
+ {error ? (
137
+ <motion.div
138
+ initial={{ opacity: 0, y: 8 }}
139
+ animate={{ opacity: 1, y: 0 }}
140
+ className="flex items-start gap-4 rounded-xl border-3 border-black bg-[#FF8080] p-5 shadow-[4px_4px_0px_rgba(0,0,0,1)]"
141
+ >
142
+ <AlertCircle size={24} strokeWidth={2.5} className="text-black shrink-0" />
143
+ <div>
144
+ <p className="text-base font-black text-black uppercase tracking-tight">Analysis failed</p>
145
+ <p className="mt-1 text-sm font-bold text-black/80">{error}</p>
146
+ </div>
147
+ </motion.div>
148
+ ) : null}
149
+
150
+ {analysis ? <AnalysisResults analysis={analysis} lang={lang} /> : null}
151
+ </div>
152
+ );
153
+ }
app/(app)/camera/PhotoMarkers.tsx ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import type { FuriSegment } from "@/lib/types/dialogue";
5
+ import { Furi } from "@/components/Furi";
6
+
7
+ type MarkerObject = {
8
+ label: string;
9
+ translation: string;
10
+ translationSegments?: FuriSegment[];
11
+ romanized?: string;
12
+ box: [number, number, number, number];
13
+ };
14
+
15
+ export function PhotoMarkers({
16
+ objects,
17
+ imageSize,
18
+ }: {
19
+ objects: MarkerObject[];
20
+ imageSize: { w: number; h: number };
21
+ }) {
22
+ const [selected, setSelected] = useState<number | null>(null);
23
+
24
+ return (
25
+ <>
26
+ {objects.map((obj, i) => {
27
+ const [x1, y1, x2, y2] = obj.box;
28
+ const isNormalized = x1 <= 1 && y1 <= 1 && x2 <= 1 && y2 <= 1;
29
+ const cx = (x1 + x2) / 2;
30
+ const cy = (y1 + y2) / 2;
31
+ const left = isNormalized ? `${cx * 100}%` : `${(cx / imageSize.w) * 100}%`;
32
+ const top = isNormalized ? `${cy * 100}%` : `${(cy / imageSize.h) * 100}%`;
33
+
34
+ const isSelected = selected === i;
35
+ const isDimmed = selected !== null && !isSelected;
36
+ const wrapperOpacity = isDimmed ? "opacity-30" : "opacity-100";
37
+ const wrapperZ = isSelected ? "z-20" : "z-10";
38
+
39
+ return (
40
+ <button
41
+ type="button"
42
+ key={`${obj.translation}-${i}`}
43
+ onClick={(e) => {
44
+ e.stopPropagation();
45
+ setSelected(isSelected ? null : i);
46
+ }}
47
+ className={`absolute flex -translate-x-1/2 -translate-y-1/2 flex-col items-center transition-opacity duration-200 ${wrapperOpacity} ${wrapperZ}`}
48
+ style={{ left, top }}
49
+ >
50
+ <span
51
+ className={`relative rounded-full border-2 border-black bg-[#FFD21E] transition-all duration-200 ${
52
+ isSelected ? "h-5 w-5 bg-[#0EA5A4] shadow-[2px_2px_0px_rgba(0,0,0,1)]" : "h-4 w-4"
53
+ }`}
54
+ />
55
+ <div
56
+ className={`mt-2 rounded-xl border-2 border-black bg-white px-3 py-1.5 text-center shadow-[3px_3px_0px_rgba(0,0,0,1)] transition-all duration-200 ${
57
+ isSelected ? "scale-110 shadow-[5px_5px_0px_rgba(0,0,0,1)] bg-[#FFD21E]" : ""
58
+ }`}
59
+ >
60
+ <div
61
+ className={`whitespace-nowrap font-black text-black ${
62
+ isSelected ? "text-sm" : "text-xs"
63
+ } ${obj.translationSegments ? "has-furi" : ""}`}
64
+ >
65
+ <Furi text={obj.translation} segments={obj.translationSegments} />
66
+ </div>
67
+ {obj.romanized ? (
68
+ <div
69
+ className={`whitespace-nowrap font-bold text-gray-700 ${
70
+ isSelected ? "text-xs" : "text-[10px]"
71
+ }`}
72
+ >
73
+ {obj.romanized}
74
+ </div>
75
+ ) : null}
76
+ </div>
77
+ </button>
78
+ );
79
+ })}
80
+ </>
81
+ );
82
+ }
app/(app)/camera/UploadDropzone.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { motion } from "framer-motion";
4
+ import { ImagePlus } from "lucide-react";
5
+
6
+ const EASE = [0.16, 1, 0.3, 1] as const;
7
+
8
+ export function UploadDropzone({ onPick }: { onPick: () => void }) {
9
+ return (
10
+ <motion.button
11
+ type="button"
12
+ initial={{ opacity: 0, y: 12 }}
13
+ animate={{ opacity: 1, y: 0 }}
14
+ transition={{ duration: 0.5, ease: EASE }}
15
+ onClick={onPick}
16
+ className="group relative flex min-h-[360px] w-full flex-col items-center justify-center rounded-2xl border-3 border-black bg-white shadow-[8px_8px_0px_rgba(0,0,0,1)] hover:translate-x-[-2px] hover:translate-y-[-2px] hover:shadow-[10px_10px_0px_rgba(0,0,0,1)] active:translate-x-[2px] active:translate-y-[2px] active:shadow-[4px_4px_0px_rgba(0,0,0,1)] transition-all p-8 text-center overflow-hidden cursor-pointer"
17
+ >
18
+ <div className="relative flex flex-col items-center z-10">
19
+ <div className="mb-6 flex h-24 w-24 items-center justify-center rounded-2xl border-3 border-black bg-[#FFD21E] shadow-[4px_4px_0px_rgba(0,0,0,1)] group-hover:scale-105 transition-transform duration-300">
20
+ <ImagePlus size={48} strokeWidth={2.5} className="text-black" />
21
+ </div>
22
+ <h3 className="text-2xl font-black text-black tracking-tight mb-2 uppercase">Drop anything to upload</h3>
23
+ <p className="max-w-sm text-sm font-bold text-gray-700 leading-relaxed">
24
+ Camera or gallery images automatically become interactive vocabulary cards.
25
+ </p>
26
+ </div>
27
+ </motion.button>
28
+ );
29
+ }
app/(app)/camera/page.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getSession } from "@/lib/auth/session";
2
+ import type { Level } from "@/lib/languages";
3
+ import { CameraClient } from "./CameraClient";
4
+ import { LanguageSwitcher } from "@/components/LanguageSwitcher";
5
+
6
+ export default async function CameraPage() {
7
+ const session = (await getSession())!;
8
+ const targetLangs = session.targetLangs ?? (session.targetLang ? [session.targetLang] : []);
9
+ return (
10
+ <section className="space-y-8 max-w-6xl mx-auto">
11
+ <div className="relative w-full rounded-2xl bg-[#3B82F6] text-white border-3 border-black p-6 sm:p-8 shadow-[5px_5px_0px_rgba(0,0,0,1)]">
12
+ <div className="flex flex-col gap-2">
13
+ <div className="min-w-0">
14
+ <span className="text-xs font-black uppercase tracking-wider text-black bg-white px-2.5 py-1 rounded-md border-2 border-black shadow-[1px_1px_0px_rgba(0,0,0,1)] inline-block">
15
+ Image practice
16
+ </span>
17
+ <h1 className="text-2xl sm:text-3xl font-black text-white tracking-tight mt-3 leading-snug">
18
+ Learn from a photo
19
+ </h1>
20
+ <p className="mt-2 text-base font-bold text-white/95 leading-relaxed max-w-2xl">
21
+ Capture objects around you and turn them into vocabulary.
22
+ </p>
23
+ </div>
24
+ <LanguageSwitcher
25
+ targetLangs={targetLangs}
26
+ currentLang={session.targetLang!}
27
+ nativeLang={session.nativeLang ?? "en"}
28
+ level={session.level ?? "B1"}
29
+ />
30
+ </div>
31
+ </div>
32
+ <CameraClient defaultLevel={(session.level ?? "B1") as Level} lang={session.targetLang ?? "en"} />
33
+ </section>
34
+ );
35
+ }
36
+
app/(app)/camera/types.ts ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { FuriSegment } from "@/lib/types/dialogue";
2
+
3
+ export type AnalysisObject = {
4
+ label: string;
5
+ translation: string;
6
+ translationSegments?: FuriSegment[];
7
+ romanized?: string;
8
+ box: [number, number, number, number];
9
+ score: number;
10
+ };
11
+
12
+ export type AnalysisSentence = {
13
+ target: string;
14
+ targetSegments?: FuriSegment[];
15
+ gloss: string;
16
+ romanized?: string;
17
+ };
18
+
19
+ export type Analysis = {
20
+ id: string;
21
+ caption: string;
22
+ objects: AnalysisObject[];
23
+ sentences: AnalysisSentence[];
24
+ imageUrl?: string;
25
+ };
app/(app)/error.tsx ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ export default function AppError({ error, reset }: { error: Error; reset: () => void }) {
4
+ return (
5
+ <section className="duo-card mx-auto max-w-md space-y-4 p-8 text-center">
6
+ <h1 className="text-2xl font-black text-[#3C3C3C]">Something went wrong</h1>
7
+ <p className="text-sm font-bold text-[#777777]">{error.message || "Please try again."}</p>
8
+ <button
9
+ onClick={reset}
10
+ className="btn-duo btn-duo-primary"
11
+ >
12
+ Try again
13
+ </button>
14
+ </section>
15
+ );
16
+ }
17
+
app/(app)/layout.tsx ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from "next/link";
2
+ import Image from "next/image";
3
+ import { redirect } from "next/navigation";
4
+ import { Camera, Home, Languages, User } from "lucide-react";
5
+ import { getSession } from "@/lib/auth/session";
6
+ import { BottomNavLink } from "./BottomNavLink";
7
+
8
+ export default async function AppLayout({ children }: { children: React.ReactNode }) {
9
+ const session = await getSession();
10
+ if (!session) redirect("/");
11
+ if (!session.targetLang || !session.level) redirect("/onboarding");
12
+
13
+ return (
14
+ <div className="min-h-screen bg-[#F8FBFF] text-[#4B4B4B]">
15
+ <header className="mx-auto flex w-full max-w-6xl items-center px-4 pt-4 sm:px-6 sm:pt-6">
16
+ <Link href="/phrases" className="flex items-center gap-3 text-[#3C3C3C]">
17
+ <Image
18
+ src="/logo.png"
19
+ alt="PraxaLing logo"
20
+ width={48}
21
+ height={48}
22
+ className="h-12 w-12 rounded-xl border-2 border-black object-cover shadow-[3px_3px_0px_rgba(0,0,0,1)]"
23
+ />
24
+ <span className="text-lg font-black leading-none">PraxaLing</span>
25
+ </Link>
26
+ </header>
27
+
28
+ <main className="duo-safe-bottom mx-auto w-full max-w-6xl px-4 py-5 sm:px-6 sm:py-7 md:pb-28">
29
+ {children}
30
+ </main>
31
+
32
+ <nav className="fixed inset-x-0 bottom-0 z-50 border-t-3 border-black bg-white px-4 pb-[calc(0.75rem+env(safe-area-inset-bottom))] pt-3 shadow-[0_-4px_0_rgba(0,0,0,0.06)]">
33
+ <div className="mx-auto grid max-w-2xl grid-cols-4 gap-2">
34
+ <BottomNavLink href="/phrases" icon={<Languages size={21} strokeWidth={2.5} />} label="Phrases" />
35
+ <BottomNavLink href="/camera" icon={<Camera size={21} strokeWidth={2.5} />} label="Camera" />
36
+ <BottomNavLink href="/practice" icon={<Home size={21} strokeWidth={2.5} />} label="Practice" />
37
+ <BottomNavLink href="/profile" icon={<User size={21} strokeWidth={2.5} />} label="Profile" />
38
+ </div>
39
+ </nav>
40
+ </div>
41
+ );
42
+ }
app/(app)/phrases/PhraseResult/EmptyState.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PenLine } from "lucide-react";
2
+
3
+ export function EmptyState({ isPending = false }: { isPending?: boolean }) {
4
+ return (
5
+ <div className="rounded-2xl bg-white p-8 text-center sm:p-12 border-3 border-black shadow-[8px_8px_0px_rgba(0,0,0,1)] relative overflow-hidden">
6
+ <div className="mx-auto flex h-20 w-20 items-center justify-center rounded-2xl bg-[#FFD21E] border-3 border-black text-black shadow-[4px_4px_0px_rgba(0,0,0,1)] rotate-3">
7
+ <PenLine size={32} strokeWidth={3} className="-rotate-3" />
8
+ </div>
9
+ {isPending ? (
10
+ <div className="mt-8 space-y-4">
11
+ <div className="text-xl sm:text-2xl font-black text-black tracking-tight uppercase leading-none">
12
+ Building your mini lesson...
13
+ </div>
14
+ <div className="mx-auto h-6 w-full max-w-md overflow-hidden rounded-full border-3 border-black bg-white shadow-[4px_4px_0px_rgba(0,0,0,1)]">
15
+ <div className="phrases-progress-bar h-full bg-[#0EA5A4]" />
16
+ </div>
17
+ </div>
18
+ ) : (
19
+ <h2 className="mt-8 text-2xl sm:text-3xl font-black text-black tracking-tight uppercase leading-none">
20
+ Enter a phrase to build a mini lesson
21
+ </h2>
22
+ )}
23
+ <style>{`
24
+ @keyframes phrases-progress-slide {
25
+ 0% { transform: translateX(-100%); }
26
+ 100% { transform: translateX(300%); }
27
+ }
28
+ .phrases-progress-bar {
29
+ width: 33%;
30
+ animation: phrases-progress-slide 1.4s ease-in-out infinite;
31
+ }
32
+ `}</style>
33
+ </div>
34
+ );
35
+ }
app/(app)/phrases/PhraseResult/HeaderCard.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { FuriSegment } from "@/lib/types/dialogue";
2
+ import { Furi } from "@/components/Furi";
3
+ import { TTSButton } from "@/components/TTSButton";
4
+
5
+ export function HeaderCard({
6
+ input,
7
+ inputSegments,
8
+ partOfSpeech,
9
+ translation,
10
+ lang,
11
+ }: {
12
+ input: string;
13
+ inputSegments?: FuriSegment[];
14
+ partOfSpeech: string;
15
+ translation: string;
16
+ lang: string;
17
+ }) {
18
+ return (
19
+ <div className="rounded-2xl overflow-hidden bg-[#FFD21E] border-3 border-black p-6 sm:p-8 text-black shadow-[5px_5px_0px_rgba(0,0,0,1)]">
20
+ <div className="inline-block rounded-md bg-white border-2 border-black px-2.5 py-1 text-xs font-black uppercase tracking-wider text-black shadow-[1px_1px_0px_rgba(0,0,0,1)] mb-3">{partOfSpeech}</div>
21
+ <div className="flex items-center gap-3">
22
+ <h2 className={`text-3xl font-black tracking-tight sm:text-4xl text-black ${inputSegments ? "has-furi" : ""}`}>
23
+ <Furi text={input} segments={inputSegments} />
24
+ </h2>
25
+ <TTSButton
26
+ text={input}
27
+ lang={lang}
28
+ />
29
+ </div>
30
+ <p className="mt-3 text-base font-black text-gray-800 sm:text-lg">{translation}</p>
31
+ </div>
32
+ );
33
+ }
app/(app)/phrases/PhraseResult/KanjiCard.tsx ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { KanjiInfo } from "@/lib/hf/phrases";
2
+ import { Furi } from "@/components/Furi";
3
+
4
+ export function KanjiCard({ kanji }: { kanji: KanjiInfo }) {
5
+ return (
6
+ <div className="rounded-xl bg-white p-5 border-3 border-black shadow-[4px_4px_0px_rgba(0,0,0,1)] transition-all hover:translate-x-[-1px] hover:translate-y-[-1px] hover:shadow-[5px_5px_0px_rgba(0,0,0,1)]">
7
+ <div className="flex items-baseline gap-3">
8
+ <div className="text-4xl font-black leading-none text-black">{kanji.kanji}</div>
9
+ <div className="text-sm font-bold text-gray-700">{kanji.meaning}</div>
10
+ </div>
11
+ <dl className="mt-4 space-y-3 text-base">
12
+ <div className="flex items-center gap-2.5">
13
+ <dt className="w-12 shrink-0 text-xs font-black uppercase tracking-wider text-black bg-[#FFD21E] border-2 border-black rounded py-1 text-center shadow-[2px_2px_0px_rgba(0,0,0,1)]">On</dt>
14
+ <dd className="text-black font-black text-lg">
15
+ {kanji.onyomi.length ? kanji.onyomi.join("、") : <span className="text-gray-300">—</span>}
16
+ </dd>
17
+ </div>
18
+ <div className="flex items-center gap-2.5">
19
+ <dt className="w-12 shrink-0 text-xs font-black uppercase tracking-wider text-white bg-[#0EA5A4] border-2 border-black rounded py-1 text-center shadow-[2px_2px_0px_rgba(0,0,0,1)]">Kun</dt>
20
+ <dd className="text-black font-black text-lg">
21
+ {kanji.kunyomi.length ? kanji.kunyomi.join("、") : <span className="text-gray-300">—</span>}
22
+ </dd>
23
+ </div>
24
+ </dl>
25
+ {kanji.exampleWord ? (
26
+ <div className="mt-4 inline-flex rounded-lg bg-[#E6FBFA] border-2 border-black px-4 py-2 text-base font-black text-black shadow-[3px_3px_0px_rgba(0,0,0,1)] has-furi">
27
+ <Furi
28
+ text={kanji.exampleWord}
29
+ segments={
30
+ kanji.exampleWordReading
31
+ ? [{ text: kanji.exampleWord, reading: kanji.exampleWordReading }]
32
+ : undefined
33
+ }
34
+ />
35
+ </div>
36
+ ) : null}
37
+ </div>
38
+ );
39
+ }
app/(app)/phrases/PhraseResult/RelatedWordCard.tsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { RelatedWord } from "@/lib/hf/phrases";
2
+ import { Furi } from "@/components/Furi";
3
+ import { TTSButton } from "@/components/TTSButton";
4
+
5
+ export function RelatedWordCard({ word, lang }: { word: RelatedWord; lang: string }) {
6
+ return (
7
+ <div className="rounded-xl bg-white p-5 border-3 border-black shadow-[4px_4px_0px_rgba(0,0,0,1)] transition-all hover:translate-x-[-1px] hover:translate-y-[-1px] hover:shadow-[5px_5px_0px_rgba(0,0,0,1)]">
8
+ <div className="flex items-center justify-between gap-2">
9
+ <div className={`text-xl font-black text-black ${word.wordSegments ? "has-furi" : ""}`}>
10
+ <Furi text={word.word} segments={word.wordSegments} />
11
+ </div>
12
+ <div className="flex items-center gap-2">
13
+ {word.partOfSpeech ? (
14
+ <span className="text-[10px] font-black uppercase tracking-wider text-black bg-[#FFD21E] border border-black rounded px-1.5 py-0.5 shadow-[1px_1px_0px_rgba(0,0,0,1)]">
15
+ {word.partOfSpeech}
16
+ </span>
17
+ ) : null}
18
+ <TTSButton text={word.word} lang={lang} />
19
+ </div>
20
+ </div>
21
+ <div className="mt-3 text-base font-bold text-gray-700">{word.translation}</div>
22
+ {word.note ? <p className="mt-2 text-sm font-black text-gray-500">{word.note}</p> : null}
23
+ </div>
24
+ );
25
+ }
app/(app)/phrases/PhraseResult/Section.tsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function Section({
2
+ title,
3
+ icon,
4
+ children,
5
+ }: {
6
+ title: string;
7
+ icon: React.ReactNode;
8
+ children: React.ReactNode;
9
+ }) {
10
+ return (
11
+ <section>
12
+ <div className="mb-4 flex items-center gap-2.5 px-1">
13
+ <span className="text-black font-black">{icon}</span>
14
+ <h2 className="text-xl font-black text-black tracking-tight sm:text-2xl">{title}</h2>
15
+ </div>
16
+ {children}
17
+ </section>
18
+ );
19
+ }
app/(app)/phrases/PhraseResult/SentenceCard.tsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { PhraseSentence } from "@/lib/hf/phrases";
2
+ import { Furi } from "@/components/Furi";
3
+ import { TTSButton } from "@/components/TTSButton";
4
+
5
+ export function SentenceCard({ sentence, lang }: { sentence: PhraseSentence; lang: string }) {
6
+ return (
7
+ <div className="rounded-xl bg-white p-5 border-3 border-black shadow-[4px_4px_0px_rgba(0,0,0,1)] transition-all hover:translate-x-[-1px] hover:translate-y-[-1px] hover:shadow-[5px_5px_0px_rgba(0,0,0,1)]">
8
+ <div className="flex items-start justify-between gap-3">
9
+ <div className={`text-lg font-black text-black ${sentence.targetSegments ? "has-furi" : ""}`}>
10
+ <Furi text={sentence.target} segments={sentence.targetSegments} />
11
+ </div>
12
+ <TTSButton text={sentence.target} lang={lang} />
13
+ </div>
14
+ <div className="mt-2 text-sm font-bold text-gray-700">{sentence.translation}</div>
15
+ {sentence.note ? (
16
+ <div className="mt-3 inline-block rounded-md bg-[#FFF7D6] border-2 border-black px-2.5 py-1 text-[10px] font-black uppercase tracking-wider text-black shadow-[1px_1px_0px_rgba(0,0,0,1)]">{sentence.note}</div>
17
+ ) : null}
18
+ </div>
19
+ );
20
+ }
app/(app)/phrases/PhraseResult/index.tsx ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BookOpen, Languages, Lightbulb, ListChecks, MessageSquareText } from "lucide-react";
2
+ import type { PhraseAnalysis } from "@/lib/hf/phrases";
3
+ import { Section } from "./Section";
4
+ import { HeaderCard } from "./HeaderCard";
5
+ import { SentenceCard } from "./SentenceCard";
6
+ import { KanjiCard } from "./KanjiCard";
7
+ import { RelatedWordCard } from "./RelatedWordCard";
8
+
9
+ export function PhraseResult({ result, lang }: { result: PhraseAnalysis; lang: string }) {
10
+ return (
11
+ <div className="space-y-6">
12
+ <HeaderCard
13
+ input={result.input}
14
+ inputSegments={result.inputSegments}
15
+ partOfSpeech={result.partOfSpeech}
16
+ translation={result.translation}
17
+ lang={lang}
18
+ />
19
+
20
+ <Section title="Example sentences" icon={<MessageSquareText size={22} />}>
21
+ <div className="grid gap-3">
22
+ {result.sentences.map((sentence, index) => (
23
+ <SentenceCard key={`${sentence.target}-${index}`} sentence={sentence} lang={lang} />
24
+ ))}
25
+ </div>
26
+ </Section>
27
+
28
+ {result.kanjiInfo.length > 0 ? (
29
+ <Section title="Kanji breakdown" icon={<Languages size={22} />}>
30
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
31
+ {result.kanjiInfo.map((kanji, index) => (
32
+ <KanjiCard key={`${kanji.kanji}-${index}`} kanji={kanji} />
33
+ ))}
34
+ </div>
35
+ </Section>
36
+ ) : null}
37
+
38
+ {result.relatedWords.length > 0 ? (
39
+ <Section title="Related words" icon={<BookOpen size={22} strokeWidth={2.5} />}>
40
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
41
+ {result.relatedWords.map((word, index) => (
42
+ <RelatedWordCard key={`${word.word}-${index}`} word={word} lang={lang} />
43
+ ))}
44
+ </div>
45
+ </Section>
46
+ ) : null}
47
+
48
+ <div className="grid gap-6 lg:grid-cols-2">
49
+ <Section title="Breakdown" icon={<ListChecks size={22} strokeWidth={2.5} />}>
50
+ <div className="space-y-3">
51
+ {result.breakdown.map((item, index) => (
52
+ <div key={`${item.part}-${index}`} className="rounded-xl bg-white border-2 border-black p-4 transition-all shadow-[3px_3px_0px_rgba(0,0,0,1)] hover:translate-x-[-1px] hover:translate-y-[-1px] hover:shadow-[4px_4px_0px_rgba(0,0,0,1)]">
53
+ <div className="text-base font-black text-black">{item.part}</div>
54
+ <p className="mt-1 text-sm font-bold leading-relaxed text-gray-700">{item.explanation}</p>
55
+ </div>
56
+ ))}
57
+ </div>
58
+ </Section>
59
+
60
+ <Section title="Tips and verb notes" icon={<Lightbulb size={22} strokeWidth={2.5} />}>
61
+ {result.verbInfo ? (
62
+ <div className="mb-4 rounded-xl border-3 border-black bg-[#FFD21E] p-4 shadow-[3px_3px_0px_rgba(0,0,0,1)] text-black">
63
+ <div className="text-xs font-black uppercase tracking-wider text-black">Verb info</div>
64
+ <p className="mt-1 text-sm font-black leading-relaxed text-black">{result.verbInfo}</p>
65
+ </div>
66
+ ) : null}
67
+ <ul className="space-y-3">
68
+ {result.tips.map((tip, index) => (
69
+ <li key={`${tip}-${index}`} className="rounded-xl bg-white border-2 border-black p-4 text-sm font-bold leading-relaxed text-black transition-all shadow-[3px_3px_0px_rgba(0,0,0,1)] hover:translate-x-[-1px] hover:translate-y-[-1px] hover:shadow-[4px_4px_0px_rgba(0,0,0,1)]">
70
+ {tip}
71
+ </li>
72
+ ))}
73
+ </ul>
74
+ </Section>
75
+ </div>
76
+ </div>
77
+ );
78
+ }
app/(app)/phrases/PhrasesClient.tsx ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import type { PhraseAnalysis } from "@/lib/hf/phrases";
5
+ import type { Level } from "@/lib/languages";
6
+ import { LevelPicker } from "@/components/LevelPicker";
7
+ import { PhraseResult } from "./PhraseResult";
8
+ import { EmptyState } from "./PhraseResult/EmptyState";
9
+
10
+ const examples = ["Car", "Dog", "Teacher", "finding lost documents"];
11
+
12
+ export function PhrasesClient({ defaultLevel, lang }: { defaultLevel: Level; lang: string }) {
13
+ const [text, setText] = useState("");
14
+ const [result, setResult] = useState<PhraseAnalysis | null>(null);
15
+ const [error, setError] = useState<string | null>(null);
16
+ const [isPending, setIsPending] = useState(false);
17
+ const [level, setLevel] = useState<Level>(defaultLevel);
18
+
19
+ async function analyze(value = text) {
20
+ if (isPending) return;
21
+ const phrase = value.trim();
22
+ if (!phrase) {
23
+ setError("Enter a phrase first.");
24
+ return;
25
+ }
26
+ setError(null);
27
+
28
+ setIsPending(true);
29
+ try {
30
+ const res = await fetch("/api/phrases", {
31
+ method: "POST",
32
+ headers: { "content-type": "application/json" },
33
+ body: JSON.stringify({ text: phrase, level }),
34
+ });
35
+ if (!res.ok) {
36
+ const data = await res.json().catch(() => ({}));
37
+ setError(data.message || data.error || "Could not analyze this phrase.");
38
+ return;
39
+ }
40
+ setResult((await res.json()) as PhraseAnalysis);
41
+ } catch {
42
+ setError("Could not analyze this phrase.");
43
+ } finally {
44
+ setIsPending(false);
45
+ }
46
+ }
47
+
48
+ return (
49
+ <div className="space-y-8">
50
+ <div className="rounded-3xl bg-white p-8 sm:p-10 border-4 border-black shadow-[10px_10px_0px_rgba(0,0,0,1)] transition-all">
51
+ <div className="flex flex-wrap items-center justify-between gap-4">
52
+ <label className="text-base font-black uppercase tracking-widest text-black" htmlFor="phrase-input">
53
+ Phrase or word
54
+ </label>
55
+ <LevelPicker value={level} onChange={setLevel} size="sm" label="Level" />
56
+ </div>
57
+ <div className="mt-6 flex flex-col gap-6 sm:flex-row sm:items-stretch">
58
+ <input
59
+ id="phrase-input"
60
+ value={text}
61
+ onChange={(event) => setText(event.target.value)}
62
+ onKeyDown={(event) => event.key === "Enter" && analyze()}
63
+ placeholder="Type a phrase, word, or verb..."
64
+ className="h-20 w-full sm:flex-1 rounded-2xl border-4 border-black bg-white px-8 text-2xl font-black text-black outline-none transition-all focus:translate-x-[-2px] focus:translate-y-[-2px] focus:shadow-[6px_6px_0px_rgba(0,0,0,1)] placeholder:text-xl placeholder:font-bold placeholder:text-gray-400 shadow-[4px_4px_0px_rgba(0,0,0,1)]"
65
+ />
66
+ <button
67
+ onClick={() => analyze()}
68
+ disabled={isPending}
69
+ className="flex h-20 w-full sm:w-auto sm:px-12 items-center justify-center rounded-2xl bg-[#0EA5A4] border-4 border-black text-xl font-black text-white shadow-[6px_6px_0px_rgba(0,0,0,1)] hover:translate-x-[-3px] hover:translate-y-[-3px] hover:shadow-[9px_9px_0px_rgba(0,0,0,1)] active:translate-x-[2px] active:translate-y-[2px] active:shadow-[2px_2px_0px_rgba(0,0,0,1)] transition-all disabled:opacity-50 uppercase tracking-widest shrink-0"
70
+ >
71
+ {isPending ? "Analyzing..." : "Analyze"}
72
+ </button>
73
+ </div>
74
+ {!text && (
75
+ <div className="mt-6 flex flex-wrap gap-3">
76
+ {examples.map((example) => (
77
+ <button
78
+ key={example}
79
+ onClick={() => {
80
+ setText(example);
81
+ analyze(example);
82
+ }}
83
+ className="rounded-xl bg-[#FFD21E] border-2 border-black px-5 py-2.5 text-sm font-black text-black shadow-[3px_3px_0px_rgba(0,0,0,1)] hover:translate-x-[-2px] hover:translate-y-[-2px] hover:shadow-[5px_5px_0px_rgba(0,0,0,1)] active:translate-x-[1px] active:translate-y-[1px] active:shadow-[1px_1px_0px_rgba(0,0,0,1)] transition-all uppercase tracking-tight"
84
+ >
85
+ {example}
86
+ </button>
87
+ ))}
88
+ </div>
89
+ )}
90
+ {error ? (
91
+ <div className="mt-6 rounded-2xl border-4 border-black bg-[#FF8080] p-6 text-base font-black text-black shadow-[5px_5px_0px_rgba(0,0,0,1)]">
92
+ {error}
93
+ </div>
94
+ ) : null}
95
+ </div>
96
+
97
+ {isPending ? <EmptyState isPending /> : result ? <PhraseResult result={result} lang={lang} /> : <EmptyState />}
98
+ </div>
99
+ );
100
+ }
app/(app)/phrases/page.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getSession } from "@/lib/auth/session";
2
+ import { languageName, type Level } from "@/lib/languages";
3
+ import { PhrasesClient } from "./PhrasesClient";
4
+ import { LanguageSwitcher } from "@/components/LanguageSwitcher";
5
+
6
+ export default async function PhrasesPage() {
7
+ const session = (await getSession())!;
8
+ const targetLangs = session.targetLangs ?? (session.targetLang ? [session.targetLang] : []);
9
+
10
+ return (
11
+ <section className="space-y-8 max-w-6xl mx-auto">
12
+ <div className="relative w-full rounded-2xl bg-[#FF6B6B] text-white border-3 border-black p-6 sm:p-8 shadow-[5px_5px_0px_rgba(0,0,0,1)]">
13
+ <div className="flex flex-col gap-2">
14
+ <div className="min-w-0">
15
+ <span className="text-xs font-black uppercase tracking-wider text-black bg-white px-2.5 py-1 rounded-md border-2 border-black shadow-[1px_1px_0px_rgba(0,0,0,1)] inline-block">
16
+ Phrase coach
17
+ </span>
18
+ <h1 className="text-2xl sm:text-3xl font-black text-white tracking-tight mt-3 leading-snug">
19
+ Break down phrases
20
+ </h1>
21
+ <p className="mt-2 text-base font-bold text-white/95 leading-relaxed max-w-2xl">
22
+ Master {languageName(session.targetLang!)} with deep grammar insights, accurate translations, and contextual usage tips.
23
+ </p>
24
+ </div>
25
+ <LanguageSwitcher
26
+ targetLangs={targetLangs}
27
+ currentLang={session.targetLang!}
28
+ nativeLang={session.nativeLang ?? "en"}
29
+ level={session.level ?? "B1"}
30
+ />
31
+ </div>
32
+ </div>
33
+ <PhrasesClient defaultLevel={(session.level ?? "B1") as Level} lang={session.targetLang ?? "en"} />
34
+ </section>
35
+ );
36
+ }
app/(app)/practice/CustomTopicCard.tsx ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { LevelPicker } from "@/components/LevelPicker";
4
+ import type { Level } from "@/lib/languages";
5
+ import { Wand2, ArrowRight } from "lucide-react";
6
+
7
+ export function CustomTopicCard({
8
+ level,
9
+ onLevelChange,
10
+ customTopic,
11
+ onCustomTopicChange,
12
+ onGenerate,
13
+ pending,
14
+ disabled,
15
+ error,
16
+ }: {
17
+ level: Level;
18
+ onLevelChange: (level: Level) => void;
19
+ customTopic: string;
20
+ onCustomTopicChange: (value: string) => void;
21
+ onGenerate: () => void;
22
+ pending: boolean;
23
+ disabled: boolean;
24
+ error: string | null;
25
+ }) {
26
+ return (
27
+ <div className="w-full space-y-3">
28
+ <div className="flex flex-wrap items-center justify-between gap-3 px-1">
29
+ <div className="text-sm font-black text-black tracking-wide uppercase flex items-center gap-2">
30
+ <Wand2 size={20} strokeWidth={2.5} />
31
+ Build your own
32
+ </div>
33
+ <LevelPicker value={level} onChange={onLevelChange} size="sm" label="CEFR" />
34
+ </div>
35
+
36
+ <div className="relative w-full">
37
+ <textarea
38
+ value={customTopic}
39
+ onChange={(event) => onCustomTopicChange(event.target.value)}
40
+ placeholder="Describe a situation, e.g. returning a damaged phone at a shop..."
41
+ rows={2}
42
+ disabled={disabled && !pending}
43
+ className="w-full resize-none rounded-2xl border-3 border-black bg-white shadow-[4px_4px_0px_rgba(0,0,0,1)] pl-4 pr-16 py-4 text-base font-bold leading-relaxed text-black outline-none transition-all focus:translate-x-[-1px] focus:translate-y-[-1px] focus:shadow-[5px_5px_0px_rgba(0,0,0,1)] placeholder:text-gray-500"
44
+ style={{ minHeight: '70px' }}
45
+ />
46
+ <div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center">
47
+ <button
48
+ onClick={onGenerate}
49
+ disabled={disabled || !customTopic.trim()}
50
+ className={`flex h-11 w-11 items-center justify-center rounded-xl border-2 border-black font-black transition-all ${
51
+ !customTopic.trim() || disabled
52
+ ? "bg-gray-100 text-gray-400 shadow-[1px_1px_0px_rgba(0,0,0,1)]"
53
+ : "bg-[#0EA5A4] text-white shadow-[2px_2px_0px_rgba(0,0,0,1)] hover:translate-x-[-1px] hover:translate-y-[-1px] hover:shadow-[3px_3px_0px_rgba(0,0,0,1)] active:translate-x-[1px] active:translate-y-[1px] active:shadow-[1px_1px_0px_rgba(0,0,0,1)]"
54
+ }`}
55
+ >
56
+ {pending ? (
57
+ <div className="h-5 w-5 rounded-full border-2 border-white border-t-transparent animate-spin" />
58
+ ) : (
59
+ <ArrowRight size={20} strokeWidth={3} />
60
+ )}
61
+ </button>
62
+ </div>
63
+ </div>
64
+
65
+ {error ? (
66
+ <div className="rounded-xl border-2 border-black bg-[#FF8080] px-3 py-2 text-xs font-black text-black shadow-[2px_2px_0px_rgba(0,0,0,1)]">
67
+ {error}
68
+ </div>
69
+ ) : null}
70
+ </div>
71
+ );
72
+ }
app/(app)/practice/DialoguePracticeClient.tsx ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import type { Dialogue } from "@/lib/types/dialogue";
5
+ import type { Level } from "@/lib/languages";
6
+ import { CustomTopicCard } from "./CustomTopicCard";
7
+ import { ScenarioLibrary } from "./ScenarioLibrary";
8
+ import { DialogueResultView } from "./DialogueResultView";
9
+ import { GeneratingDialogueProgress } from "./GeneratingDialogueProgress";
10
+
11
+ export function DialoguePracticeClient({ defaultLevel }: { defaultLevel: Level }) {
12
+ const [customTopic, setCustomTopic] = useState("");
13
+ const [error, setError] = useState<string | null>(null);
14
+ const [activeScenario, setActiveScenario] = useState<string | null>(null);
15
+ const [generatedDialogue, setGeneratedDialogue] = useState<Dialogue | null>(null);
16
+ const [level, setLevel] = useState<Level>(defaultLevel);
17
+
18
+ async function generate(scenario: string, label: string) {
19
+ if (activeScenario) return;
20
+ setError(null);
21
+ setActiveScenario(label);
22
+
23
+ try {
24
+ const res = await fetch("/api/dialogues", {
25
+ method: "POST",
26
+ headers: { "content-type": "application/json" },
27
+ body: JSON.stringify({ scenario, level }),
28
+ });
29
+
30
+ if (!res.ok) {
31
+ const data = await res.json().catch(() => ({}));
32
+ setError(data.message || data.error || "Could not generate this dialogue.");
33
+ setActiveScenario(null);
34
+ return;
35
+ }
36
+
37
+ setGeneratedDialogue((await res.json()) as Dialogue);
38
+ } catch {
39
+ setError("Could not generate this dialogue.");
40
+ setActiveScenario(null);
41
+ }
42
+ }
43
+
44
+ function generateCustom() {
45
+ const topic = customTopic.trim();
46
+ if (!topic) {
47
+ setError("Enter a situation first.");
48
+ return;
49
+ }
50
+ generate(topic, "Custom topic");
51
+ }
52
+
53
+ if (generatedDialogue) {
54
+ return (
55
+ <DialogueResultView
56
+ dialogue={generatedDialogue}
57
+ onBack={() => {
58
+ setGeneratedDialogue(null);
59
+ setActiveScenario(null);
60
+ }}
61
+ />
62
+ );
63
+ }
64
+
65
+ if (activeScenario) {
66
+ return <GeneratingDialogueProgress scenario={activeScenario} />;
67
+ }
68
+
69
+ return (
70
+ <section className="space-y-6">
71
+ <CustomTopicCard
72
+ level={level}
73
+ onLevelChange={setLevel}
74
+ customTopic={customTopic}
75
+ onCustomTopicChange={setCustomTopic}
76
+ onGenerate={generateCustom}
77
+ pending={activeScenario === "Custom topic"}
78
+ disabled={activeScenario !== null}
79
+ error={error}
80
+ />
81
+ <ScenarioLibrary activeScenario={activeScenario} onGenerate={generate} />
82
+ </section>
83
+ );
84
+ }
app/(app)/practice/DialogueResultView.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Dialogue } from "@/lib/types/dialogue";
2
+ import { DialogueClient } from "./dialogues/[id]/DialogueClient";
3
+ import { ArrowLeft } from "lucide-react";
4
+
5
+ export function DialogueResultView({
6
+ dialogue,
7
+ onBack,
8
+ }: {
9
+ dialogue: Dialogue;
10
+ onBack: () => void;
11
+ }) {
12
+ return (
13
+ <section className="mx-auto max-w-4xl space-y-6">
14
+ <button
15
+ onClick={onBack}
16
+ className="inline-flex items-center gap-2.5 px-5 py-2.5 rounded-xl border-2 border-black bg-white text-black font-black text-xs uppercase tracking-wider shadow-[2px_2px_0px_rgba(0,0,0,1)] hover:translate-x-[-2px] hover:translate-y-[-2px] hover:shadow-[5px_5px_0px_rgba(0,0,0,1)] active:translate-x-[1px] active:translate-y-[1px] active:shadow-[1px_1px_0px_rgba(0,0,0,1)] transition-all"
17
+ >
18
+ <ArrowLeft size={18} strokeWidth={3} />
19
+ Back to scenarios
20
+ </button>
21
+ <div className="rounded-2xl overflow-hidden bg-[#FFD21E] border-3 border-black p-6 sm:p-8 text-black shadow-[5px_5px_0px_rgba(0,0,0,1)]">
22
+ <div className="inline-block rounded-md bg-white border-2 border-black px-2.5 py-1 text-xs font-black uppercase tracking-wider text-black shadow-[1px_1px_0px_rgba(0,0,0,1)] mb-3">
23
+ {dialogue.targetLang.toUpperCase()} / {dialogue.level}
24
+ </div>
25
+ <h1 className="text-2xl sm:text-4xl font-black tracking-tight text-black leading-snug uppercase">{dialogue.title}</h1>
26
+ </div>
27
+ <DialogueClient dialogue={dialogue} />
28
+ </section>
29
+ );
30
+ }
app/(app)/practice/GeneratingDialogueProgress.tsx ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from "react";
2
+ import { Wand2, PenLine, Globe, Settings2, BrainCircuit, type LucideIcon } from "lucide-react";
3
+ import { StageItem } from "./StageItem";
4
+
5
+ type Stage = {
6
+ icon: LucideIcon;
7
+ label: string;
8
+ description: string;
9
+ };
10
+
11
+ const STAGES: Stage[] = [
12
+ {
13
+ icon: Wand2,
14
+ label: "Setting the scene",
15
+ description: "Crafting a believable situation around your topic.",
16
+ },
17
+ {
18
+ icon: PenLine,
19
+ label: "Writing the dialogue",
20
+ description: "Generating natural turn-by-turn conversation.",
21
+ },
22
+ {
23
+ icon: Globe,
24
+ label: "Adding translations",
25
+ description: "Aligning each line with your native language.",
26
+ },
27
+ {
28
+ icon: Settings2,
29
+ label: "Polishing details",
30
+ description: "Tuning vocabulary to your level and style.",
31
+ },
32
+ ];
33
+
34
+ const STAGE_DURATIONS_MS = [2800, 4200, 5500, 7000];
35
+
36
+ export function GeneratingDialogueProgress({ scenario }: { scenario: string }) {
37
+ const [stageIndex, setStageIndex] = useState(0);
38
+ const [progress, setProgress] = useState(4);
39
+
40
+ useEffect(() => {
41
+ const timers: ReturnType<typeof setTimeout>[] = [];
42
+ let elapsed = 0;
43
+ STAGE_DURATIONS_MS.forEach((duration, i) => {
44
+ elapsed += duration;
45
+ if (i < STAGES.length - 1) {
46
+ timers.push(setTimeout(() => setStageIndex(i + 1), elapsed));
47
+ }
48
+ });
49
+ return () => timers.forEach(clearTimeout);
50
+ }, []);
51
+
52
+ useEffect(() => {
53
+ const id = setInterval(() => {
54
+ setProgress((prev) => {
55
+ if (prev >= 92) return prev;
56
+ const delta = prev < 60 ? 1.5 : prev < 80 ? 0.7 : 0.25;
57
+ return Math.min(92, prev + delta);
58
+ });
59
+ }, 250);
60
+ return () => clearInterval(id);
61
+ }, []);
62
+
63
+ return (
64
+ <div className="mx-auto mt-4 max-w-2xl rounded-2xl border-3 border-black bg-white p-6 sm:p-8 shadow-[5px_5px_0px_rgba(0,0,0,1)]">
65
+ <div className="flex items-center gap-4">
66
+ <div className="relative flex h-16 w-16 shrink-0 items-center justify-center rounded-xl border-2 border-black bg-[#FFD21E] shadow-[2px_2px_0px_rgba(0,0,0,1)] text-black">
67
+ <BrainCircuit size={32} strokeWidth={2.5} />
68
+ <span className="absolute -bottom-1.5 -right-1.5 flex h-6 w-6 items-center justify-center rounded-full border-2 border-black bg-[#0EA5A4]">
69
+ <span className="h-2.5 w-2.5 animate-spin rounded-full border-2 border-white border-t-transparent" />
70
+ </span>
71
+ </div>
72
+ <div className="min-w-0 flex-1">
73
+ <div className="text-xs font-black uppercase tracking-wider text-gray-500">
74
+ Generating dialogue
75
+ </div>
76
+ <h2 className="mt-1 truncate text-xl font-black text-black sm:text-2xl uppercase tracking-tight">{scenario}</h2>
77
+ </div>
78
+ </div>
79
+
80
+ <div className="mt-6">
81
+ <div className="flex items-center justify-between text-xs font-black uppercase tracking-wider text-gray-600">
82
+ <span>{STAGES[stageIndex].label}</span>
83
+ <span>{Math.round(progress)}%</span>
84
+ </div>
85
+ <div className="mt-2 h-4 w-full overflow-hidden rounded-full border-2 border-black bg-gray-100">
86
+ <div
87
+ className="h-full rounded-full bg-[#0EA5A4] transition-[width] duration-300 ease-out"
88
+ style={{ width: `${progress}%` }}
89
+ />
90
+ </div>
91
+ </div>
92
+
93
+ <ul className="mt-8 space-y-3">
94
+ {STAGES.map((stage, index) => (
95
+ <StageItem
96
+ key={stage.label}
97
+ icon={stage.icon}
98
+ label={stage.label}
99
+ description={stage.description}
100
+ isDone={index < stageIndex}
101
+ isActive={index === stageIndex}
102
+ />
103
+ ))}
104
+ </ul>
105
+
106
+ <p className="mt-6 text-center text-xs font-black uppercase tracking-wider text-gray-400">
107
+ Hang tight — generating a custom dialogue can take up to 30 seconds.
108
+ </p>
109
+ </div>
110
+ );
111
+ }
app/(app)/practice/ScenarioCard.tsx ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { DialogueScenario } from "@/lib/dialogue-scenarios";
2
+ import { SCENARIO_THEMES } from "@/lib/dialogue-themes";
3
+ import { ArrowRight } from "lucide-react";
4
+
5
+ export function ScenarioCard({
6
+ scenario,
7
+ index,
8
+ pending,
9
+ disabled,
10
+ onGenerate,
11
+ }: {
12
+ scenario: DialogueScenario;
13
+ index: number;
14
+ pending: boolean;
15
+ disabled: boolean;
16
+ onGenerate: () => void;
17
+ }) {
18
+ const theme = SCENARIO_THEMES[index % SCENARIO_THEMES.length];
19
+ const Icon = theme.icon;
20
+
21
+ return (
22
+ <button
23
+ onClick={onGenerate}
24
+ disabled={disabled}
25
+ className="group relative flex min-h-[180px] w-full flex-col text-left rounded-2xl bg-white p-6 border-3 border-black shadow-[5px_5px_0px_rgba(0,0,0,1)] hover:translate-x-[-2px] hover:translate-y-[-2px] hover:shadow-[7px_7px_0px_rgba(0,0,0,1)] active:translate-x-[2px] active:translate-y-[2px] active:shadow-[1px_1px_0px_rgba(0,0,0,1)] transition-all duration-200 disabled:opacity-60 disabled:pointer-events-none justify-between overflow-hidden"
26
+ >
27
+ <div>
28
+ <div className="flex items-center justify-between mb-4">
29
+ <div className={`flex h-12 w-12 shrink-0 items-center justify-center rounded-xl border-2 border-black ${theme.tile} ${theme.tileText} shadow-[2px_2px_0px_rgba(0,0,0,1)]`}>
30
+ <Icon size={24} strokeWidth={2.5} />
31
+ </div>
32
+ <div className="flex h-9 w-9 items-center justify-center rounded-full border-2 border-black bg-[#FFD21E] text-black shadow-[2px_2px_0px_rgba(0,0,0,1)] group-hover:scale-105 transition-transform">
33
+ <ArrowRight size={18} strokeWidth={3} className="group-hover:translate-x-0.5 transition-transform" />
34
+ </div>
35
+ </div>
36
+ <h3 className="text-lg font-black text-black tracking-tight leading-snug mb-2 line-clamp-1 uppercase">{scenario.title}</h3>
37
+ <p className="line-clamp-2 text-xs font-bold leading-relaxed text-gray-700">{scenario.description}</p>
38
+ </div>
39
+
40
+ <div className="pt-4 mt-4 border-t-2 border-black w-full flex items-center justify-between">
41
+ <span className="text-xs font-black tracking-wider text-black uppercase">
42
+ Generate fresh
43
+ </span>
44
+ {pending ? (
45
+ <span className="h-4 w-4 rounded-full border-2 border-black border-t-transparent animate-spin" />
46
+ ) : null}
47
+ </div>
48
+ </button>
49
+ );
50
+ }
app/(app)/practice/ScenarioLibrary.tsx ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { DIALOGUE_SCENARIOS } from "@/lib/dialogue-scenarios";
2
+ import { ScenarioCard } from "./ScenarioCard";
3
+
4
+ export function ScenarioLibrary({
5
+ activeScenario,
6
+ onGenerate,
7
+ }: {
8
+ activeScenario: string | null;
9
+ onGenerate: (scenario: string, label: string) => void;
10
+ }) {
11
+ return (
12
+ <div className="space-y-6">
13
+ <div className="flex flex-col gap-2">
14
+ <span className="text-xs font-black uppercase tracking-wider text-black bg-[#E6FBFA] border-2 border-black px-2.5 py-1 rounded-md shadow-[1px_1px_0px_rgba(0,0,0,1)] inline-block w-fit">
15
+ Starter packs
16
+ </span>
17
+ <h2 className="text-2xl sm:text-3xl font-black tracking-tight text-black uppercase">Scenario Library</h2>
18
+ <div className="text-xs font-bold text-gray-500 uppercase tracking-widest">
19
+ {DIALOGUE_SCENARIOS.length} scenarios available
20
+ </div>
21
+ </div>
22
+
23
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
24
+ {DIALOGUE_SCENARIOS.map((scenario, index) => (
25
+ <ScenarioCard
26
+ key={scenario.title}
27
+ scenario={scenario}
28
+ index={index}
29
+ pending={activeScenario === scenario.title}
30
+ disabled={activeScenario !== null}
31
+ onGenerate={() => onGenerate(scenario.scenario, scenario.title)}
32
+ />
33
+ ))}
34
+ </div>
35
+ </div>
36
+ );
37
+ }
app/(app)/practice/StageItem.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Check, type LucideIcon } from "lucide-react";
4
+
5
+ export function StageItem({
6
+ icon: Icon,
7
+ label,
8
+ description,
9
+ isDone,
10
+ isActive,
11
+ }: {
12
+ icon: LucideIcon;
13
+ label: string;
14
+ description: string;
15
+ isDone: boolean;
16
+ isActive: boolean;
17
+ }) {
18
+ return (
19
+ <li
20
+ className={`flex items-start gap-3 rounded-xl border-2 px-3.5 py-3 transition-all duration-200 ${
21
+ isActive
22
+ ? "border-black bg-[#FFF7D6] shadow-[2px_2px_0px_rgba(0,0,0,1)] translate-x-[-1px] translate-y-[-1px]"
23
+ : isDone
24
+ ? "border-[#0EA5A4] bg-[#E6FBFA] opacity-80"
25
+ : "border-gray-200 bg-gray-50 opacity-60"
26
+ }`}
27
+ >
28
+ <div
29
+ className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border-2 transition-colors ${
30
+ isActive
31
+ ? "border-black bg-white text-black"
32
+ : isDone
33
+ ? "border-[#0EA5A4] bg-[#0EA5A4] text-white"
34
+ : "border-gray-300 bg-white text-gray-400"
35
+ }`}
36
+ >
37
+ {isDone ? <Check size={20} strokeWidth={3} /> : <Icon size={20} strokeWidth={2.5} />}
38
+ </div>
39
+ <div className="min-w-0 flex-1">
40
+ <div
41
+ className={`text-base font-black ${
42
+ isActive ? "text-black" : isDone ? "text-[#0a6e6d]" : "text-gray-500"
43
+ }`}
44
+ >
45
+ {label}
46
+ {isActive && (
47
+ <span className="ml-2 inline-flex gap-1 align-middle">
48
+ <span className="h-1.5 w-1.5 animate-bounce rounded-full bg-black [animation-delay:-0.3s]" />
49
+ <span className="h-1.5 w-1.5 animate-bounce rounded-full bg-black [animation-delay:-0.15s]" />
50
+ <span className="h-1.5 w-1.5 animate-bounce rounded-full bg-black" />
51
+ </span>
52
+ )}
53
+ </div>
54
+ <div
55
+ className={`mt-1 text-xs font-bold leading-relaxed ${
56
+ isActive ? "text-gray-700" : isDone ? "text-[#0a6e6d]/80" : "text-gray-400"
57
+ }`}
58
+ >
59
+ {description}
60
+ </div>
61
+ </div>
62
+ </li>
63
+ );
64
+ }
app/(app)/practice/[storyId]/StoryLoader.tsx ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from "next/link";
2
+ import { ArrowLeft } from "lucide-react";
3
+
4
+ export function StoryLoader() {
5
+ return (
6
+ <div className="duo-card mx-auto max-w-md p-8 text-center">
7
+ <p className="text-lg font-black text-[#3C3C3C]">Story no longer available</p>
8
+ <p className="mt-2 text-sm font-bold leading-6 text-[#777777]">
9
+ Generated stories are not stored or kept as a history in PraxaLing.
10
+ </p>
11
+ <Link href="/practice" className="mt-4 inline-flex items-center gap-2 text-sm font-black text-[#3B82F6]">
12
+ <ArrowLeft size={16} />
13
+ Back to practice
14
+ </Link>
15
+ </div>
16
+ );
17
+ }
app/(app)/practice/[storyId]/StoryQuiz.tsx ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { motion } from "framer-motion";
5
+ import { BookOpen, CheckCircle } from "lucide-react";
6
+
7
+ type StoryQuestion = {
8
+ question: string;
9
+ options: string[];
10
+ correctIndex: number;
11
+ explanation: string;
12
+ };
13
+
14
+ const EASE = [0.16, 1, 0.3, 1] as const;
15
+
16
+ export function StoryQuiz({ storyId, content }: { storyId: string; content: string }) {
17
+ const [loading, setLoading] = useState(false);
18
+ const [questions, setQuestions] = useState<StoryQuestion[] | null>(null);
19
+ const [answers, setAnswers] = useState<Record<number, number>>({});
20
+ const [error, setError] = useState<string | null>(null);
21
+
22
+ const answeredAll = questions !== null && questions.length > 0 && Object.keys(answers).length === questions.length;
23
+ const score = answeredAll ? questions.filter((q, i) => answers[i] === q.correctIndex).length : null;
24
+
25
+ async function loadQuiz() {
26
+ setLoading(true);
27
+ setError(null);
28
+ try {
29
+ const res = await fetch(`/api/stories/${storyId}/questions`, {
30
+ method: "POST",
31
+ headers: { "content-type": "application/json" },
32
+ body: JSON.stringify({ content }),
33
+ });
34
+ if (!res.ok) throw new Error("failed");
35
+ const data = await res.json();
36
+ setQuestions(Array.isArray(data) ? data : []);
37
+ } catch {
38
+ setError("Could not load questions. Try again.");
39
+ } finally {
40
+ setLoading(false);
41
+ }
42
+ }
43
+
44
+ function handleAnswer(qIdx: number, oIdx: number) {
45
+ setAnswers((prev) => (prev[qIdx] !== undefined ? prev : { ...prev, [qIdx]: oIdx }));
46
+ }
47
+
48
+ return (
49
+ <section className="duo-card p-5 sm:p-6">
50
+ <div className="mb-5 flex items-center gap-3">
51
+ <div className="grid h-11 w-11 place-items-center rounded-2xl bg-[#EFF6FF] text-[#3B82F6]">
52
+ <BookOpen size={22} />
53
+ </div>
54
+ <div>
55
+ <h2 className="text-lg font-black text-[#3C3C3C]">Test yourself</h2>
56
+ <div className="text-xs font-bold text-[#777777]">Answer every question to finish the lesson.</div>
57
+ </div>
58
+ </div>
59
+
60
+ {!questions && !loading && !error ? (
61
+ <button onClick={loadQuiz} className="btn-duo btn-duo-primary w-full sm:w-auto">
62
+ Generate questions
63
+ </button>
64
+ ) : null}
65
+
66
+ {loading ? <p className="text-sm font-bold text-[#777777]">Generating questions...</p> : null}
67
+
68
+ {error ? (
69
+ <div className="space-y-3">
70
+ <p className="text-sm font-bold text-[#BE123C]">{error}</p>
71
+ <button onClick={loadQuiz} className="btn-duo btn-duo-white text-sm">
72
+ Retry
73
+ </button>
74
+ </div>
75
+ ) : null}
76
+
77
+ {questions && questions.length === 0 ? (
78
+ <p className="text-sm font-bold text-[#777777]">No questions were generated for this story.</p>
79
+ ) : null}
80
+
81
+ {questions && questions.length > 0 ? (
82
+ <div className="space-y-7">
83
+ {questions.map((q, questionIndex) => (
84
+ <QuizQuestion
85
+ key={questionIndex}
86
+ question={q}
87
+ index={questionIndex}
88
+ answer={answers[questionIndex]}
89
+ onAnswer={(optionIndex) => handleAnswer(questionIndex, optionIndex)}
90
+ />
91
+ ))}
92
+
93
+ {score !== null ? <QuizScore score={score} total={questions.length} /> : null}
94
+ </div>
95
+ ) : null}
96
+ </section>
97
+ );
98
+ }
99
+
100
+ function QuizQuestion({
101
+ question,
102
+ index,
103
+ answer,
104
+ onAnswer,
105
+ }: {
106
+ question: StoryQuestion;
107
+ index: number;
108
+ answer: number | undefined;
109
+ onAnswer: (optionIndex: number) => void;
110
+ }) {
111
+ const made = answer !== undefined;
112
+
113
+ return (
114
+ <motion.div
115
+ initial={{ opacity: 0, y: 12 }}
116
+ animate={{ opacity: 1, y: 0 }}
117
+ transition={{ duration: 0.45, delay: index * 0.05, ease: EASE }}
118
+ className="space-y-3"
119
+ >
120
+ <p className="text-base font-black text-[#3C3C3C]">
121
+ {index + 1}. {question.question}
122
+ </p>
123
+ <div className="grid gap-3">
124
+ {question.options.map((option, optionIndex) => {
125
+ const chosen = answer === optionIndex;
126
+ const correct = optionIndex === question.correctIndex;
127
+ let className = "duo-card duo-card-interactive p-4 text-left text-sm font-black text-black";
128
+
129
+ if (made && correct) className = "rounded-2xl border-3 border-black bg-[#0EA5A4] p-4 text-left text-sm font-black text-white shadow-[3px_3px_0px_rgba(0,0,0,1)]";
130
+ if (made && chosen && !correct) className = "rounded-2xl border-3 border-black bg-[#FF8080] p-4 text-left text-sm font-black text-black shadow-[3px_3px_0px_rgba(0,0,0,1)]";
131
+ if (made && !chosen && !correct) className = "rounded-2xl border-2 border-gray-300 bg-gray-50 p-4 text-left text-sm font-black text-gray-400 shadow-[2px_2px_0px_rgba(0,0,0,0.15)]";
132
+
133
+ return (
134
+ <button key={optionIndex} disabled={made} onClick={() => onAnswer(optionIndex)} className={className}>
135
+ {option}
136
+ </button>
137
+ );
138
+ })}
139
+ </div>
140
+ {made ? <p className="text-xs font-bold leading-5 text-[#777777]">{question.explanation}</p> : null}
141
+ </motion.div>
142
+ );
143
+ }
144
+
145
+ function QuizScore({ score, total }: { score: number; total: number }) {
146
+ return (
147
+ <motion.div
148
+ initial={{ opacity: 0, scale: 0.96 }}
149
+ animate={{ opacity: 1, scale: 1 }}
150
+ transition={{ duration: 0.45, ease: EASE }}
151
+ className="rounded-2xl border-3 border-black bg-[#E6FBFA] p-6 text-center shadow-[5px_5px_0px_rgba(0,0,0,1)]"
152
+ >
153
+ <CheckCircle size={36} strokeWidth={2.5} className="mx-auto text-[#0B7C7B]" />
154
+ <div className="mt-2 text-3xl font-black text-[#0B7C7B]">
155
+ {score} / {total}
156
+ </div>
157
+ <p className="mt-1 text-sm font-black uppercase tracking-wide text-[#0B7C7B]">
158
+ {score === total ? "Perfect score!" : "Keep reading to improve!"}
159
+ </p>
160
+ </motion.div>
161
+ );
162
+ }
app/(app)/practice/[storyId]/StoryReader.tsx ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+ import { StoryText } from "./StoryText";
5
+ import { WordPopup } from "./WordPopup";
6
+ import { VocabList } from "./VocabList";
7
+ import { StoryQuiz } from "./StoryQuiz";
8
+
9
+ type Vocab = { word: string; gloss: string };
10
+
11
+ type Props = {
12
+ storyId: string;
13
+ content: string;
14
+ vocab: Vocab[];
15
+ fromLang: string;
16
+ toLang: string;
17
+ };
18
+
19
+ type Selection = { word: string; rect: DOMRect } | null;
20
+
21
+ export function StoryReader({ storyId, content, vocab, fromLang, toLang }: Props) {
22
+ const [selection, setSelection] = useState<Selection>(null);
23
+ const [translation, setTranslation] = useState<string | null>(null);
24
+ const [translating, setTranslating] = useState(false);
25
+ // Increments per lookup so a slow response can't overwrite a newer word's translation.
26
+ const lookupSeq = useRef(0);
27
+
28
+ const vocabMap = useMemo(() => {
29
+ const map = new Map<string, string>();
30
+ for (const v of vocab) map.set(v.word.toLowerCase(), v.gloss);
31
+ return map;
32
+ }, [vocab]);
33
+
34
+ useEffect(() => {
35
+ fetch(`/api/stories/${storyId}/read`, { method: "POST" }).catch(() => {});
36
+ }, [storyId]);
37
+
38
+ async function handleWord(word: string, rect: DOMRect) {
39
+ const clean = word.replace(/[^\p{L}\p{M}\p{N}\-']+/gu, "");
40
+ if (!clean) return;
41
+ const seq = ++lookupSeq.current;
42
+ setSelection({ word: clean, rect });
43
+ setTranslation(null);
44
+
45
+ const pre = vocabMap.get(clean.toLowerCase());
46
+ if (pre) {
47
+ setTranslation(pre);
48
+ return;
49
+ }
50
+
51
+ setTranslating(true);
52
+ try {
53
+ const res = await fetch("/api/translate", {
54
+ method: "POST",
55
+ headers: { "content-type": "application/json" },
56
+ body: JSON.stringify({ text: clean, from: fromLang, to: toLang }),
57
+ });
58
+ const data = await res.json();
59
+ if (seq === lookupSeq.current) setTranslation(data.translation ?? data.error ?? "-");
60
+ } catch {
61
+ if (seq === lookupSeq.current) setTranslation("-");
62
+ } finally {
63
+ if (seq === lookupSeq.current) setTranslating(false);
64
+ }
65
+ }
66
+
67
+ return (
68
+ <div className="relative space-y-6">
69
+ <StoryText content={content} onWord={(word, rect) => void handleWord(word, rect)} />
70
+
71
+ {selection ? (
72
+ <WordPopup
73
+ word={selection.word}
74
+ rect={selection.rect}
75
+ translation={translation}
76
+ translating={translating}
77
+ onClose={() => setSelection(null)}
78
+ />
79
+ ) : null}
80
+
81
+ <VocabList vocab={vocab} />
82
+
83
+ <StoryQuiz storyId={storyId} content={content} />
84
+ </div>
85
+ );
86
+ }
app/(app)/practice/[storyId]/StoryText.tsx ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ type Props = {
4
+ content: string;
5
+ onWord: (word: string, rect: DOMRect) => void;
6
+ };
7
+
8
+ export function StoryText({ content, onWord }: Props) {
9
+ return (
10
+ <section className="duo-card p-5 sm:p-7">
11
+ {content.split(/\n\n+/).map((paragraph, paragraphIndex) => (
12
+ <p key={paragraphIndex} className="mb-5 text-lg font-bold leading-8 text-[#3C3C3C]">
13
+ {tokenize(paragraph).map((token, tokenIndex) =>
14
+ token.word ? (
15
+ <span
16
+ key={tokenIndex}
17
+ className="cursor-pointer rounded-md px-0.5 underline decoration-[#0EA5A4] decoration-2 underline-offset-4 transition hover:bg-[#CCFBF1]"
18
+ onClick={(e) => {
19
+ const rect = (e.target as HTMLElement).getBoundingClientRect();
20
+ onWord(token.text, rect);
21
+ }}
22
+ >
23
+ {token.text}
24
+ </span>
25
+ ) : (
26
+ <span key={tokenIndex}>{token.text}</span>
27
+ ),
28
+ )}
29
+ </p>
30
+ ))}
31
+ </section>
32
+ );
33
+ }
34
+
35
+ function tokenize(text: string): Array<{ text: string; word: boolean }> {
36
+ const out: Array<{ text: string; word: boolean }> = [];
37
+ const re = /([\p{L}\p{M}][\p{L}\p{M}\p{N}\-']*)|([^\p{L}\p{M}\p{N}]+)/gu;
38
+ for (const match of text.matchAll(re)) {
39
+ if (match[1]) out.push({ text: match[1], word: true });
40
+ else if (match[2]) out.push({ text: match[2], word: false });
41
+ }
42
+ return out;
43
+ }
app/(app)/practice/[storyId]/VocabList.tsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ type Vocab = { word: string; gloss: string };
2
+
3
+ export function VocabList({ vocab }: { vocab: Vocab[] }) {
4
+ if (vocab.length === 0) return null;
5
+ return (
6
+ <section className="duo-card p-5">
7
+ <h2 className="duo-eyebrow">Key vocabulary</h2>
8
+ <dl className="mt-4 grid gap-3 sm:grid-cols-2">
9
+ {vocab.map((v) => (
10
+ <div key={v.word} className="duo-soft-panel p-4">
11
+ <dt className="text-base font-black text-[#3C3C3C]">{v.word}</dt>
12
+ <dd className="mt-1 text-sm font-bold text-[#777777]">{v.gloss}</dd>
13
+ </div>
14
+ ))}
15
+ </dl>
16
+ </section>
17
+ );
18
+ }