diff --git a/.agents/skills/frontend-designer.md b/.agents/skills/frontend-designer.md new file mode 100644 index 0000000..afcbfd4 --- /dev/null +++ b/.agents/skills/frontend-designer.md @@ -0,0 +1,42 @@ +--- +name: frontend-design +description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics. +license: Complete terms in LICENSE.txt +--- + +This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices. + +The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints. + +## Design Thinking + +Before coding, understand the context and commit to a BOLD aesthetic direction: +- **Purpose**: What problem does this interface solve? Who uses it? +- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction. +- **Constraints**: Technical requirements (framework, performance, accessibility). +- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember? + +**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity. + +Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is: +- Production-grade and functional +- Visually striking and memorable +- Cohesive with a clear aesthetic point-of-view +- Meticulously refined in every detail + +## Frontend Aesthetics Guidelines + +Focus on: +- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font. +- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. +- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise. +- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density. +- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays. + +NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character. + +Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations. + +**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well. + +Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision. diff --git a/.crush.json b/.crush.json new file mode 100644 index 0000000..2ca2fe9 --- /dev/null +++ b/.crush.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://charm.land/crush.json", + "permissions": { + "allowed_tools": [ + "view", + "ls", + "grep", + "glob", + "edit", + "go", + "just", + "agent", + "agentic_fetch" + ] + }, + "options": { + "initialize_as": "AGENTS.md", + "skills_paths": ["./.agents/skills"] + }, + "lsp": { + "typescript": { + "command": "typescript-language-server", + "args": ["--stdio"] + }, + "omnisharp": { + "command": "wsl", + "args": ["--", "bash", "-lc", "\"OmniSharp\""] + } + } +} diff --git a/.gitignore b/.gitignore index 29a138d..d914c32 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,41 @@ -# .NET -bin/ -obj/ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ dist/ -*.dll -*.exe -*.pdb +web-build/ +expo-env.d.ts -# IDE -.vs/ -.vscode/ -.idea/ -*.user -*.suo +# Native +.kotlin/ +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision -# OS +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS .DS_Store -Thumbs.db +*.pem -# Project specific -*.csv -*.dlens -*.apk +# local env files +.env*.local + +# typescript +*.tsbuildinfo + +# generated native folders +/ios +/android diff --git a/.just/emu.just b/.just/emu.just new file mode 100644 index 0000000..7152410 --- /dev/null +++ b/.just/emu.just @@ -0,0 +1,35 @@ +set shell := ["C:/Program Files/Git/usr/bin/bash.exe", "-c"] +set unstable := true + +android_sdk := replace(env('LOCALAPPDATA'), '\', '/') / "Android/Sdk" +adb := android_sdk / "platform-tools/adb.exe" +avd := android_sdk / "cmdline-tools/latest/bin/avdmanager.bat" +emulator := android_sdk / "emulator/emulator.exe" +camera_virtual := "-camera-back virtualscene -virtualscene-poster wall=\"" + (justfile_directory() / "TestImages/reference_alpha/serra_angel.jpg") + "\"" +camera_webcam := "-camera-back webcam0 -camera-front webcam0" + +default camera="virtual": + {{ emulator }} -avd Pixel_6 {{ if camera == "virtual" { camera_virtual } else { camera_webcam } }} -no-snapshot-load -gpu host + +install: + {{ avd }} delete avd -n Pixel_6 + {{ avd }} create avd -n Pixel_6 -k "system-images;android-36;google_apis_playstore;x86_64" -d pixel_6 + +# Wait for emulator to fully boot (timeout after 2 minutes) +[script] +emu-wait: + # Wait for Android emulator to boot with timeout + TIMEOUT=120 + + echo "Waiting for emulator to boot..." + + for ((i=TIMEOUT; i>0; i--)); do + if [ "$({{ adb }} shell getprop sys.boot_completed 2>/dev/null)" = "1" ]; then + echo "Emulator ready" + exit 0 + fi + sleep 1 + done + + echo "Emulator failed to boot within 2 minutes" + exit 1 diff --git a/.justfile b/.justfile index e62b769..df476fa 100644 --- a/.justfile +++ b/.justfile @@ -1,19 +1,80 @@ -# Scry build recipes +# Scry development commands -# Default recipe - show available commands -default: - @just --list +set shell := ["C:/Program Files/Git/usr/bin/bash.exe", "-c"] +set unstable := true -# Build both standard and embedded versions for all platforms -build apk="delver.apk": - rm -rf dist - dotnet publish -c Release -r win-x64 -o dist/win-x64/standard - dotnet publish -c Release -r win-x64 -p:EmbeddedApk={{apk}} -o dist/win-x64/embedded - dotnet publish -c Release -r linux-x64 -o dist/linux-x64/standard - dotnet publish -c Release -r linux-x64 -p:EmbeddedApk={{apk}} -o dist/linux-x64/embedded - dotnet publish -c Release -r osx-x64 -o dist/osx-x64/standard - dotnet publish -c Release -r osx-x64 -p:EmbeddedApk={{apk}} -o dist/osx-x64/embedded +mod emu '.just/emu.just' -# Clean build artifacts -clean: - rm -rf bin obj dist +# Android SDK paths + +android_sdk := replace(env('LOCALAPPDATA'), '\', '/') / "Android/Sdk" +adb := android_sdk / "platform-tools/adb.exe" + +[private] +@default: + just --list + +# Build a project +build project="src/Scry.App" target="net10.0-android": + @echo "Building {{ project }}..." + dotnet build {{ project }} -f {{ target }} -c Debug + @echo "Build complete" + +# Publish a project (creates distributable) +publish project="src/Scry.App" target="net10.0-android": + @echo "Publishing {{ project }} (this takes a while)..." + dotnet publish {{ project }} -f {{ target }} -c Release + @echo "Publish complete" + +# Install APK to emulator/device +install: + {{ adb }} install -r src/Scry.App/bin/Release/net10.0-android/publish/land.charm.scry-Signed.apk + +# Launch the app on emulator/device +launch: + {{ adb }} shell am start -n land.charm.scry/crc64fb23cc0d511b0157.MainActivity + +# Publish, install, and launch +run: (publish "src/Scry.App") install launch + +# View app crash logs +logs: + {{ adb }} logcat -d | grep -iE "land.charm.scry|scry|mono|dotnet" | tail -80 + +# Run tests +test: + dotnet test test/Scry.Tests + +# Generate the card hash database from Scryfall +gen-db: (build "tools/DbGenerator" "net10.0") + @echo "Running Database generator (this takes a while)..." + dotnet run --project tools/DbGenerator --no-build -- src/Scry.App/Resources/Raw/card_hashes.db + @echo "Completed generating the database" + +# Start Expo dev server with Convex (hot reload) +dev: + bun run dev + +# Start just the Expo dev server +start: + bun run dev:expo + +# Start just Convex dev server +convex-dev: + bun run dev:convex + +# Run Expo app on Android emulator +android: + bun run android + +# Install Expo app dependencies +expo-install: + bun install + +# Run hash migration to Convex +expo-migrate: + bun run migrate + +# TypeScript check for Expo app +expo-typecheck: + bun run typecheck diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..78b4cc7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,288 @@ +# Agent Instructions + +## Overview + +Scry is a Magic: The Gathering card scanner app built with Expo (React Native) and Convex. It uses perceptual hashing to match photographed cards against a database of known card images from Scryfall. + +**Tech Stack:** +- **Frontend**: Expo/React Native with Expo Router (file-based routing) +- **Backend**: Convex (serverless functions + real-time database) +- **Image Processing**: React Native Skia, fast-opencv +- **Camera**: Adaptive (expo-camera in dev, react-native-vision-camera in production) +- **Auth**: Convex Auth with Zitadel OIDC (GDPR-compliant, no PII stored) +- **Package Manager**: Bun (not npm/yarn) + +## Build Commands + +Use `just` commands (defined in `.justfile`): + +| Task | Command | Notes | +|------|---------|-------| +| Start dev server | `just dev` | Runs Convex + Expo together | +| Expo only | `just start` | Just the Expo dev server | +| Convex only | `just convex-dev` | Just the Convex dev server | +| Run on Android | `just android` | Starts Android emulator | +| Install deps | `just expo-install` | Runs `bun install` | +| Migrate hashes | `just expo-migrate` | Migrate card hashes to Convex | +| Type check | `just expo-typecheck` | TypeScript check | +| Start emulator | `just emu` | Virtual camera (submodule) | + +### Direct Bun Commands + +```bash +bun install # Install dependencies +bun run dev # Convex + Expo hot reload +bun run dev:convex # Convex dev server only +bun run dev:expo # Expo dev server only +bun run android # Run on Android +bun run migrate # Migrate hashes to Convex +bun run typecheck # TypeScript check +bunx convex dev # Convex CLI +``` + +## Project Structure + +``` +app/ # Expo Router pages +├── _layout.tsx # Root layout (Convex + HashCache providers) +├── modal.tsx # Card details modal +├── +not-found.tsx # 404 page +└── (tabs)/ # Tab navigation group + ├── _layout.tsx # Tab bar layout + ├── index.tsx # Collection tab (home) + ├── scan.tsx # Camera scan tab + └── settings.tsx # Settings tab + +components/ +├── camera/ # Adaptive camera system +│ ├── index.tsx # AdaptiveCamera wrapper +│ ├── ExpoCamera.tsx # expo-camera (Expo Go) +│ └── VisionCamera.tsx # react-native-vision-camera (production) +└── *.tsx # Shared UI components + +convex/ # Backend (Convex functions + schema) +├── schema.ts # Database schema +├── auth.ts # Zitadel OIDC configuration +├── http.ts # HTTP endpoints for auth +├── cards.ts # Card queries/mutations +├── collections.ts # User collection functions +├── users.ts # User functions +├── scanHistory.ts # Scan history functions +└── _generated/ # Auto-generated types + +lib/ +├── recognition/ # Card recognition pipeline +│ ├── recognitionService.ts # Main recognition logic +│ ├── cardDetector.ts # Edge detection, find card quad +│ ├── perspectiveCorrection.ts # Warp to rectangle +│ ├── clahe.ts # CLAHE lighting normalization +│ ├── perceptualHash.ts # 192-bit color hash (24 bytes) +│ ├── imageUtils.ts # Resize, rotate, grayscale +│ ├── imageLoader.ts # Load/resize images +│ └── skiaDecoder.ts # Decode images with Skia +├── hooks/ # React hooks +│ ├── useAuth.ts # OAuth flow with expo-auth-session +│ ├── useCamera.ts # Adaptive camera permissions +│ ├── useConvex.ts # Convex data hooks +│ ├── useSync.ts # Hash sync hook +│ └── useUserProfile.ts # Fetch profile from Zitadel +├── context/ # React contexts +│ └── HashCacheContext.tsx # In-memory hash cache +└── db/ # Local database utilities + ├── localDatabase.ts # SQLite wrapper + └── syncService.ts # Sync with Convex + +scripts/ +└── migrate-hashes.ts # Migration script + +TestImages/ # Test images (225 files) +``` + +## Architecture + +### Recognition Pipeline + +``` +Camera Image + │ + ▼ +┌─────────────────────┐ +│ loadImageAsBase64 │ ← Resize to 480×640 +└─────────────────────┘ + │ + ▼ +┌─────────────────────┐ +│ decodeImageBase64 │ ← Skia decodes to RGBA pixels +└─────────────────────┘ + │ + ▼ +┌─────────────────────┐ +│ detectCard │ ← Edge detection, find card quad +│ (optional) │ +└─────────────────────┘ + │ + ▼ +┌─────────────────────┐ +│ warpPerspective │ ← Warp detected quad to rectangle +└─────────────────────┘ + │ + ▼ +┌─────────────────────┐ +│ applyCLAHE │ ← Lighting normalization +└─────────────────────┘ + │ + ▼ +┌─────────────────────┐ +│ computeColorHash │ ← Compute 192-bit color hash (24 bytes) +└─────────────────────┘ + │ + ▼ +┌─────────────────────┐ +│ recognizeCard │ ← Hamming distance match against cache +└─────────────────────┘ +``` + +### Data Model (Convex Schema) + +| Table | Purpose | +|-------|---------| +| `users` | Minimal auth (no PII, GDPR compliant) | +| `cards` | Card printings with 24-byte perceptual hashes | +| `oracles` | Abstract game cards (one per unique card name) | +| `sets` | MTG sets with metadata | +| `collections` | User card collections | +| `scanHistory` | Scan history with confidence scores | +| `metadata` | Sync metadata | + +### GDPR Compliance + +- Database stores **no user PII** - only auth subject ID +- User profile (name, email, image) fetched from Zitadel userinfo endpoint on demand +- Profile held in memory only, never persisted + +### Adaptive Camera System + +The app detects its runtime environment and uses the appropriate camera: + +- **Expo Go** (`Constants.appOwnership === "expo"`): Uses `expo-camera` +- **Production builds**: Uses `react-native-vision-camera` + +Both expose the same `CameraHandle` interface with `takePhoto()`. + +## Key Algorithms + +### Perceptual Hash (pHash) + +Color-aware 192-bit hash: +1. Resize to 32×32 +2. For each RGB channel: + - Compute 2D DCT + - Extract 8×8 low-frequency coefficients (skip DC) + - Compare each to median → 63 bits per channel +3. Concatenate R, G, B hashes → 24 bytes (192 bits) + +Matching uses Hamming distance with threshold ≤25 bits and minimum confidence 85%. + +### CLAHE (Contrast Limited Adaptive Histogram Equalization) + +Applied in LAB color space to L channel only: +- Tile-based histogram equalization (8×8 tiles) +- Clip limit prevents over-amplification of noise +- Bilinear interpolation between tiles for smooth output + +## Code Conventions + +### General + +- **TypeScript**: Strict mode enabled +- **Formatting**: Prettier defaults +- **Package Manager**: Bun (never use npm/yarn) + +### React/React Native + +- Functional components with hooks +- Expo Router for navigation (file-based) +- Convex hooks for data (`useQuery`, `useMutation`) + +### Naming + +- Hooks: `useCardHashes`, `useCameraPermission` +- Components: PascalCase (`AdaptiveCamera`, `ScanScreen`) +- Files: camelCase for modules, PascalCase for components +- Convex functions: camelCase (`cards.ts`, `getByScryfallId`) + +### Convex Backend + +- Queries are reactive and cached +- Mutations are transactional +- Use `v.` validators for all arguments +- Index frequently queried fields + +## Environment Variables + +### Convex Backend (`convex/.env.local`) + +```bash +AUTH_ZITADEL_ID=your-client-id +AUTH_ZITADEL_SECRET=your-client-secret +AUTH_ZITADEL_ISSUER=https://your-zitadel-instance +``` + +### Client Side (`.env`) + +```bash +EXPO_PUBLIC_CONVEX_URL=https://your-deployment.convex.cloud +EXPO_PUBLIC_ZITADEL_ISSUER=https://your-zitadel-instance +``` + +## Common Tasks + +### Adding a New Convex Function + +1. Add function to appropriate file in `convex/` (e.g., `cards.ts`) +2. Run `bunx convex dev` to regenerate types +3. Import from `convex/_generated/api` in client code + +### Testing Recognition + +1. Add test images to `TestImages/` +2. Use the scan tab in the app +3. Check console logs for `[Scry]` prefixed messages + +### Debugging Camera Issues + +- In Expo Go: Uses `expo-camera`, check for "Dev mode" indicator +- In production: Uses Vision Camera, requires EAS build + +## Dependencies + +### Core + +- `expo` ~54.0.33 +- `expo-router` ~6.0.23 +- `convex` ^1.31.7 +- `@convex-dev/auth` ^0.0.90 +- `react` 19.1.0 +- `react-native` 0.81.5 + +### Image Processing + +- `@shopify/react-native-skia` ^2.4.18 +- `react-native-fast-opencv` ^0.4.7 + +### Camera + +- `expo-camera` ^17.0.10 (Expo Go) +- `react-native-vision-camera` ^4.7.3 (production) + +### Auth + +- `expo-auth-session` ^7.0.10 +- `expo-secure-store` ^15.0.8 + +## External Resources + +- [Scryfall API](https://scryfall.com/docs/api) - Card data source +- [Convex Docs](https://docs.convex.dev/) - Backend documentation +- [Expo Router](https://docs.expo.dev/router/introduction/) - Navigation +- [docs/CARD_RECOGNITION.md](docs/CARD_RECOGNITION.md) - Recognition architecture diff --git a/Program.cs b/Program.cs deleted file mode 100644 index 06ed5ea..0000000 --- a/Program.cs +++ /dev/null @@ -1,369 +0,0 @@ -using System.CommandLine; -using System.Reflection; -using System.Text; -using ICSharpCode.SharpZipLib.Zip; -using Microsoft.Data.Sqlite; -using Spectre.Console; - -// Ensure UTF-8 output for Unicode characters -Console.OutputEncoding = Encoding.UTF8; - -var dlensArgument = new Argument("dlens"); -dlensArgument.Description = "Path to the .dlens database file"; - -var outputOption = new Option("--output", "-o"); -outputOption.Description = "Output CSV file path (defaults to collection.csv)"; - -var showTableOption = new Option("--show-table", "-t"); -showTableOption.Description = "Display the card collection as a table"; -showTableOption.DefaultValueFactory = _ => false; - -#if EMBEDDED_APK -var rootCommand = new RootCommand("Extract and display card data from Delver Lens") -{ - dlensArgument, - outputOption, - showTableOption -}; - -rootCommand.SetAction(async (parseResult, cancellationToken) => -{ - var dlensFile = parseResult.GetValue(dlensArgument)!; - var outputFile = parseResult.GetValue(outputOption) ?? new FileInfo("collection.csv"); - var showTable = parseResult.GetValue(showTableOption); - await ProcessFiles(null, dlensFile, outputFile, showTable); -}); -#else -var apkArgument = new Argument("apk"); -apkArgument.Description = "Path to the Delver Lens APK file"; - -var rootCommand = new RootCommand("Extract and display card data from Delver Lens") -{ - apkArgument, - dlensArgument, - outputOption, - showTableOption -}; - -rootCommand.SetAction(async (parseResult, cancellationToken) => -{ - var apkFile = parseResult.GetValue(apkArgument)!; - var dlensFile = parseResult.GetValue(dlensArgument)!; - var outputFile = parseResult.GetValue(outputOption) ?? new FileInfo("collection.csv"); - var showTable = parseResult.GetValue(showTableOption); - await ProcessFiles(apkFile, dlensFile, outputFile, showTable); -}); -#endif - -return await rootCommand.Parse(args).InvokeAsync(); - -async Task ProcessFiles(FileInfo? apkFile, FileInfo dlensFile, FileInfo outputFile, bool showTable) -{ -#if !EMBEDDED_APK - if (apkFile == null || !apkFile.Exists) - { - AnsiConsole.MarkupLine($"[red]APK file not found:[/] {apkFile?.FullName}"); - return; - } -#endif - - if (!dlensFile.Exists) - { - AnsiConsole.MarkupLine($"[red]dlens file not found:[/] {dlensFile.FullName}"); - return; - } - - List? scannedCards = null; - List? collection = null; - var steps = new[] { false, false, false }; - - Panel BuildPanel() - { - var content = $""" - [bold yellow]Progress[/] - - {Step(0, "Read scanned cards from dlens")} - {Step(1, "Resolve card data from APK")} - {Step(2, "Export collection to CSV")} - """; - - if (steps[2]) - { - content += $""" - - - [bold yellow]Summary[/] - - [blue]Your collection:[/] {collection!.Count} unique cards, {collection.Sum(c => c.Quantity)} total - [green]Exported to:[/] {outputFile.FullName} - - [bold yellow]How to import into Archidekt[/] - - 1. Go to [link]https://archidekt.com/collection[/] - 2. Click [yellow]Import[/] - 3. Click [yellow]Add manual column[/] [blue]6 times[/] - 4. Set the columns in order: - • Quantity → [blue]Quantity[/] - • Scryfall ID → [blue]Scryfall ID[/] - • Foil → [blue]Foil[/] - • Card Name → [blue]Ignore[/] - • Set Code → [blue]Ignore[/] - • Collector Number → [blue]Ignore[/] - 5. Set [yellow]Skip first row[/] to [blue]true[/] [grey](the CSV has a header)[/] - 6. Set the csv file by either dragging and dropping it, or clicking the upload box - 7. Click [yellow]Upload[/] - """; - } - - return new Panel(content) - { - Header = new PanelHeader(" Delver Lens → Archidekt "), - Border = BoxBorder.Rounded, - Padding = new Padding(2, 1) - }; - } - - var spinnerFrames = new[] { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" }; - var spinnerIndex = 0; - var currentStep = 0; - - string Step(int index, string text) - { - if (steps[index]) - return $"[green][[✓]][/] {text}"; - if (index == currentStep) - return $"[blue][[{spinnerFrames[spinnerIndex]}]][/] {text}"; - return $"[grey][[○]][/] [grey]{text}[/]"; - } - - // When piped, output CSV to stdout for composability - if (Console.IsOutputRedirected) - { - scannedCards = await GetScannedCards(dlensFile); - collection = await ResolveCollection(apkFile, scannedCards); - WriteCsvToStdout(collection); - return; - } - - // Interactive: use live display with progress panel - using var cts = new CancellationTokenSource(); - - await AnsiConsole.Live(BuildPanel()) - .StartAsync(async ctx => - { - // Spinner animation task - var spinnerTask = Task.Run(async () => - { - while (!cts.Token.IsCancellationRequested) - { - await Task.Delay(80, cts.Token).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); - spinnerIndex = (spinnerIndex + 1) % spinnerFrames.Length; - ctx.UpdateTarget(BuildPanel()); - } - }); - - scannedCards = await GetScannedCards(dlensFile); - steps[0] = true; - currentStep = 1; - ctx.UpdateTarget(BuildPanel()); - - collection = await ResolveCollection(apkFile, scannedCards); - steps[1] = true; - currentStep = 2; - ctx.UpdateTarget(BuildPanel()); - - await ExportCsv(collection, outputFile); - steps[2] = true; - ctx.UpdateTarget(BuildPanel()); - - cts.Cancel(); - await spinnerTask; - }); - - // Display table if requested (after live panel completes) - if (showTable) - { - DisplayCollection(collection!); - } -} - -async Task> ResolveCollection(FileInfo? apkFile, List scannedCards) -{ - var tempDbPath = Path.GetTempFileName(); - var cardIds = scannedCards.Select(c => c.CardId).ToHashSet(); - - try - { - // Get APK stream from embedded resource or file -#if EMBEDDED_APK - var assembly = Assembly.GetExecutingAssembly(); - await using var apkStream = assembly.GetManifestResourceStream("delver.apk") - ?? throw new Exception("Embedded APK resource not found"); -#else - await using var apkStream = apkFile!.OpenRead(); -#endif - - using (var zipFile = new ZipFile(apkStream)) - { - var entry = zipFile.GetEntry("res/raw/data.db"); - if (entry == null) - { - throw new Exception("Could not find res/raw/data.db in APK"); - } - - await using var zipStream = zipFile.GetInputStream(entry); - await using var outputStream = File.Create(tempDbPath); - await zipStream.CopyToAsync(outputStream); - } - - var cardData = new Dictionary(); - - await using (var connection = new SqliteConnection($"Data Source={tempDbPath};Mode=ReadOnly")) - { - await connection.OpenAsync(); - - await using var cmd = connection.CreateCommand(); - cmd.CommandText = @" - SELECT - c._id, - n.name, - e.tl_abb, - c.number, - c.scryfall_id - FROM cards c - JOIN names n ON c.name = n._id - JOIN editions e ON c.edition = e._id;"; - - await using var reader = await cmd.ExecuteReaderAsync(); - while (await reader.ReadAsync()) - { - var id = reader.GetInt32(0); - if (cardIds.Contains(id)) - { - cardData[id] = ( - reader.GetString(1), - reader.GetString(2), - reader.IsDBNull(3) ? "" : reader.GetString(3), - reader.IsDBNull(4) ? "" : reader.GetString(4) - ); - } - } - } - - var collection = new List(); - foreach (var scanned in scannedCards) - { - if (cardData.TryGetValue(scanned.CardId, out var data)) - { - collection.Add(new CollectionCard( - scanned.Quantity, - data.Name, - data.SetCode, - data.CollectorNumber, - data.ScryfallId, - scanned.Foil - )); - } - else - { - collection.Add(new CollectionCard( - scanned.Quantity, - $"Unknown (ID: {scanned.CardId})", - "", - "", - "", - scanned.Foil - )); - } - } - - return collection; - } - finally - { - SqliteConnection.ClearAllPools(); - if (File.Exists(tempDbPath)) - { - File.Delete(tempDbPath); - } - } -} - -void DisplayCollection(List collection) -{ - var table = new Table(); - table.Border = TableBorder.Rounded; - table.AddColumn("Qty"); - table.AddColumn("Name"); - table.AddColumn("Set"); - table.AddColumn("#"); - table.AddColumn("Foil"); - table.AddColumn("Scryfall ID"); - - foreach (var card in collection.OrderBy(c => c.Name).ThenBy(c => c.SetCode)) - { - table.AddRow( - card.Quantity.ToString(), - card.Name.Length > 30 ? card.Name[..27] + "..." : card.Name, - card.SetCode, - card.CollectorNumber, - card.Foil ? "[yellow]Yes[/]" : "", - card.ScryfallId.Length > 8 ? card.ScryfallId[..8] + "..." : card.ScryfallId - ); - } - - AnsiConsole.Write(table); -} - -async Task ExportCsv(List collection, FileInfo outputFile) -{ - var sb = new StringBuilder(); - sb.AppendLine("Quantity,Scryfall ID,Foil,Card Name,Set Code,Collector Number"); - - foreach (var card in collection.OrderBy(c => c.Name).ThenBy(c => c.SetCode)) - { - var foilStr = card.Foil ? "Foil" : "Normal"; - var name = card.Name.Contains(',') ? $"\"{card.Name}\"" : card.Name; - sb.AppendLine($"{card.Quantity},{card.ScryfallId},{foilStr},{name},{card.SetCode},{card.CollectorNumber}"); - } - - await File.WriteAllTextAsync(outputFile.FullName, sb.ToString()); -} - -void WriteCsvToStdout(List collection) -{ - Console.WriteLine("Quantity,Scryfall ID,Foil,Card Name,Set Code,Collector Number"); - - foreach (var card in collection.OrderBy(c => c.Name).ThenBy(c => c.SetCode)) - { - var foilStr = card.Foil ? "Foil" : "Normal"; - var name = card.Name.Contains(',') ? $"\"{card.Name}\"" : card.Name; - Console.WriteLine($"{card.Quantity},{card.ScryfallId},{foilStr},{name},{card.SetCode},{card.CollectorNumber}"); - } -} - -async Task> GetScannedCards(FileInfo dlensFile) -{ - var cards = new List(); - - await using var connection = new SqliteConnection($"Data Source={dlensFile.FullName};Mode=ReadOnly"); - await connection.OpenAsync(); - - await using var command = connection.CreateCommand(); - command.CommandText = "SELECT * FROM cards"; - - await using var reader = await command.ExecuteReaderAsync(); - while (await reader.ReadAsync()) - { - var cardId = reader.GetInt32(reader.GetOrdinal("card")); - var quantity = reader.GetInt32(reader.GetOrdinal("quantity")); - var foil = reader.GetInt32(reader.GetOrdinal("foil")) == 1; - - cards.Add(new ScannedCard(cardId, quantity, foil)); - } - - return cards; -} - -record ScannedCard(int CardId, int Quantity, bool Foil); -record CollectionCard(int Quantity, string Name, string SetCode, string CollectorNumber, string ScryfallId, bool Foil); diff --git a/Scry.csproj b/Scry.csproj deleted file mode 100644 index b3f1ef6..0000000 --- a/Scry.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - - Exe - net10.0 - enable - enable - true - true - true - true - none - false - - - - $(DefineConstants);EMBEDDED_APK - - - - - delver.apk - - - - - - - - - - - diff --git a/TestImages/README.md b/TestImages/README.md new file mode 100644 index 0000000..799ba29 --- /dev/null +++ b/TestImages/README.md @@ -0,0 +1,151 @@ +# Test Images + +This directory contains **225 reference images** for testing card recognition algorithms without requiring actual hardware. + +## Directory Structure + +| Category | Count | Description | +|----------|-------|-------------| +| `reference_alpha/` | 47 | Alpha edition reference cards (old frame) | +| `varying_quality/` | 38 | Different lighting, blur, exposure, angles | +| `single_cards/` | 19 | Individual card photos | +| `real_photos/` | 18 | Phone camera photos from Visions project | +| `foreign/` | 16 | Non-English cards (Japanese, German, French, etc.) | +| `worn/` | 15 | Heavily played, damaged, worn cards | +| `foil/` | 14 | Foil cards with holographic glare/reflections | +| `low_light/` | 14 | Poor lighting, glare, shadows, amateur photos | +| `tokens/` | 13 | Tokens and planeswalker emblems | +| `hands/` | 11 | Cards held in hand (partial visibility) | +| `ocr_test/` | 10 | Images optimized for OCR testing | +| `reference/` | 9 | High-quality reference scans | +| `multiple_cards/` | 6 | Multiple cards in frame | +| `augmented/` | 4 | Augmented training examples | +| `training_examples/` | 3 | ML training set samples | +| `angled/` | 2 | Perspective distortion | + +## Image Sources + +Images from open-source MIT-licensed projects: + +- [hj3yoo/mtg_card_detector](https://github.com/hj3yoo/mtg_card_detector) +- [tmikonen/magic_card_detector](https://github.com/tmikonen/magic_card_detector) +- [fortierq/mtgscan](https://github.com/fortierq/mtgscan) +- [LauriHursti/visions](https://github.com/LauriHursti/visions) +- [KLuml/CardScanner](https://github.com/KLuml/CardScanner) +- [dills122/MTG-Card-Analyzer](https://github.com/dills122/MTG-Card-Analyzer) +- [ryanlin/Turtle](https://github.com/ryanlin/Turtle) + +Additional images from: +- Reddit r/magicTCG (user-submitted photos) +- Flickr (Creative Commons) +- Card Kingdom / Face to Face Games grading guides +- Scryfall (foreign language card scans) + +## Usage + +```csharp +[Theory] +[InlineData("varying_quality/test1.jpg")] +[InlineData("angled/tilted_card_1.jpg")] +[InlineData("hands/hand_of_card_1.png")] +[InlineData("foil/rainbow_foil_secret_lair.jpg")] +[InlineData("worn/hp_shuffle_crease.webp")] +[InlineData("foreign/japanese_aang.jpg")] +public async Task RecognizeCard_VaryingConditions(string imagePath) +{ + using var stream = File.OpenRead(Path.Combine("TestImages", imagePath)); + var result = await _recognitionService.RecognizeCardAsync(stream); + + Assert.True(result.Success); + Assert.NotNull(result.Card); + Assert.True(result.Confidence >= 0.7f); +} +``` + +## Category Details + +### foil/ +Foil cards showing holographic effects that challenge recognition: +- Rainbow foils with color-shifting (`rainbow_foil_secret_lair.jpg`) +- Surge foils with holo stickers (`surge_foils_holo.jpeg`) +- Old-style foils (`old_foil_yawgmoth.jpg`) +- Textured/dragonscale foils (`dragonscale_foil.jpg`) +- Foil curling examples showing warping + +### worn/ +Heavily played and damaged cards: +- Edge whitening (`edge_white.png`, `very_good_*.jpg`) +- Scratches and scuffs (`scratch.png`, `hp_scratches.png`) +- Creases and bends (`hp_shuffle_crease.webp`, `bent_creased.jpg`) +- Binder damage (`hp_binder_bite_*.webp`) +- Water damage (`hp_water_warping.png`) +- Corner damage (`hp_compromised_corner.webp`) + +### low_light/ +Poor lighting and amateur photography conditions: +- Glare from toploaders/sleeves (`glare_toploader.png`) +- Direct light causing hotspots (`glare_straight_down.jpg`) +- Depth of field blur (`dof_blur_amateur.jpg`) +- Amateur condition photos with shadows +- Flickr collection shots with mixed lighting + +### foreign/ +Non-English cards (8 languages): +- Japanese (日本語) +- German (Deutsch) +- French (Français) +- Italian (Italiano) +- Spanish (Español) +- Russian (Русский) +- Simplified Chinese (简体中文) +- Korean (한국어) + +### tokens/ +Tokens and planeswalker emblems: +- Official WotC tokens +- Custom/altered tokens +- Planeswalker emblems (Elspeth, Gideon, Narset) +- Token collections and gameplay shots + +### varying_quality/ +Images with various real-world challenges: +- Different camera exposures +- BGS graded cases (`counterspell_bgs.jpg`) +- Cards in plastic sleeves (`card_in_plastic_case.jpg`) +- Various lighting conditions +- 28 numbered test images (`test1.jpg` - `test27.jpg`) + +### reference_alpha/ +47 Limited Edition Alpha cards for old-frame recognition: +- Power Nine (Black Lotus, Ancestral Recall, Moxen, etc.) +- Dual lands (Underground Sea, Volcanic Island, etc.) +- Classic staples (Lightning Bolt, Counterspell, Sol Ring) + +### hands/ +Cards held in hand - partial visibility, stacked: +- Various deck archetypes (Tron, Green, Red) +- New and old frame cards +- Different lighting conditions + +### real_photos/ +Phone camera photos from Visions project: +- Real-world scanning conditions +- Various resolutions and crops +- Includes processed result images + +### ocr_test/ +From CardScanner project, graded by difficulty: +- `card0-4.jpg`: Easier recognition +- `card10-13.jpg`: Harder recognition (noted ~less accuracy) + +## TODO: Additional Categories Needed + +- [ ] **double_faced/** - Transform/MDFC cards (both sides) +- [ ] **art_cards/** - Art series cards without text boxes +- [ ] **promos/** - Extended art, borderless, showcase frames +- [ ] **very_low_light/** - Near-dark conditions +- [ ] **motion_blur/** - Cards in motion during scanning + +## License + +Card artwork is property of Wizards of the Coast. Images used for testing/research purposes only. diff --git a/TestImages/angled/tilted_card_1.jpg b/TestImages/angled/tilted_card_1.jpg new file mode 100644 index 0000000..e973651 Binary files /dev/null and b/TestImages/angled/tilted_card_1.jpg differ diff --git a/TestImages/angled/tilted_card_2.jpg b/TestImages/angled/tilted_card_2.jpg new file mode 100644 index 0000000..d1edf41 Binary files /dev/null and b/TestImages/angled/tilted_card_2.jpg differ diff --git a/TestImages/augmented/augmented_1.jpg b/TestImages/augmented/augmented_1.jpg new file mode 100644 index 0000000..1f75ac5 Binary files /dev/null and b/TestImages/augmented/augmented_1.jpg differ diff --git a/TestImages/augmented/augmented_2.jpg b/TestImages/augmented/augmented_2.jpg new file mode 100644 index 0000000..5eb1c4e Binary files /dev/null and b/TestImages/augmented/augmented_2.jpg differ diff --git a/TestImages/augmented/augmented_3.jpg b/TestImages/augmented/augmented_3.jpg new file mode 100644 index 0000000..9d41972 Binary files /dev/null and b/TestImages/augmented/augmented_3.jpg differ diff --git a/TestImages/augmented/augmented_4.jpg b/TestImages/augmented/augmented_4.jpg new file mode 100644 index 0000000..d4897ae Binary files /dev/null and b/TestImages/augmented/augmented_4.jpg differ diff --git a/TestImages/foil/dragonscale_foil.jpg b/TestImages/foil/dragonscale_foil.jpg new file mode 100644 index 0000000..ce1bbfc Binary files /dev/null and b/TestImages/foil/dragonscale_foil.jpg differ diff --git a/TestImages/foil/foil_curling_1.jpg b/TestImages/foil/foil_curling_1.jpg new file mode 100644 index 0000000..6d20039 Binary files /dev/null and b/TestImages/foil/foil_curling_1.jpg differ diff --git a/TestImages/foil/foil_curling_2.jpg b/TestImages/foil/foil_curling_2.jpg new file mode 100644 index 0000000..a37dfcd Binary files /dev/null and b/TestImages/foil/foil_curling_2.jpg differ diff --git a/TestImages/foil/foil_jpn_mystical_archives.jpg b/TestImages/foil/foil_jpn_mystical_archives.jpg new file mode 100644 index 0000000..436d844 Binary files /dev/null and b/TestImages/foil/foil_jpn_mystical_archives.jpg differ diff --git a/TestImages/foil/foil_peel_holo_layer.jpg b/TestImages/foil/foil_peel_holo_layer.jpg new file mode 100644 index 0000000..f19ee43 Binary files /dev/null and b/TestImages/foil/foil_peel_holo_layer.jpg differ diff --git a/TestImages/foil/foil_quality_comparison.jpeg b/TestImages/foil/foil_quality_comparison.jpeg new file mode 100644 index 0000000..7899964 Binary files /dev/null and b/TestImages/foil/foil_quality_comparison.jpeg differ diff --git a/TestImages/foil/foil_swamp_collection.jpg b/TestImages/foil/foil_swamp_collection.jpg new file mode 100644 index 0000000..3111393 Binary files /dev/null and b/TestImages/foil/foil_swamp_collection.jpg differ diff --git a/TestImages/foil/modern_vs_og_foils.jpg b/TestImages/foil/modern_vs_og_foils.jpg new file mode 100644 index 0000000..7a04366 Binary files /dev/null and b/TestImages/foil/modern_vs_og_foils.jpg differ diff --git a/TestImages/foil/old_foil_yawgmoth.jpg b/TestImages/foil/old_foil_yawgmoth.jpg new file mode 100644 index 0000000..5c72d87 Binary files /dev/null and b/TestImages/foil/old_foil_yawgmoth.jpg differ diff --git a/TestImages/foil/rainbow_foil_secret_lair.jpg b/TestImages/foil/rainbow_foil_secret_lair.jpg new file mode 100644 index 0000000..11aa32a Binary files /dev/null and b/TestImages/foil/rainbow_foil_secret_lair.jpg differ diff --git a/TestImages/foil/rainbow_foil_sheldons.jpg b/TestImages/foil/rainbow_foil_sheldons.jpg new file mode 100644 index 0000000..e4e3072 Binary files /dev/null and b/TestImages/foil/rainbow_foil_sheldons.jpg differ diff --git a/TestImages/foil/surge_foil_rhino.jpeg b/TestImages/foil/surge_foil_rhino.jpeg new file mode 100644 index 0000000..c9c48ea Binary files /dev/null and b/TestImages/foil/surge_foil_rhino.jpeg differ diff --git a/TestImages/foil/surge_foils_holo.jpeg b/TestImages/foil/surge_foils_holo.jpeg new file mode 100644 index 0000000..fd3e806 Binary files /dev/null and b/TestImages/foil/surge_foils_holo.jpeg differ diff --git a/TestImages/foil/textured_foils.jpg b/TestImages/foil/textured_foils.jpg new file mode 100644 index 0000000..5e204dd Binary files /dev/null and b/TestImages/foil/textured_foils.jpg differ diff --git a/TestImages/foreign/chinese_aarakocra.jpg b/TestImages/foreign/chinese_aarakocra.jpg new file mode 100644 index 0000000..6d092a3 Binary files /dev/null and b/TestImages/foreign/chinese_aarakocra.jpg differ diff --git a/TestImages/foreign/chinese_abattoir_ghoul.jpg b/TestImages/foreign/chinese_abattoir_ghoul.jpg new file mode 100644 index 0000000..1abb3e0 Binary files /dev/null and b/TestImages/foreign/chinese_abattoir_ghoul.jpg differ diff --git a/TestImages/foreign/french_aang.jpg b/TestImages/foreign/french_aang.jpg new file mode 100644 index 0000000..e4955a1 Binary files /dev/null and b/TestImages/foreign/french_aang.jpg differ diff --git a/TestImages/foreign/french_abattoir_ghoul.jpg b/TestImages/foreign/french_abattoir_ghoul.jpg new file mode 100644 index 0000000..16090fc Binary files /dev/null and b/TestImages/foreign/french_abattoir_ghoul.jpg differ diff --git a/TestImages/foreign/german_aang.jpg b/TestImages/foreign/german_aang.jpg new file mode 100644 index 0000000..d0615f9 Binary files /dev/null and b/TestImages/foreign/german_aang.jpg differ diff --git a/TestImages/foreign/german_abattoir_ghoul.jpg b/TestImages/foreign/german_abattoir_ghoul.jpg new file mode 100644 index 0000000..382f59c Binary files /dev/null and b/TestImages/foreign/german_abattoir_ghoul.jpg differ diff --git a/TestImages/foreign/italian_aang.jpg b/TestImages/foreign/italian_aang.jpg new file mode 100644 index 0000000..c18f85a Binary files /dev/null and b/TestImages/foreign/italian_aang.jpg differ diff --git a/TestImages/foreign/japanese_aang.jpg b/TestImages/foreign/japanese_aang.jpg new file mode 100644 index 0000000..2a3fec1 Binary files /dev/null and b/TestImages/foreign/japanese_aang.jpg differ diff --git a/TestImages/foreign/japanese_abduction.jpg b/TestImages/foreign/japanese_abduction.jpg new file mode 100644 index 0000000..0f7dc2d Binary files /dev/null and b/TestImages/foreign/japanese_abduction.jpg differ diff --git a/TestImages/foreign/japanese_aberrant_researcher.jpg b/TestImages/foreign/japanese_aberrant_researcher.jpg new file mode 100644 index 0000000..9906fd1 Binary files /dev/null and b/TestImages/foreign/japanese_aberrant_researcher.jpg differ diff --git a/TestImages/foreign/japanese_abhorrent_overlord.jpg b/TestImages/foreign/japanese_abhorrent_overlord.jpg new file mode 100644 index 0000000..f81b500 Binary files /dev/null and b/TestImages/foreign/japanese_abhorrent_overlord.jpg differ diff --git a/TestImages/foreign/korean_aarakocra.jpg b/TestImages/foreign/korean_aarakocra.jpg new file mode 100644 index 0000000..0239a26 Binary files /dev/null and b/TestImages/foreign/korean_aarakocra.jpg differ diff --git a/TestImages/foreign/korean_abattoir_ghoul.jpg b/TestImages/foreign/korean_abattoir_ghoul.jpg new file mode 100644 index 0000000..d764f62 Binary files /dev/null and b/TestImages/foreign/korean_abattoir_ghoul.jpg differ diff --git a/TestImages/foreign/russian_aarakocra.jpg b/TestImages/foreign/russian_aarakocra.jpg new file mode 100644 index 0000000..6470a93 Binary files /dev/null and b/TestImages/foreign/russian_aarakocra.jpg differ diff --git a/TestImages/foreign/russian_abattoir_ghoul.jpg b/TestImages/foreign/russian_abattoir_ghoul.jpg new file mode 100644 index 0000000..b43afff Binary files /dev/null and b/TestImages/foreign/russian_abattoir_ghoul.jpg differ diff --git a/TestImages/foreign/spanish_aang.jpg b/TestImages/foreign/spanish_aang.jpg new file mode 100644 index 0000000..b222c29 Binary files /dev/null and b/TestImages/foreign/spanish_aang.jpg differ diff --git a/TestImages/hands/handOfCards.jpg b/TestImages/hands/handOfCards.jpg new file mode 100644 index 0000000..8f8f53e Binary files /dev/null and b/TestImages/hands/handOfCards.jpg differ diff --git a/TestImages/hands/hand_of_card_1.png b/TestImages/hands/hand_of_card_1.png new file mode 100644 index 0000000..8323d5c Binary files /dev/null and b/TestImages/hands/hand_of_card_1.png differ diff --git a/TestImages/hands/hand_of_card_green_1.jpg b/TestImages/hands/hand_of_card_green_1.jpg new file mode 100644 index 0000000..13f5b75 Binary files /dev/null and b/TestImages/hands/hand_of_card_green_1.jpg differ diff --git a/TestImages/hands/hand_of_card_green_2.jpeg b/TestImages/hands/hand_of_card_green_2.jpeg new file mode 100644 index 0000000..86109fa Binary files /dev/null and b/TestImages/hands/hand_of_card_green_2.jpeg differ diff --git a/TestImages/hands/hand_of_card_ktk.png b/TestImages/hands/hand_of_card_ktk.png new file mode 100644 index 0000000..456ab69 Binary files /dev/null and b/TestImages/hands/hand_of_card_ktk.png differ diff --git a/TestImages/hands/hand_of_card_new_frame.webp b/TestImages/hands/hand_of_card_new_frame.webp new file mode 100644 index 0000000..1eb5b04 Binary files /dev/null and b/TestImages/hands/hand_of_card_new_frame.webp differ diff --git a/TestImages/hands/hand_of_card_one_hand.jpg b/TestImages/hands/hand_of_card_one_hand.jpg new file mode 100644 index 0000000..bae5d8d Binary files /dev/null and b/TestImages/hands/hand_of_card_one_hand.jpg differ diff --git a/TestImages/hands/hand_of_card_red.jpeg b/TestImages/hands/hand_of_card_red.jpeg new file mode 100644 index 0000000..4469e9f Binary files /dev/null and b/TestImages/hands/hand_of_card_red.jpeg differ diff --git a/TestImages/hands/hand_of_card_tron.png b/TestImages/hands/hand_of_card_tron.png new file mode 100644 index 0000000..b2f569c Binary files /dev/null and b/TestImages/hands/hand_of_card_tron.png differ diff --git a/TestImages/hands/klomparens_hand.png b/TestImages/hands/klomparens_hand.png new file mode 100644 index 0000000..09cc0b3 Binary files /dev/null and b/TestImages/hands/klomparens_hand.png differ diff --git a/TestImages/hands/li38_handOfCards.jpg b/TestImages/hands/li38_handOfCards.jpg new file mode 100644 index 0000000..e7e91be Binary files /dev/null and b/TestImages/hands/li38_handOfCards.jpg differ diff --git a/TestImages/low_light/authenticity_check.jpg b/TestImages/low_light/authenticity_check.jpg new file mode 100644 index 0000000..7618852 Binary files /dev/null and b/TestImages/low_light/authenticity_check.jpg differ diff --git a/TestImages/low_light/basic_lands_amateur.jpg b/TestImages/low_light/basic_lands_amateur.jpg new file mode 100644 index 0000000..f95979b Binary files /dev/null and b/TestImages/low_light/basic_lands_amateur.jpg differ diff --git a/TestImages/low_light/condition_amateur_1.jpg b/TestImages/low_light/condition_amateur_1.jpg new file mode 100644 index 0000000..46b0d27 Binary files /dev/null and b/TestImages/low_light/condition_amateur_1.jpg differ diff --git a/TestImages/low_light/condition_amateur_2.jpg b/TestImages/low_light/condition_amateur_2.jpg new file mode 100644 index 0000000..49d0e2b Binary files /dev/null and b/TestImages/low_light/condition_amateur_2.jpg differ diff --git a/TestImages/low_light/diy_lighting_rig.jpg b/TestImages/low_light/diy_lighting_rig.jpg new file mode 100644 index 0000000..e49fb06 Binary files /dev/null and b/TestImages/low_light/diy_lighting_rig.jpg differ diff --git a/TestImages/low_light/dof_blur_amateur.jpg b/TestImages/low_light/dof_blur_amateur.jpg new file mode 100644 index 0000000..9e3a974 Binary files /dev/null and b/TestImages/low_light/dof_blur_amateur.jpg differ diff --git a/TestImages/low_light/fake_detection.jpg b/TestImages/low_light/fake_detection.jpg new file mode 100644 index 0000000..54f1bdd Binary files /dev/null and b/TestImages/low_light/fake_detection.jpg differ diff --git a/TestImages/low_light/flickr_collection_1.jpg b/TestImages/low_light/flickr_collection_1.jpg new file mode 100644 index 0000000..057b426 Binary files /dev/null and b/TestImages/low_light/flickr_collection_1.jpg differ diff --git a/TestImages/low_light/flickr_collection_2.jpg b/TestImages/low_light/flickr_collection_2.jpg new file mode 100644 index 0000000..6764c6e Binary files /dev/null and b/TestImages/low_light/flickr_collection_2.jpg differ diff --git a/TestImages/low_light/flickr_collection_3.jpg b/TestImages/low_light/flickr_collection_3.jpg new file mode 100644 index 0000000..f7e6483 Binary files /dev/null and b/TestImages/low_light/flickr_collection_3.jpg differ diff --git a/TestImages/low_light/glare_straight_down.jpg b/TestImages/low_light/glare_straight_down.jpg new file mode 100644 index 0000000..fdf5838 Binary files /dev/null and b/TestImages/low_light/glare_straight_down.jpg differ diff --git a/TestImages/low_light/glare_toploader.png b/TestImages/low_light/glare_toploader.png new file mode 100644 index 0000000..5a3f6b2 Binary files /dev/null and b/TestImages/low_light/glare_toploader.png differ diff --git a/TestImages/low_light/grading_amateur.jpg b/TestImages/low_light/grading_amateur.jpg new file mode 100644 index 0000000..8a7a040 Binary files /dev/null and b/TestImages/low_light/grading_amateur.jpg differ diff --git a/TestImages/low_light/macro_monday_shadows.jpg b/TestImages/low_light/macro_monday_shadows.jpg new file mode 100644 index 0000000..bf47519 Binary files /dev/null and b/TestImages/low_light/macro_monday_shadows.jpg differ diff --git a/TestImages/multiple_cards/alpha_deck.jpg b/TestImages/multiple_cards/alpha_deck.jpg new file mode 100644 index 0000000..281ff42 Binary files /dev/null and b/TestImages/multiple_cards/alpha_deck.jpg differ diff --git a/TestImages/multiple_cards/geyser_twister_fireball.jpg b/TestImages/multiple_cards/geyser_twister_fireball.jpg new file mode 100644 index 0000000..47263a3 Binary files /dev/null and b/TestImages/multiple_cards/geyser_twister_fireball.jpg differ diff --git a/TestImages/multiple_cards/lands_and_fatties.jpg b/TestImages/multiple_cards/lands_and_fatties.jpg new file mode 100644 index 0000000..344b26b Binary files /dev/null and b/TestImages/multiple_cards/lands_and_fatties.jpg differ diff --git a/TestImages/multiple_cards/magic1.png b/TestImages/multiple_cards/magic1.png new file mode 100644 index 0000000..a6480fb Binary files /dev/null and b/TestImages/multiple_cards/magic1.png differ diff --git a/TestImages/multiple_cards/pro_tour_side.png b/TestImages/multiple_cards/pro_tour_side.png new file mode 100644 index 0000000..759ddf3 Binary files /dev/null and b/TestImages/multiple_cards/pro_tour_side.png differ diff --git a/TestImages/multiple_cards/pro_tour_table.png b/TestImages/multiple_cards/pro_tour_table.png new file mode 100644 index 0000000..e02960b Binary files /dev/null and b/TestImages/multiple_cards/pro_tour_table.png differ diff --git a/TestImages/ocr_test/card.jpg b/TestImages/ocr_test/card.jpg new file mode 100644 index 0000000..ff57b28 Binary files /dev/null and b/TestImages/ocr_test/card.jpg differ diff --git a/TestImages/ocr_test/card0.jpg b/TestImages/ocr_test/card0.jpg new file mode 100644 index 0000000..5a5f5d8 Binary files /dev/null and b/TestImages/ocr_test/card0.jpg differ diff --git a/TestImages/ocr_test/card1.jpg b/TestImages/ocr_test/card1.jpg new file mode 100644 index 0000000..151d89f Binary files /dev/null and b/TestImages/ocr_test/card1.jpg differ diff --git a/TestImages/ocr_test/card10.jpg b/TestImages/ocr_test/card10.jpg new file mode 100644 index 0000000..1d25cf2 Binary files /dev/null and b/TestImages/ocr_test/card10.jpg differ diff --git a/TestImages/ocr_test/card11.jpg b/TestImages/ocr_test/card11.jpg new file mode 100644 index 0000000..339fc0c Binary files /dev/null and b/TestImages/ocr_test/card11.jpg differ diff --git a/TestImages/ocr_test/card12.jpg b/TestImages/ocr_test/card12.jpg new file mode 100644 index 0000000..4de7f50 Binary files /dev/null and b/TestImages/ocr_test/card12.jpg differ diff --git a/TestImages/ocr_test/card13.jpg b/TestImages/ocr_test/card13.jpg new file mode 100644 index 0000000..3b96f8d Binary files /dev/null and b/TestImages/ocr_test/card13.jpg differ diff --git a/TestImages/ocr_test/card2.jpg b/TestImages/ocr_test/card2.jpg new file mode 100644 index 0000000..b974812 Binary files /dev/null and b/TestImages/ocr_test/card2.jpg differ diff --git a/TestImages/ocr_test/card3.jpg b/TestImages/ocr_test/card3.jpg new file mode 100644 index 0000000..56347eb Binary files /dev/null and b/TestImages/ocr_test/card3.jpg differ diff --git a/TestImages/ocr_test/card4.jpg b/TestImages/ocr_test/card4.jpg new file mode 100644 index 0000000..4e73d9c Binary files /dev/null and b/TestImages/ocr_test/card4.jpg differ diff --git a/TestImages/real_photos/visions_1.jpg b/TestImages/real_photos/visions_1.jpg new file mode 100644 index 0000000..9408b6d Binary files /dev/null and b/TestImages/real_photos/visions_1.jpg differ diff --git a/TestImages/real_photos/visions_1_square.jpg b/TestImages/real_photos/visions_1_square.jpg new file mode 100644 index 0000000..a15da3e Binary files /dev/null and b/TestImages/real_photos/visions_1_square.jpg differ diff --git a/TestImages/real_photos/visions_2.jpg b/TestImages/real_photos/visions_2.jpg new file mode 100644 index 0000000..04878b2 Binary files /dev/null and b/TestImages/real_photos/visions_2.jpg differ diff --git a/TestImages/real_photos/visions_2_square.jpg b/TestImages/real_photos/visions_2_square.jpg new file mode 100644 index 0000000..389a603 Binary files /dev/null and b/TestImages/real_photos/visions_2_square.jpg differ diff --git a/TestImages/real_photos/visions_3.jpg b/TestImages/real_photos/visions_3.jpg new file mode 100644 index 0000000..5fcc36b Binary files /dev/null and b/TestImages/real_photos/visions_3.jpg differ diff --git a/TestImages/real_photos/visions_4.jpg b/TestImages/real_photos/visions_4.jpg new file mode 100644 index 0000000..2664cca Binary files /dev/null and b/TestImages/real_photos/visions_4.jpg differ diff --git a/TestImages/real_photos/visions_5.jpg b/TestImages/real_photos/visions_5.jpg new file mode 100644 index 0000000..67ef2f0 Binary files /dev/null and b/TestImages/real_photos/visions_5.jpg differ diff --git a/TestImages/real_photos/visions_6.jpg b/TestImages/real_photos/visions_6.jpg new file mode 100644 index 0000000..39b27fd Binary files /dev/null and b/TestImages/real_photos/visions_6.jpg differ diff --git a/TestImages/real_photos/visions_6_square.jpg b/TestImages/real_photos/visions_6_square.jpg new file mode 100644 index 0000000..15bd9bc Binary files /dev/null and b/TestImages/real_photos/visions_6_square.jpg differ diff --git a/TestImages/real_photos/visions_7.jpg b/TestImages/real_photos/visions_7.jpg new file mode 100644 index 0000000..4a5525b Binary files /dev/null and b/TestImages/real_photos/visions_7.jpg differ diff --git a/TestImages/real_photos/visions_8.jpg b/TestImages/real_photos/visions_8.jpg new file mode 100644 index 0000000..5205411 Binary files /dev/null and b/TestImages/real_photos/visions_8.jpg differ diff --git a/TestImages/real_photos/visions_8_big.jpg b/TestImages/real_photos/visions_8_big.jpg new file mode 100644 index 0000000..aacdb0a Binary files /dev/null and b/TestImages/real_photos/visions_8_big.jpg differ diff --git a/TestImages/real_photos/visions_9.jpg b/TestImages/real_photos/visions_9.jpg new file mode 100644 index 0000000..04cb000 Binary files /dev/null and b/TestImages/real_photos/visions_9.jpg differ diff --git a/TestImages/real_photos/visions_9_small.jpg b/TestImages/real_photos/visions_9_small.jpg new file mode 100644 index 0000000..230f5b2 Binary files /dev/null and b/TestImages/real_photos/visions_9_small.jpg differ diff --git a/TestImages/real_photos/visions_result_1.jpg b/TestImages/real_photos/visions_result_1.jpg new file mode 100644 index 0000000..a669ee2 Binary files /dev/null and b/TestImages/real_photos/visions_result_1.jpg differ diff --git a/TestImages/real_photos/visions_result_2.jpg b/TestImages/real_photos/visions_result_2.jpg new file mode 100644 index 0000000..abd29ed Binary files /dev/null and b/TestImages/real_photos/visions_result_2.jpg differ diff --git a/TestImages/real_photos/visions_result_3.jpg b/TestImages/real_photos/visions_result_3.jpg new file mode 100644 index 0000000..988e068 Binary files /dev/null and b/TestImages/real_photos/visions_result_3.jpg differ diff --git a/TestImages/real_photos/visions_result_4.jpg b/TestImages/real_photos/visions_result_4.jpg new file mode 100644 index 0000000..a28fd0a Binary files /dev/null and b/TestImages/real_photos/visions_result_4.jpg differ diff --git a/TestImages/reference/brainstorm.png b/TestImages/reference/brainstorm.png new file mode 100644 index 0000000..bf7f8f5 Binary files /dev/null and b/TestImages/reference/brainstorm.png differ diff --git a/TestImages/reference/force_of_will.png b/TestImages/reference/force_of_will.png new file mode 100644 index 0000000..6ec00e5 Binary files /dev/null and b/TestImages/reference/force_of_will.png differ diff --git a/TestImages/reference/griselbrand.png b/TestImages/reference/griselbrand.png new file mode 100644 index 0000000..e73c642 Binary files /dev/null and b/TestImages/reference/griselbrand.png differ diff --git a/TestImages/reference/lotus_petal.png b/TestImages/reference/lotus_petal.png new file mode 100644 index 0000000..d048c9f Binary files /dev/null and b/TestImages/reference/lotus_petal.png differ diff --git a/TestImages/reference/ponder.png b/TestImages/reference/ponder.png new file mode 100644 index 0000000..48ae59d Binary files /dev/null and b/TestImages/reference/ponder.png differ diff --git a/TestImages/reference/show_and_tell.png b/TestImages/reference/show_and_tell.png new file mode 100644 index 0000000..9dee849 Binary files /dev/null and b/TestImages/reference/show_and_tell.png differ diff --git a/TestImages/reference/tropical_island.png b/TestImages/reference/tropical_island.png new file mode 100644 index 0000000..5ddb71f Binary files /dev/null and b/TestImages/reference/tropical_island.png differ diff --git a/TestImages/reference/volcanic_island.png b/TestImages/reference/volcanic_island.png new file mode 100644 index 0000000..d14eb98 Binary files /dev/null and b/TestImages/reference/volcanic_island.png differ diff --git a/TestImages/reference/wasteland.png b/TestImages/reference/wasteland.png new file mode 100644 index 0000000..54b12ab Binary files /dev/null and b/TestImages/reference/wasteland.png differ diff --git a/TestImages/reference_alpha/ancestral_recall.jpg b/TestImages/reference_alpha/ancestral_recall.jpg new file mode 100644 index 0000000..273d451 Binary files /dev/null and b/TestImages/reference_alpha/ancestral_recall.jpg differ diff --git a/TestImages/reference_alpha/badlands.jpg b/TestImages/reference_alpha/badlands.jpg new file mode 100644 index 0000000..34b8f20 Binary files /dev/null and b/TestImages/reference_alpha/badlands.jpg differ diff --git a/TestImages/reference_alpha/balance.jpg b/TestImages/reference_alpha/balance.jpg new file mode 100644 index 0000000..004e76b Binary files /dev/null and b/TestImages/reference_alpha/balance.jpg differ diff --git a/TestImages/reference_alpha/bayou.jpg b/TestImages/reference_alpha/bayou.jpg new file mode 100644 index 0000000..77ed6ab Binary files /dev/null and b/TestImages/reference_alpha/bayou.jpg differ diff --git a/TestImages/reference_alpha/birds_of_paradise.jpg b/TestImages/reference_alpha/birds_of_paradise.jpg new file mode 100644 index 0000000..83407e2 Binary files /dev/null and b/TestImages/reference_alpha/birds_of_paradise.jpg differ diff --git a/TestImages/reference_alpha/black_lotus.jpg b/TestImages/reference_alpha/black_lotus.jpg new file mode 100644 index 0000000..b529a2b Binary files /dev/null and b/TestImages/reference_alpha/black_lotus.jpg differ diff --git a/TestImages/reference_alpha/channel.jpg b/TestImages/reference_alpha/channel.jpg new file mode 100644 index 0000000..ea61345 Binary files /dev/null and b/TestImages/reference_alpha/channel.jpg differ diff --git a/TestImages/reference_alpha/chaos_orb.jpg b/TestImages/reference_alpha/chaos_orb.jpg new file mode 100644 index 0000000..d67b23a Binary files /dev/null and b/TestImages/reference_alpha/chaos_orb.jpg differ diff --git a/TestImages/reference_alpha/clone.jpg b/TestImages/reference_alpha/clone.jpg new file mode 100644 index 0000000..937461a Binary files /dev/null and b/TestImages/reference_alpha/clone.jpg differ diff --git a/TestImages/reference_alpha/control_magic.jpg b/TestImages/reference_alpha/control_magic.jpg new file mode 100644 index 0000000..51f94d9 Binary files /dev/null and b/TestImages/reference_alpha/control_magic.jpg differ diff --git a/TestImages/reference_alpha/counterspell.jpg b/TestImages/reference_alpha/counterspell.jpg new file mode 100644 index 0000000..44a134c Binary files /dev/null and b/TestImages/reference_alpha/counterspell.jpg differ diff --git a/TestImages/reference_alpha/dark_ritual.jpg b/TestImages/reference_alpha/dark_ritual.jpg new file mode 100644 index 0000000..92829be Binary files /dev/null and b/TestImages/reference_alpha/dark_ritual.jpg differ diff --git a/TestImages/reference_alpha/demonic_tutor.jpg b/TestImages/reference_alpha/demonic_tutor.jpg new file mode 100644 index 0000000..bf0375d Binary files /dev/null and b/TestImages/reference_alpha/demonic_tutor.jpg differ diff --git a/TestImages/reference_alpha/disenchant.jpg b/TestImages/reference_alpha/disenchant.jpg new file mode 100644 index 0000000..a159c61 Binary files /dev/null and b/TestImages/reference_alpha/disenchant.jpg differ diff --git a/TestImages/reference_alpha/fireball.jpg b/TestImages/reference_alpha/fireball.jpg new file mode 100644 index 0000000..a683353 Binary files /dev/null and b/TestImages/reference_alpha/fireball.jpg differ diff --git a/TestImages/reference_alpha/force_of_nature.jpg b/TestImages/reference_alpha/force_of_nature.jpg new file mode 100644 index 0000000..497c7c5 Binary files /dev/null and b/TestImages/reference_alpha/force_of_nature.jpg differ diff --git a/TestImages/reference_alpha/fork.jpg b/TestImages/reference_alpha/fork.jpg new file mode 100644 index 0000000..40ac20d Binary files /dev/null and b/TestImages/reference_alpha/fork.jpg differ diff --git a/TestImages/reference_alpha/giant_growth.jpg b/TestImages/reference_alpha/giant_growth.jpg new file mode 100644 index 0000000..45bc473 Binary files /dev/null and b/TestImages/reference_alpha/giant_growth.jpg differ diff --git a/TestImages/reference_alpha/hypnotic_specter.jpg b/TestImages/reference_alpha/hypnotic_specter.jpg new file mode 100644 index 0000000..11ebb95 Binary files /dev/null and b/TestImages/reference_alpha/hypnotic_specter.jpg differ diff --git a/TestImages/reference_alpha/lightning_bolt.jpg b/TestImages/reference_alpha/lightning_bolt.jpg new file mode 100644 index 0000000..710b69a Binary files /dev/null and b/TestImages/reference_alpha/lightning_bolt.jpg differ diff --git a/TestImages/reference_alpha/llanowar_elves.jpg b/TestImages/reference_alpha/llanowar_elves.jpg new file mode 100644 index 0000000..bdfbfc1 Binary files /dev/null and b/TestImages/reference_alpha/llanowar_elves.jpg differ diff --git a/TestImages/reference_alpha/mahamoti_djinn.jpg b/TestImages/reference_alpha/mahamoti_djinn.jpg new file mode 100644 index 0000000..5265950 Binary files /dev/null and b/TestImages/reference_alpha/mahamoti_djinn.jpg differ diff --git a/TestImages/reference_alpha/mind_twist.jpg b/TestImages/reference_alpha/mind_twist.jpg new file mode 100644 index 0000000..6ee690b Binary files /dev/null and b/TestImages/reference_alpha/mind_twist.jpg differ diff --git a/TestImages/reference_alpha/mox_emerald.jpg b/TestImages/reference_alpha/mox_emerald.jpg new file mode 100644 index 0000000..25c0e11 Binary files /dev/null and b/TestImages/reference_alpha/mox_emerald.jpg differ diff --git a/TestImages/reference_alpha/mox_jet.jpg b/TestImages/reference_alpha/mox_jet.jpg new file mode 100644 index 0000000..a3e18bf Binary files /dev/null and b/TestImages/reference_alpha/mox_jet.jpg differ diff --git a/TestImages/reference_alpha/mox_pearl.jpg b/TestImages/reference_alpha/mox_pearl.jpg new file mode 100644 index 0000000..97d12ee Binary files /dev/null and b/TestImages/reference_alpha/mox_pearl.jpg differ diff --git a/TestImages/reference_alpha/mox_ruby.jpg b/TestImages/reference_alpha/mox_ruby.jpg new file mode 100644 index 0000000..c2d1d3b Binary files /dev/null and b/TestImages/reference_alpha/mox_ruby.jpg differ diff --git a/TestImages/reference_alpha/mox_sapphire.jpg b/TestImages/reference_alpha/mox_sapphire.jpg new file mode 100644 index 0000000..ed7e87e Binary files /dev/null and b/TestImages/reference_alpha/mox_sapphire.jpg differ diff --git a/TestImages/reference_alpha/nightmare.jpg b/TestImages/reference_alpha/nightmare.jpg new file mode 100644 index 0000000..d1a0a15 Binary files /dev/null and b/TestImages/reference_alpha/nightmare.jpg differ diff --git a/TestImages/reference_alpha/plateau.jpg b/TestImages/reference_alpha/plateau.jpg new file mode 100644 index 0000000..0d5ccd5 Binary files /dev/null and b/TestImages/reference_alpha/plateau.jpg differ diff --git a/TestImages/reference_alpha/regrowth.jpg b/TestImages/reference_alpha/regrowth.jpg new file mode 100644 index 0000000..97fd879 Binary files /dev/null and b/TestImages/reference_alpha/regrowth.jpg differ diff --git a/TestImages/reference_alpha/rock_hydra.jpg b/TestImages/reference_alpha/rock_hydra.jpg new file mode 100644 index 0000000..b88b8c5 Binary files /dev/null and b/TestImages/reference_alpha/rock_hydra.jpg differ diff --git a/TestImages/reference_alpha/royal_assassin.jpg b/TestImages/reference_alpha/royal_assassin.jpg new file mode 100644 index 0000000..fa23a71 Binary files /dev/null and b/TestImages/reference_alpha/royal_assassin.jpg differ diff --git a/TestImages/reference_alpha/savannah.jpg b/TestImages/reference_alpha/savannah.jpg new file mode 100644 index 0000000..2ef8dd9 Binary files /dev/null and b/TestImages/reference_alpha/savannah.jpg differ diff --git a/TestImages/reference_alpha/scrubland.jpg b/TestImages/reference_alpha/scrubland.jpg new file mode 100644 index 0000000..bfaf8b8 Binary files /dev/null and b/TestImages/reference_alpha/scrubland.jpg differ diff --git a/TestImages/reference_alpha/serra_angel.jpg b/TestImages/reference_alpha/serra_angel.jpg new file mode 100644 index 0000000..7bc59cf Binary files /dev/null and b/TestImages/reference_alpha/serra_angel.jpg differ diff --git a/TestImages/reference_alpha/shivan_dragon.jpg b/TestImages/reference_alpha/shivan_dragon.jpg new file mode 100644 index 0000000..3126461 Binary files /dev/null and b/TestImages/reference_alpha/shivan_dragon.jpg differ diff --git a/TestImages/reference_alpha/sol_ring.jpg b/TestImages/reference_alpha/sol_ring.jpg new file mode 100644 index 0000000..a754249 Binary files /dev/null and b/TestImages/reference_alpha/sol_ring.jpg differ diff --git a/TestImages/reference_alpha/swords_to_plowshares.jpg b/TestImages/reference_alpha/swords_to_plowshares.jpg new file mode 100644 index 0000000..964667e Binary files /dev/null and b/TestImages/reference_alpha/swords_to_plowshares.jpg differ diff --git a/TestImages/reference_alpha/taiga.jpg b/TestImages/reference_alpha/taiga.jpg new file mode 100644 index 0000000..a9465b7 Binary files /dev/null and b/TestImages/reference_alpha/taiga.jpg differ diff --git a/TestImages/reference_alpha/time_walk.jpg b/TestImages/reference_alpha/time_walk.jpg new file mode 100644 index 0000000..0807e9a Binary files /dev/null and b/TestImages/reference_alpha/time_walk.jpg differ diff --git a/TestImages/reference_alpha/timetwister.jpg b/TestImages/reference_alpha/timetwister.jpg new file mode 100644 index 0000000..aa95c55 Binary files /dev/null and b/TestImages/reference_alpha/timetwister.jpg differ diff --git a/TestImages/reference_alpha/tropical_island.jpg b/TestImages/reference_alpha/tropical_island.jpg new file mode 100644 index 0000000..186a951 Binary files /dev/null and b/TestImages/reference_alpha/tropical_island.jpg differ diff --git a/TestImages/reference_alpha/tundra.jpg b/TestImages/reference_alpha/tundra.jpg new file mode 100644 index 0000000..d2769bc Binary files /dev/null and b/TestImages/reference_alpha/tundra.jpg differ diff --git a/TestImages/reference_alpha/underground_sea.jpg b/TestImages/reference_alpha/underground_sea.jpg new file mode 100644 index 0000000..6824628 Binary files /dev/null and b/TestImages/reference_alpha/underground_sea.jpg differ diff --git a/TestImages/reference_alpha/wheel_of_fortune.jpg b/TestImages/reference_alpha/wheel_of_fortune.jpg new file mode 100644 index 0000000..603136f Binary files /dev/null and b/TestImages/reference_alpha/wheel_of_fortune.jpg differ diff --git a/TestImages/reference_alpha/wrath_of_god.jpg b/TestImages/reference_alpha/wrath_of_god.jpg new file mode 100644 index 0000000..9339812 Binary files /dev/null and b/TestImages/reference_alpha/wrath_of_god.jpg differ diff --git a/TestImages/single_cards/adanto_vanguard.png b/TestImages/single_cards/adanto_vanguard.png new file mode 100644 index 0000000..a7d27c2 Binary files /dev/null and b/TestImages/single_cards/adanto_vanguard.png differ diff --git a/TestImages/single_cards/angel_of_sanctions.png b/TestImages/single_cards/angel_of_sanctions.png new file mode 100644 index 0000000..181ed0b Binary files /dev/null and b/TestImages/single_cards/angel_of_sanctions.png differ diff --git a/TestImages/single_cards/attunement.jpg b/TestImages/single_cards/attunement.jpg new file mode 100644 index 0000000..5994502 Binary files /dev/null and b/TestImages/single_cards/attunement.jpg differ diff --git a/TestImages/single_cards/avaricious_dragon.jpg b/TestImages/single_cards/avaricious_dragon.jpg new file mode 100644 index 0000000..396fa6c Binary files /dev/null and b/TestImages/single_cards/avaricious_dragon.jpg differ diff --git a/TestImages/single_cards/burgeoning.png b/TestImages/single_cards/burgeoning.png new file mode 100644 index 0000000..0a5baba Binary files /dev/null and b/TestImages/single_cards/burgeoning.png differ diff --git a/TestImages/single_cards/fireball.jpg b/TestImages/single_cards/fireball.jpg new file mode 100644 index 0000000..1a6a56f Binary files /dev/null and b/TestImages/single_cards/fireball.jpg differ diff --git a/TestImages/single_cards/jarad_golgari.jpg b/TestImages/single_cards/jarad_golgari.jpg new file mode 100644 index 0000000..ee26e77 Binary files /dev/null and b/TestImages/single_cards/jarad_golgari.jpg differ diff --git a/TestImages/single_cards/llanowar_elves.jpg b/TestImages/single_cards/llanowar_elves.jpg new file mode 100644 index 0000000..33adb4b Binary files /dev/null and b/TestImages/single_cards/llanowar_elves.jpg differ diff --git a/TestImages/single_cards/meletis_charlatan.jpg b/TestImages/single_cards/meletis_charlatan.jpg new file mode 100644 index 0000000..8c736f7 Binary files /dev/null and b/TestImages/single_cards/meletis_charlatan.jpg differ diff --git a/TestImages/single_cards/mindstab_thrull.jpeg b/TestImages/single_cards/mindstab_thrull.jpeg new file mode 100644 index 0000000..95b1c61 Binary files /dev/null and b/TestImages/single_cards/mindstab_thrull.jpeg differ diff --git a/TestImages/single_cards/pacifism.jpg b/TestImages/single_cards/pacifism.jpg new file mode 100644 index 0000000..7ed4f88 Binary files /dev/null and b/TestImages/single_cards/pacifism.jpg differ diff --git a/TestImages/single_cards/platinum_angel.jpg b/TestImages/single_cards/platinum_angel.jpg new file mode 100644 index 0000000..b971461 Binary files /dev/null and b/TestImages/single_cards/platinum_angel.jpg differ diff --git a/TestImages/single_cards/queen_marchesa.png b/TestImages/single_cards/queen_marchesa.png new file mode 100644 index 0000000..aa2b3f7 Binary files /dev/null and b/TestImages/single_cards/queen_marchesa.png differ diff --git a/TestImages/single_cards/queen_marchesa_analyzer.png b/TestImages/single_cards/queen_marchesa_analyzer.png new file mode 100644 index 0000000..aa2b3f7 Binary files /dev/null and b/TestImages/single_cards/queen_marchesa_analyzer.png differ diff --git a/TestImages/single_cards/shivan_dragon.jpg b/TestImages/single_cards/shivan_dragon.jpg new file mode 100644 index 0000000..50276a1 Binary files /dev/null and b/TestImages/single_cards/shivan_dragon.jpg differ diff --git a/TestImages/single_cards/spellseeker.png b/TestImages/single_cards/spellseeker.png new file mode 100644 index 0000000..0a3cb75 Binary files /dev/null and b/TestImages/single_cards/spellseeker.png differ diff --git a/TestImages/single_cards/tarmogoyf.jpg b/TestImages/single_cards/tarmogoyf.jpg new file mode 100644 index 0000000..e547a94 Binary files /dev/null and b/TestImages/single_cards/tarmogoyf.jpg differ diff --git a/TestImages/single_cards/thought_reflection.jpg b/TestImages/single_cards/thought_reflection.jpg new file mode 100644 index 0000000..e1c7ba5 Binary files /dev/null and b/TestImages/single_cards/thought_reflection.jpg differ diff --git a/TestImages/single_cards/unsummon.jpg b/TestImages/single_cards/unsummon.jpg new file mode 100644 index 0000000..a44be04 Binary files /dev/null and b/TestImages/single_cards/unsummon.jpg differ diff --git a/TestImages/tokens/angel_token_alter.jpg b/TestImages/tokens/angel_token_alter.jpg new file mode 100644 index 0000000..8a94cae Binary files /dev/null and b/TestImages/tokens/angel_token_alter.jpg differ diff --git a/TestImages/tokens/brothers_tokens.jpg b/TestImages/tokens/brothers_tokens.jpg new file mode 100644 index 0000000..f3363d3 Binary files /dev/null and b/TestImages/tokens/brothers_tokens.jpg differ diff --git a/TestImages/tokens/christopher_rush_tokens.jpg b/TestImages/tokens/christopher_rush_tokens.jpg new file mode 100644 index 0000000..bc93444 Binary files /dev/null and b/TestImages/tokens/christopher_rush_tokens.jpg differ diff --git a/TestImages/tokens/custom_tokens.jpg b/TestImages/tokens/custom_tokens.jpg new file mode 100644 index 0000000..89d4dda Binary files /dev/null and b/TestImages/tokens/custom_tokens.jpg differ diff --git a/TestImages/tokens/elspeth_emblem.jpg b/TestImages/tokens/elspeth_emblem.jpg new file mode 100644 index 0000000..78be8d9 Binary files /dev/null and b/TestImages/tokens/elspeth_emblem.jpg differ diff --git a/TestImages/tokens/elspeth_starwars_emblem.jpg b/TestImages/tokens/elspeth_starwars_emblem.jpg new file mode 100644 index 0000000..d37ba4d Binary files /dev/null and b/TestImages/tokens/elspeth_starwars_emblem.jpg differ diff --git a/TestImages/tokens/gideon_emblem.jpg b/TestImages/tokens/gideon_emblem.jpg new file mode 100644 index 0000000..a9292d3 Binary files /dev/null and b/TestImages/tokens/gideon_emblem.jpg differ diff --git a/TestImages/tokens/narset_emblem.jpg b/TestImages/tokens/narset_emblem.jpg new file mode 100644 index 0000000..5b2c0fc Binary files /dev/null and b/TestImages/tokens/narset_emblem.jpg differ diff --git a/TestImages/tokens/ratadrabik_token.jpg b/TestImages/tokens/ratadrabik_token.jpg new file mode 100644 index 0000000..9a10a4f Binary files /dev/null and b/TestImages/tokens/ratadrabik_token.jpg differ diff --git a/TestImages/tokens/rkpost_rhino_tokens.jpg b/TestImages/tokens/rkpost_rhino_tokens.jpg new file mode 100644 index 0000000..34ccd1b Binary files /dev/null and b/TestImages/tokens/rkpost_rhino_tokens.jpg differ diff --git a/TestImages/tokens/token_collection_pucatrade.jpg b/TestImages/tokens/token_collection_pucatrade.jpg new file mode 100644 index 0000000..4297869 Binary files /dev/null and b/TestImages/tokens/token_collection_pucatrade.jpg differ diff --git a/TestImages/tokens/tokens_foils_lands.jpg b/TestImages/tokens/tokens_foils_lands.jpg new file mode 100644 index 0000000..850bd60 Binary files /dev/null and b/TestImages/tokens/tokens_foils_lands.jpg differ diff --git a/TestImages/tokens/vampire_knight_token.jpg b/TestImages/tokens/vampire_knight_token.jpg new file mode 100644 index 0000000..5649e7e Binary files /dev/null and b/TestImages/tokens/vampire_knight_token.jpg differ diff --git a/TestImages/training_examples/training_set_1.jpg b/TestImages/training_examples/training_set_1.jpg new file mode 100644 index 0000000..b3d4ffe Binary files /dev/null and b/TestImages/training_examples/training_set_1.jpg differ diff --git a/TestImages/training_examples/training_set_2.jpg b/TestImages/training_examples/training_set_2.jpg new file mode 100644 index 0000000..32bd556 Binary files /dev/null and b/TestImages/training_examples/training_set_2.jpg differ diff --git a/TestImages/training_examples/training_set_3.jpg b/TestImages/training_examples/training_set_3.jpg new file mode 100644 index 0000000..8467af5 Binary files /dev/null and b/TestImages/training_examples/training_set_3.jpg differ diff --git a/TestImages/varying_quality/black.jpg b/TestImages/varying_quality/black.jpg new file mode 100644 index 0000000..dc90cae Binary files /dev/null and b/TestImages/varying_quality/black.jpg differ diff --git a/TestImages/varying_quality/card_in_plastic_case.jpg b/TestImages/varying_quality/card_in_plastic_case.jpg new file mode 100644 index 0000000..e771a5c Binary files /dev/null and b/TestImages/varying_quality/card_in_plastic_case.jpg differ diff --git a/TestImages/varying_quality/counterspell_bgs.jpg b/TestImages/varying_quality/counterspell_bgs.jpg new file mode 100644 index 0000000..25a8e1c Binary files /dev/null and b/TestImages/varying_quality/counterspell_bgs.jpg differ diff --git a/TestImages/varying_quality/dragon_whelp.jpg b/TestImages/varying_quality/dragon_whelp.jpg new file mode 100644 index 0000000..effdde6 Binary files /dev/null and b/TestImages/varying_quality/dragon_whelp.jpg differ diff --git a/TestImages/varying_quality/evil_eye.jpg b/TestImages/varying_quality/evil_eye.jpg new file mode 100644 index 0000000..faad74e Binary files /dev/null and b/TestImages/varying_quality/evil_eye.jpg differ diff --git a/TestImages/varying_quality/frilly.jpg b/TestImages/varying_quality/frilly.jpg new file mode 100644 index 0000000..5ab39fd Binary files /dev/null and b/TestImages/varying_quality/frilly.jpg differ diff --git a/TestImages/varying_quality/image_orig.jpg b/TestImages/varying_quality/image_orig.jpg new file mode 100644 index 0000000..440ad18 Binary files /dev/null and b/TestImages/varying_quality/image_orig.jpg differ diff --git a/TestImages/varying_quality/instill_energy.jpg b/TestImages/varying_quality/instill_energy.jpg new file mode 100644 index 0000000..c443961 Binary files /dev/null and b/TestImages/varying_quality/instill_energy.jpg differ diff --git a/TestImages/varying_quality/ruby.jpg b/TestImages/varying_quality/ruby.jpg new file mode 100644 index 0000000..a343232 Binary files /dev/null and b/TestImages/varying_quality/ruby.jpg differ diff --git a/TestImages/varying_quality/s-l300.jpg b/TestImages/varying_quality/s-l300.jpg new file mode 100644 index 0000000..819daca Binary files /dev/null and b/TestImages/varying_quality/s-l300.jpg differ diff --git a/TestImages/varying_quality/test.jpg b/TestImages/varying_quality/test.jpg new file mode 100644 index 0000000..233ffa8 Binary files /dev/null and b/TestImages/varying_quality/test.jpg differ diff --git a/TestImages/varying_quality/test1.jpg b/TestImages/varying_quality/test1.jpg new file mode 100644 index 0000000..a75278e Binary files /dev/null and b/TestImages/varying_quality/test1.jpg differ diff --git a/TestImages/varying_quality/test10.jpg b/TestImages/varying_quality/test10.jpg new file mode 100644 index 0000000..8e9062b Binary files /dev/null and b/TestImages/varying_quality/test10.jpg differ diff --git a/TestImages/varying_quality/test11.jpg b/TestImages/varying_quality/test11.jpg new file mode 100644 index 0000000..b0795f4 Binary files /dev/null and b/TestImages/varying_quality/test11.jpg differ diff --git a/TestImages/varying_quality/test12.jpg b/TestImages/varying_quality/test12.jpg new file mode 100644 index 0000000..c2f5de6 Binary files /dev/null and b/TestImages/varying_quality/test12.jpg differ diff --git a/TestImages/varying_quality/test13.jpg b/TestImages/varying_quality/test13.jpg new file mode 100644 index 0000000..878cbad Binary files /dev/null and b/TestImages/varying_quality/test13.jpg differ diff --git a/TestImages/varying_quality/test14.jpg b/TestImages/varying_quality/test14.jpg new file mode 100644 index 0000000..bf5094a Binary files /dev/null and b/TestImages/varying_quality/test14.jpg differ diff --git a/TestImages/varying_quality/test15.jpg b/TestImages/varying_quality/test15.jpg new file mode 100644 index 0000000..39f1dd4 Binary files /dev/null and b/TestImages/varying_quality/test15.jpg differ diff --git a/TestImages/varying_quality/test16.jpg b/TestImages/varying_quality/test16.jpg new file mode 100644 index 0000000..c514771 Binary files /dev/null and b/TestImages/varying_quality/test16.jpg differ diff --git a/TestImages/varying_quality/test17.jpg b/TestImages/varying_quality/test17.jpg new file mode 100644 index 0000000..4ad12f7 Binary files /dev/null and b/TestImages/varying_quality/test17.jpg differ diff --git a/TestImages/varying_quality/test18.jpg b/TestImages/varying_quality/test18.jpg new file mode 100644 index 0000000..a0f9390 Binary files /dev/null and b/TestImages/varying_quality/test18.jpg differ diff --git a/TestImages/varying_quality/test19.jpg b/TestImages/varying_quality/test19.jpg new file mode 100644 index 0000000..8f3c5a6 Binary files /dev/null and b/TestImages/varying_quality/test19.jpg differ diff --git a/TestImages/varying_quality/test2.jpg b/TestImages/varying_quality/test2.jpg new file mode 100644 index 0000000..1fceb1f Binary files /dev/null and b/TestImages/varying_quality/test2.jpg differ diff --git a/TestImages/varying_quality/test20.jpg b/TestImages/varying_quality/test20.jpg new file mode 100644 index 0000000..8717d5f Binary files /dev/null and b/TestImages/varying_quality/test20.jpg differ diff --git a/TestImages/varying_quality/test21.jpg b/TestImages/varying_quality/test21.jpg new file mode 100644 index 0000000..342577c Binary files /dev/null and b/TestImages/varying_quality/test21.jpg differ diff --git a/TestImages/varying_quality/test22.png b/TestImages/varying_quality/test22.png new file mode 100644 index 0000000..179f188 Binary files /dev/null and b/TestImages/varying_quality/test22.png differ diff --git a/TestImages/varying_quality/test23.jpg b/TestImages/varying_quality/test23.jpg new file mode 100644 index 0000000..af79a6f Binary files /dev/null and b/TestImages/varying_quality/test23.jpg differ diff --git a/TestImages/varying_quality/test24.jpg b/TestImages/varying_quality/test24.jpg new file mode 100644 index 0000000..937354c Binary files /dev/null and b/TestImages/varying_quality/test24.jpg differ diff --git a/TestImages/varying_quality/test25.jpg b/TestImages/varying_quality/test25.jpg new file mode 100644 index 0000000..6e39077 Binary files /dev/null and b/TestImages/varying_quality/test25.jpg differ diff --git a/TestImages/varying_quality/test26.jpg b/TestImages/varying_quality/test26.jpg new file mode 100644 index 0000000..ee83759 Binary files /dev/null and b/TestImages/varying_quality/test26.jpg differ diff --git a/TestImages/varying_quality/test27.jpg b/TestImages/varying_quality/test27.jpg new file mode 100644 index 0000000..0ee79be Binary files /dev/null and b/TestImages/varying_quality/test27.jpg differ diff --git a/TestImages/varying_quality/test3.jpg b/TestImages/varying_quality/test3.jpg new file mode 100644 index 0000000..fd1f2cb Binary files /dev/null and b/TestImages/varying_quality/test3.jpg differ diff --git a/TestImages/varying_quality/test4.jpg b/TestImages/varying_quality/test4.jpg new file mode 100644 index 0000000..1f2ffc6 Binary files /dev/null and b/TestImages/varying_quality/test4.jpg differ diff --git a/TestImages/varying_quality/test5.jpg b/TestImages/varying_quality/test5.jpg new file mode 100644 index 0000000..f9e8a1f Binary files /dev/null and b/TestImages/varying_quality/test5.jpg differ diff --git a/TestImages/varying_quality/test6.jpg b/TestImages/varying_quality/test6.jpg new file mode 100644 index 0000000..1454673 Binary files /dev/null and b/TestImages/varying_quality/test6.jpg differ diff --git a/TestImages/varying_quality/test7.jpg b/TestImages/varying_quality/test7.jpg new file mode 100644 index 0000000..82dfb3c Binary files /dev/null and b/TestImages/varying_quality/test7.jpg differ diff --git a/TestImages/varying_quality/test8.jpg b/TestImages/varying_quality/test8.jpg new file mode 100644 index 0000000..2d480ce Binary files /dev/null and b/TestImages/varying_quality/test8.jpg differ diff --git a/TestImages/varying_quality/test9.jpg b/TestImages/varying_quality/test9.jpg new file mode 100644 index 0000000..c8b0f53 Binary files /dev/null and b/TestImages/varying_quality/test9.jpg differ diff --git a/TestImages/worn/bent_creased.jpg b/TestImages/worn/bent_creased.jpg new file mode 100644 index 0000000..18c948a Binary files /dev/null and b/TestImages/worn/bent_creased.jpg differ diff --git a/TestImages/worn/edge_nick.png b/TestImages/worn/edge_nick.png new file mode 100644 index 0000000..68a7251 Binary files /dev/null and b/TestImages/worn/edge_nick.png differ diff --git a/TestImages/worn/edge_white.png b/TestImages/worn/edge_white.png new file mode 100644 index 0000000..1c91723 Binary files /dev/null and b/TestImages/worn/edge_white.png differ diff --git a/TestImages/worn/good_1.jpg b/TestImages/worn/good_1.jpg new file mode 100644 index 0000000..cd0007e Binary files /dev/null and b/TestImages/worn/good_1.jpg differ diff --git a/TestImages/worn/good_2.jpg b/TestImages/worn/good_2.jpg new file mode 100644 index 0000000..bd6e04e Binary files /dev/null and b/TestImages/worn/good_2.jpg differ diff --git a/TestImages/worn/hp_binder_bite_back.webp b/TestImages/worn/hp_binder_bite_back.webp new file mode 100644 index 0000000..727f380 Binary files /dev/null and b/TestImages/worn/hp_binder_bite_back.webp differ diff --git a/TestImages/worn/hp_binder_bite_front.webp b/TestImages/worn/hp_binder_bite_front.webp new file mode 100644 index 0000000..936ce8d Binary files /dev/null and b/TestImages/worn/hp_binder_bite_front.webp differ diff --git a/TestImages/worn/hp_compromised_corner.webp b/TestImages/worn/hp_compromised_corner.webp new file mode 100644 index 0000000..8665a6d Binary files /dev/null and b/TestImages/worn/hp_compromised_corner.webp differ diff --git a/TestImages/worn/hp_scratches.png b/TestImages/worn/hp_scratches.png new file mode 100644 index 0000000..b179f72 Binary files /dev/null and b/TestImages/worn/hp_scratches.png differ diff --git a/TestImages/worn/hp_shuffle_crease.webp b/TestImages/worn/hp_shuffle_crease.webp new file mode 100644 index 0000000..6ad1542 Binary files /dev/null and b/TestImages/worn/hp_shuffle_crease.webp differ diff --git a/TestImages/worn/hp_water_warping.png b/TestImages/worn/hp_water_warping.png new file mode 100644 index 0000000..590dfc0 Binary files /dev/null and b/TestImages/worn/hp_water_warping.png differ diff --git a/TestImages/worn/scratch.png b/TestImages/worn/scratch.png new file mode 100644 index 0000000..d7830d6 Binary files /dev/null and b/TestImages/worn/scratch.png differ diff --git a/TestImages/worn/spotting.png b/TestImages/worn/spotting.png new file mode 100644 index 0000000..f559c42 Binary files /dev/null and b/TestImages/worn/spotting.png differ diff --git a/TestImages/worn/very_good_1.jpg b/TestImages/worn/very_good_1.jpg new file mode 100644 index 0000000..938cd43 Binary files /dev/null and b/TestImages/worn/very_good_1.jpg differ diff --git a/TestImages/worn/very_good_2.jpg b/TestImages/worn/very_good_2.jpg new file mode 100644 index 0000000..2431c08 Binary files /dev/null and b/TestImages/worn/very_good_2.jpg differ diff --git a/app.json b/app.json new file mode 100644 index 0000000..91820a9 --- /dev/null +++ b/app.json @@ -0,0 +1,46 @@ +{ + "expo": { + "name": "Scry", + "slug": "scry", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/images/icon.png", + "scheme": "scry", + "userInterfaceStyle": "automatic", + "newArchEnabled": true, + "splash": { + "image": "./assets/images/splash-icon.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "ios": { + "supportsTablet": true + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/images/adaptive-icon.png", + "backgroundColor": "#ffffff" + }, + "edgeToEdgeEnabled": true, + "predictiveBackGestureEnabled": false + }, + "web": { + "bundler": "metro", + "output": "static", + "favicon": "./assets/images/favicon.png" + }, + "plugins": [ + "expo-router", + [ + "expo-media-library", + { + "photosPermission": "Allow Scry to save debug images to your photo library.", + "savePhotosPermission": "Allow Scry to save debug images to your photo library." + } + ] + ], + "experiments": { + "typedRoutes": true + } + } +} diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx new file mode 100644 index 0000000..8454c7d --- /dev/null +++ b/app/(tabs)/_layout.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import FontAwesome from "@expo/vector-icons/FontAwesome"; +import { Tabs } from "expo-router"; +import { useColorScheme } from "@/components/useColorScheme"; +import Colors from "@/constants/Colors"; + +function TabBarIcon(props: { + name: React.ComponentProps["name"]; + color: string; +}) { + return ; +} + +export default function TabLayout() { + const colorScheme = useColorScheme(); + + return ( + + ( + + ), + }} + /> + , + }} + /> + , + }} + /> + + ); +} diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx new file mode 100644 index 0000000..3219241 --- /dev/null +++ b/app/(tabs)/index.tsx @@ -0,0 +1,406 @@ +/** + * Collection screen - displays user's scanned cards. + */ + +import React, { useState, useCallback, useMemo } from "react"; +import { + StyleSheet, + View, + Text, + FlatList, + Image, + Pressable, + TextInput, + RefreshControl, + Dimensions, + ActivityIndicator, +} from "react-native"; +import { FontAwesome } from "@expo/vector-icons"; +import { useQuery } from "convex/react"; +import { useRouter } from "expo-router"; +import { api } from "../../convex/_generated/api"; +import { useCurrentUser } from "@/lib/hooks"; + +const { width: SCREEN_WIDTH } = Dimensions.get("window"); +const CARD_WIDTH = (SCREEN_WIDTH - 48) / 3; +const CARD_HEIGHT = CARD_WIDTH * (88 / 63); // MTG aspect ratio + +interface CollectionItem { + id: string; + cardId: string; + name: string; + setCode: string; + imageUri?: string; + quantity: number; + isFoil: boolean; + addedAt: number; +} + +export default function CollectionScreen() { + const router = useRouter(); + const [searchQuery, setSearchQuery] = useState(""); + const [refreshing, setRefreshing] = useState(false); + const [sortBy, setSortBy] = useState<"name" | "set" | "recent">("recent"); + + // Get authenticated user + const { user, isAuthenticated } = useCurrentUser(); + const userId = user?._id ?? null; + + // Fetch collection from Convex + const rawCollection = useQuery( + api.collections.getByUser, + userId ? { userId: userId as any } : "skip" + ); + + // Transform Convex data to UI format + const collection = useMemo(() => { + if (!rawCollection) return []; + + return rawCollection.map((entry) => ({ + id: entry._id, + cardId: entry.cardId, + name: entry.card?.name || "Unknown", + setCode: entry.card?.setCode || "???", + imageUri: entry.card?.imageUri, + quantity: entry.quantity, + isFoil: entry.isFoil, + addedAt: entry.addedAt, + })); + }, [rawCollection]); + + // Filter collection by search query + const filteredCollection = useMemo( + () => + collection.filter((item) => + item.name.toLowerCase().includes(searchQuery.toLowerCase()) + ), + [collection, searchQuery] + ); + + // Sort collection + const sortedCollection = useMemo( + () => + [...filteredCollection].sort((a, b) => { + switch (sortBy) { + case "name": + return a.name.localeCompare(b.name); + case "set": + return a.setCode.localeCompare(b.setCode); + case "recent": + default: + return b.addedAt - a.addedAt; + } + }), + [filteredCollection, sortBy] + ); + + const onRefresh = useCallback(async () => { + setRefreshing(true); + // Convex automatically syncs - just show loading briefly + await new Promise((resolve) => setTimeout(resolve, 500)); + setRefreshing(false); + }, []); + + const renderCard = useCallback( + ({ item }: { item: CollectionItem }) => ( + + router.push({ + pathname: "/modal", + params: { + collectionEntryId: item.id, + cardId: item.cardId, + }, + }) + } + > + {item.imageUri ? ( + + ) : ( + + + + )} + {item.quantity > 1 && ( + + ×{item.quantity} + + )} + {item.isFoil && ( + + + + )} + + ), + [router] + ); + + const renderEmpty = useCallback( + () => ( + + + No Cards Yet + + {userId + ? "Start scanning cards to build your collection!" + : "Sign in to start building your collection!"} + + + ), + [userId] + ); + + // Loading state + if (rawCollection === undefined && userId) { + return ( + + + Loading collection... + + ); + } + + // Total cards count + const totalCount = collection.reduce((sum, item) => sum + item.quantity, 0); + + return ( + + {/* Search bar */} + + + + {searchQuery.length > 0 && ( + setSearchQuery("")}> + + + )} + + + {/* Stats and sort bar */} + + + {sortedCollection.length} unique + {collection.length > 0 && ` (${totalCount} total)`} + + + + setSortBy("recent")} + > + + Recent + + + setSortBy("name")} + > + + Name + + + setSortBy("set")} + > + + Set + + + + + + {/* Card grid */} + item.id} + numColumns={3} + contentContainerStyle={styles.gridContent} + ListEmptyComponent={renderEmpty} + refreshControl={ + + } + /> + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#1a1a1a", + }, + searchContainer: { + flexDirection: "row", + alignItems: "center", + backgroundColor: "#2a2a2a", + marginHorizontal: 16, + marginTop: 16, + marginBottom: 8, + paddingHorizontal: 12, + borderRadius: 10, + }, + searchIcon: { + marginRight: 8, + }, + searchInput: { + flex: 1, + height: 44, + color: "#fff", + fontSize: 16, + }, + statsBar: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingHorizontal: 16, + paddingVertical: 8, + }, + statsText: { + color: "#888", + fontSize: 14, + }, + sortButtons: { + flexDirection: "row", + gap: 4, + }, + sortButton: { + paddingHorizontal: 10, + paddingVertical: 6, + borderRadius: 6, + }, + sortButtonActive: { + backgroundColor: "#333", + }, + sortButtonText: { + color: "#666", + fontSize: 12, + fontWeight: "500", + }, + sortButtonTextActive: { + color: "#fff", + }, + gridContent: { + padding: 16, + paddingTop: 8, + }, + cardContainer: { + width: CARD_WIDTH, + height: CARD_HEIGHT, + marginRight: 8, + marginBottom: 8, + borderRadius: 8, + overflow: "hidden", + }, + cardImage: { + width: "100%", + height: "100%", + backgroundColor: "#2a2a2a", + borderRadius: 8, + }, + cardPlaceholder: { + justifyContent: "center", + alignItems: "center", + }, + quantityBadge: { + position: "absolute", + top: 4, + right: 4, + backgroundColor: "rgba(0,0,0,0.8)", + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: 4, + }, + quantityText: { + color: "#fff", + fontSize: 11, + fontWeight: "600", + }, + foilBadge: { + position: "absolute", + top: 4, + left: 4, + backgroundColor: "rgba(0,0,0,0.8)", + padding: 4, + borderRadius: 4, + }, + emptyContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + paddingTop: 100, + paddingHorizontal: 40, + }, + emptyTitle: { + marginTop: 20, + fontSize: 22, + fontWeight: "bold", + color: "#fff", + }, + emptyText: { + marginTop: 8, + fontSize: 16, + color: "#888", + textAlign: "center", + }, + loadingContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + backgroundColor: "#1a1a1a", + }, + loadingText: { + marginTop: 16, + color: "#888", + fontSize: 16, + }, +}); diff --git a/app/(tabs)/scan.tsx b/app/(tabs)/scan.tsx new file mode 100644 index 0000000..b7ab201 --- /dev/null +++ b/app/(tabs)/scan.tsx @@ -0,0 +1,591 @@ +/** + * Camera scanning screen for card recognition. + */ + +import React, { useCallback, useRef, useState, useEffect } from "react"; +import { + StyleSheet, + View, + Text, + Pressable, + ActivityIndicator, + Dimensions, + Image, + Alert, +} from "react-native"; +import { FontAwesome } from "@expo/vector-icons"; +import { useMutation, useQuery } from "convex/react"; +import { api } from "../../convex/_generated/api"; +import { useCameraPermission } from "@/lib/hooks/useCamera"; +import { useCurrentUser } from "@/lib/hooks"; +import { useHashCache } from "@/lib/context"; +import { recognizeCard, configureDebug } from "@/lib/recognition"; +import { loadImageAsBase64 } from "@/lib/recognition/imageLoader"; +import { decodeImageBase64 } from "@/lib/recognition/skiaDecoder"; +import { AdaptiveCamera, CameraHandle, isExpoGo } from "@/components/camera"; + +const { height: SCREEN_HEIGHT } = Dimensions.get("window"); +const CARD_ASPECT_RATIO = 63 / 88; // Width / Height +const SCAN_BOX_HEIGHT = SCREEN_HEIGHT * 0.5; +const SCAN_BOX_WIDTH = SCAN_BOX_HEIGHT * CARD_ASPECT_RATIO; + +interface ScanResult { + cardId: string; + cardName: string; + setCode: string; + imageUri?: string; + confidence: number; + distance: number; + timestamp: number; +} + +export default function ScanScreen() { + const { hasPermission, isLoading: permLoading, requestPermission } = useCameraPermission(); + const cameraRef = useRef(null); + + // Local scan state + const [isProcessing, setIsProcessing] = useState(false); + const [flashEnabled, setFlashEnabled] = useState(false); + const [isAddingToCollection, setIsAddingToCollection] = useState(false); + const [lastScanResult, setLastScanResult] = useState(null); + const [scanError, setScanError] = useState(null); + const [debugEnabled, setDebugEnabled] = useState(false); + + // Hash cache from context + const { cardHashes, hashesLoaded } = useHashCache(); + + // Get authenticated user + const { user, isAuthenticated } = useCurrentUser(); + const userId = user?._id ?? null; + + // Convex mutations + const addToCollection = useMutation(api.collections.add); + const getCardByScryfallId = useQuery( + api.cards.getByScryfallId, + lastScanResult ? { scryfallId: lastScanResult.cardId } : "skip" + ); + + // Toggle debug mode + const toggleDebug = useCallback(() => { + const newState = !debugEnabled; + setDebugEnabled(newState); + configureDebug({ + enabled: newState, + albumName: "Scry Debug", + }); + console.log("[Scry] Debug mode:", newState ? "ON" : "OFF"); + }, [debugEnabled]); + + const clearScanState = useCallback(() => { + setLastScanResult(null); + setScanError(null); + }, []); + + // Auto-clear scan result after 5 seconds + useEffect(() => { + if (lastScanResult) { + const timer = setTimeout(() => { + clearScanState(); + }, 5000); + return () => clearTimeout(timer); + } + }, [lastScanResult, clearScanState]); + + const handleCapture = useCallback(async () => { + if (!cameraRef.current || isProcessing || !hashesLoaded) return; + + setIsProcessing(true); + + try { + // Take photo using adaptive camera + const photo = await cameraRef.current.takePhoto(); + + console.log("[Scry] Photo captured:", photo.uri); + + // Load and resize image for processing + const { base64 } = await loadImageAsBase64( + photo.uri, + 480, // Target width for processing + 640 // Target height + ); + + if (!base64) { + throw new Error("Failed to load image data"); + } + + // Decode to RGBA pixels using Skia + const decoded = decodeImageBase64(base64); + + if (!decoded) { + throw new Error("Failed to decode image pixels"); + } + + console.log("[Scry] Image decoded:", decoded.width, "x", decoded.height); + console.log("[Scry] Matching against", cardHashes.length, "cards"); + + // Run recognition + const result = recognizeCard( + decoded.pixels, + decoded.width, + decoded.height, + cardHashes, + { + enableCardDetection: true, + enableRotationMatching: true, + minConfidence: 0.85, + debug: debugEnabled, + } + ); + + console.log("[Scry] Recognition result:", result); + + if (result.success && result.match) { + // Find the card info from our hashes + const matchedCard = cardHashes.find((c) => c.id === result.match!.cardId); + + setLastScanResult({ + cardId: result.match.cardId, + cardName: matchedCard?.name || "Unknown Card", + setCode: matchedCard?.setCode || "???", + imageUri: matchedCard?.imageUri, + confidence: result.match.confidence, + distance: result.match.distance, + timestamp: Date.now(), + }); + setScanError(null); + } else { + setScanError(result.error || "No match found. Try adjusting lighting or angle."); + setLastScanResult(null); + } + } catch (error) { + console.error("[Scry] Capture error:", error); + setScanError(error instanceof Error ? error.message : "Failed to capture image"); + setLastScanResult(null); + } finally { + setIsProcessing(false); + } + }, [isProcessing, hashesLoaded, cardHashes, debugEnabled]); + + const handleAddToCollection = useCallback(async () => { + if (!lastScanResult || !userId || !getCardByScryfallId) return; + + setIsAddingToCollection(true); + + try { + await addToCollection({ + userId: userId as any, + cardId: getCardByScryfallId._id, + quantity: 1, + isFoil: false, + }); + + Alert.alert( + "Added to Collection", + `${lastScanResult.cardName} has been added to your collection.`, + [{ text: "OK" }] + ); + + clearScanState(); + } catch (error) { + console.error("[Scry] Failed to add to collection:", error); + Alert.alert("Error", "Failed to add card to collection. Please try again.", [ + { text: "OK" }, + ]); + } finally { + setIsAddingToCollection(false); + } + }, [lastScanResult, userId, getCardByScryfallId, addToCollection, clearScanState]); + + // Loading state + if (permLoading) { + return ( + + + Checking camera permission... + + ); + } + + // Permission denied + if (!hasPermission) { + return ( + + + Camera Access Required + + Scry needs camera access to scan your Magic cards. + + + Enable Camera + + + ); + } + + return ( + + {/* Adaptive camera - uses expo-camera in Expo Go, Vision Camera in production */} + + + {/* Overlay with scan box */} + + {/* Top dark area */} + + + {/* Middle row with scan box */} + + + + {/* Corner markers */} + + + + + + + + + {/* Bottom dark area */} + + + + {/* Instructions */} + + + {hashesLoaded + ? `Position card in frame • ${cardHashes.length} cards loaded` + : "Loading card database..."} + + {isExpoGo && ( + Dev mode (expo-camera) + )} + + + {/* Scan result overlay */} + {lastScanResult && ( + + + {lastScanResult.imageUri && ( + + )} + + {lastScanResult.cardName} + {lastScanResult.setCode.toUpperCase()} + + {Math.round(lastScanResult.confidence * 100)}% match + + + + {isAddingToCollection ? ( + + ) : ( + <> + + Add + + )} + + + + )} + + {/* Error message */} + {scanError && ( + + {scanError} + + Dismiss + + + )} + + {/* Controls */} + + {/* Flash toggle */} + setFlashEnabled(!flashEnabled)}> + + + + {/* Capture button */} + + {isProcessing ? ( + + ) : ( + + )} + + + {/* Debug toggle */} + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#000", + }, + centered: { + flex: 1, + justifyContent: "center", + alignItems: "center", + backgroundColor: "#1a1a1a", + padding: 20, + }, + loadingText: { + marginTop: 16, + color: "#888", + fontSize: 16, + }, + permissionTitle: { + marginTop: 20, + fontSize: 24, + fontWeight: "bold", + color: "#fff", + }, + permissionText: { + marginTop: 12, + fontSize: 16, + color: "#888", + textAlign: "center", + maxWidth: 280, + }, + permissionButton: { + marginTop: 24, + backgroundColor: "#007AFF", + paddingHorizontal: 32, + paddingVertical: 14, + borderRadius: 12, + }, + permissionButtonText: { + color: "#fff", + fontSize: 18, + fontWeight: "600", + }, + errorText: { + marginTop: 16, + fontSize: 18, + color: "#FF6B6B", + textAlign: "center", + }, + overlay: { + ...StyleSheet.absoluteFillObject, + }, + overlayTop: { + flex: 1, + backgroundColor: "rgba(0,0,0,0.6)", + }, + overlayMiddle: { + flexDirection: "row", + height: SCAN_BOX_HEIGHT, + }, + overlaySide: { + flex: 1, + backgroundColor: "rgba(0,0,0,0.6)", + }, + scanBox: { + width: SCAN_BOX_WIDTH, + height: SCAN_BOX_HEIGHT, + borderWidth: 2, + borderColor: "rgba(255,255,255,0.3)", + borderRadius: 12, + }, + overlayBottom: { + flex: 1, + backgroundColor: "rgba(0,0,0,0.6)", + }, + corner: { + position: "absolute", + width: 24, + height: 24, + borderColor: "#007AFF", + }, + cornerTopLeft: { + top: -2, + left: -2, + borderTopWidth: 4, + borderLeftWidth: 4, + borderTopLeftRadius: 12, + }, + cornerTopRight: { + top: -2, + right: -2, + borderTopWidth: 4, + borderRightWidth: 4, + borderTopRightRadius: 12, + }, + cornerBottomLeft: { + bottom: -2, + left: -2, + borderBottomWidth: 4, + borderLeftWidth: 4, + borderBottomLeftRadius: 12, + }, + cornerBottomRight: { + bottom: -2, + right: -2, + borderBottomWidth: 4, + borderRightWidth: 4, + borderBottomRightRadius: 12, + }, + instructionContainer: { + position: "absolute", + top: 60, + left: 0, + right: 0, + alignItems: "center", + }, + instructionText: { + color: "#fff", + fontSize: 14, + backgroundColor: "rgba(0,0,0,0.5)", + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 20, + }, + devModeText: { + marginTop: 8, + color: "#FFD700", + fontSize: 12, + backgroundColor: "rgba(0,0,0,0.5)", + paddingHorizontal: 12, + paddingVertical: 4, + borderRadius: 12, + }, + resultContainer: { + position: "absolute", + bottom: 140, + left: 16, + right: 16, + }, + resultCard: { + flexDirection: "row", + backgroundColor: "rgba(0,0,0,0.9)", + borderRadius: 12, + padding: 12, + alignItems: "center", + }, + resultImage: { + width: 50, + height: 70, + borderRadius: 4, + }, + resultInfo: { + flex: 1, + marginLeft: 12, + }, + resultName: { + color: "#fff", + fontSize: 16, + fontWeight: "600", + }, + resultSet: { + color: "#888", + fontSize: 12, + marginTop: 2, + }, + resultConfidence: { + color: "#4CD964", + fontSize: 12, + marginTop: 4, + }, + addButton: { + flexDirection: "row", + alignItems: "center", + backgroundColor: "#4CD964", + paddingHorizontal: 16, + paddingVertical: 10, + borderRadius: 8, + gap: 6, + }, + addButtonDisabled: { + opacity: 0.5, + }, + addButtonText: { + color: "#fff", + fontSize: 14, + fontWeight: "600", + }, + errorContainer: { + position: "absolute", + bottom: 140, + left: 16, + right: 16, + backgroundColor: "rgba(255,107,107,0.95)", + borderRadius: 12, + padding: 16, + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + }, + errorMessage: { + color: "#fff", + fontSize: 14, + flex: 1, + }, + errorDismiss: { + color: "#fff", + fontSize: 14, + fontWeight: "600", + marginLeft: 12, + }, + controls: { + position: "absolute", + bottom: 40, + left: 0, + right: 0, + flexDirection: "row", + justifyContent: "space-around", + alignItems: "center", + paddingHorizontal: 40, + }, + controlButton: { + width: 50, + height: 50, + justifyContent: "center", + alignItems: "center", + }, + captureButton: { + width: 72, + height: 72, + borderRadius: 36, + backgroundColor: "#fff", + justifyContent: "center", + alignItems: "center", + borderWidth: 4, + borderColor: "rgba(255,255,255,0.3)", + }, + captureButtonDisabled: { + opacity: 0.5, + }, + captureButtonInner: { + width: 56, + height: 56, + borderRadius: 28, + backgroundColor: "#fff", + }, +}); diff --git a/app/(tabs)/settings.tsx b/app/(tabs)/settings.tsx new file mode 100644 index 0000000..5fa9c83 --- /dev/null +++ b/app/(tabs)/settings.tsx @@ -0,0 +1,313 @@ +/** + * Settings screen for Scry app. + */ + +import React, { useState } from "react"; +import { + StyleSheet, + View, + Text, + ScrollView, + Pressable, + Switch, + ActivityIndicator, + Alert, +} from "react-native"; +import { FontAwesome } from "@expo/vector-icons"; +import { useQuery } from "convex/react"; +import { api } from "../../convex/_generated/api"; +import { useSync } from "@/lib/hooks/useSync"; +import { useHashCache } from "@/lib/context"; + +interface SettingRowProps { + icon: React.ComponentProps["name"]; + title: string; + subtitle?: string; + onPress?: () => void; + rightElement?: React.ReactNode; + destructive?: boolean; +} + +function SettingRow({ + icon, + title, + subtitle, + onPress, + rightElement, + destructive, +}: SettingRowProps) { + return ( + [ + styles.settingRow, + pressed && onPress && styles.settingRowPressed, + ]} + onPress={onPress} + disabled={!onPress} + > + + + + + + {title} + + {subtitle && {subtitle}} + + {rightElement || (onPress && )} + + ); +} + +function SettingSection({ title, children }: { title: string; children: React.ReactNode }) { + return ( + + {title} + {children} + + ); +} + +export default function SettingsScreen() { + const [cardDetectionEnabled, setCardDetectionEnabled] = useState(true); + const [rotationMatchingEnabled, setRotationMatchingEnabled] = useState(true); + + // Get hash count from context + const { cardHashes, hashesLoaded } = useHashCache(); + + // Sync hook for cache management + const { isInitialized, isSyncing, lastSync, localCardCount, error: syncError, sync, clearCache } = + useSync(); + + // Get total card count from Convex + const cardCount = useQuery(api.cards.count); + + const formatLastSync = (timestamp: number) => { + if (!timestamp) return "Never"; + const date = new Date(timestamp); + return date.toLocaleDateString() + " " + date.toLocaleTimeString(); + }; + + const handleClearCache = () => { + Alert.alert( + "Clear Local Cache", + "This will remove all downloaded card data. You'll need to sync again to scan cards.", + [ + { text: "Cancel", style: "cancel" }, + { + text: "Clear", + style: "destructive", + onPress: async () => { + await clearCache(); + Alert.alert("Cache Cleared", "Local card data has been removed."); + }, + }, + ] + ); + }; + + const handleManualSync = async () => { + await sync(); + Alert.alert("Sync Complete", `${localCardCount} cards now available for scanning.`); + }; + + return ( + + {/* Database section */} + + : undefined} + /> + : undefined} + /> + {syncError && ( + + )} + + + {/* Recognition section */} + + + } + /> + + } + /> + + + {/* Collection section */} + + + { + // TODO: Implement export + }} + /> + + + {/* About section */} + + + { + // TODO: Open source URL + }} + /> + + + {/* Danger zone */} + + { + Alert.alert( + "Clear Collection", + "This will remove all cards from your collection. This cannot be undone.", + [ + { text: "Cancel", style: "cancel" }, + { text: "Clear", style: "destructive", onPress: () => {} }, + ] + ); + }} + destructive + /> + + + + {/* Footer */} + + Scry • Card scanner for Magic: The Gathering + Card data © Wizards of the Coast + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#1a1a1a", + }, + content: { + paddingBottom: 40, + }, + section: { + marginTop: 24, + }, + sectionTitle: { + color: "#888", + fontSize: 13, + fontWeight: "600", + textTransform: "uppercase", + letterSpacing: 0.5, + marginLeft: 16, + marginBottom: 8, + }, + sectionContent: { + backgroundColor: "#2a2a2a", + borderRadius: 12, + marginHorizontal: 16, + overflow: "hidden", + }, + settingRow: { + flexDirection: "row", + alignItems: "center", + padding: 14, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: "#3a3a3a", + }, + settingRowPressed: { + backgroundColor: "#333", + }, + settingIcon: { + width: 32, + height: 32, + borderRadius: 8, + backgroundColor: "rgba(0,122,255,0.1)", + justifyContent: "center", + alignItems: "center", + marginRight: 12, + }, + settingIconDestructive: { + backgroundColor: "rgba(255,107,107,0.1)", + }, + settingContent: { + flex: 1, + }, + settingTitle: { + color: "#fff", + fontSize: 16, + }, + settingTitleDestructive: { + color: "#FF6B6B", + }, + settingSubtitle: { + color: "#888", + fontSize: 13, + marginTop: 2, + }, + footer: { + marginTop: 40, + alignItems: "center", + paddingHorizontal: 16, + }, + footerText: { + color: "#666", + fontSize: 14, + }, + footerSubtext: { + color: "#444", + fontSize: 12, + marginTop: 4, + }, +}); diff --git a/app/+html.tsx b/app/+html.tsx new file mode 100644 index 0000000..cb31090 --- /dev/null +++ b/app/+html.tsx @@ -0,0 +1,38 @@ +import { ScrollViewStyleReset } from 'expo-router/html'; + +// This file is web-only and used to configure the root HTML for every +// web page during static rendering. +// The contents of this function only run in Node.js environments and +// do not have access to the DOM or browser APIs. +export default function Root({ children }: { children: React.ReactNode }) { + return ( + + + + + + + {/* + Disable body scrolling on web. This makes ScrollView components work closer to how they do on native. + However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line. + */} + + + {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */} +