{"id":51272890,"url":"https://github.com/arcsymer/pasicard","last_synced_at":"2026-06-29T19:30:32.657Z","repository":{"id":366714668,"uuid":"1277052859","full_name":"arcsymer/pasicard","owner":"arcsymer","description":"Native Samsung Tizen NUI flashcard app (C#/.NET 10) with a Leitner spaced-repetition engine and TizenFX integration","archived":false,"fork":false,"pushed_at":"2026-06-23T01:53:54.000Z","size":71,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-23T03:21:36.283Z","etag":null,"topics":["csharp","dotnet","maui","portfolio","spaced-repetition","tizen"],"latest_commit_sha":null,"homepage":null,"language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/arcsymer.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-22T14:37:27.000Z","updated_at":"2026-06-23T01:53:58.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/arcsymer/pasicard","commit_stats":null,"previous_names":["arcsymer/pasicard"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/arcsymer/pasicard","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arcsymer%2Fpasicard","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arcsymer%2Fpasicard/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arcsymer%2Fpasicard/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arcsymer%2Fpasicard/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/arcsymer","download_url":"https://codeload.github.com/arcsymer/pasicard/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arcsymer%2Fpasicard/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34941025,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-29T02:00:05.398Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["csharp","dotnet","maui","portfolio","spaced-repetition","tizen"],"created_at":"2026-06-29T19:30:29.044Z","updated_at":"2026-06-29T19:30:32.641Z","avatar_url":"https://github.com/arcsymer.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PasiCard — Tizen .NET Spaced-Repetition Flashcard Trainer\n\n[![CI](https://img.shields.io/github/actions/workflow/status/arcsymer/pasicard/ci.yml?branch=main\u0026label=CI\u0026logo=github)](https://github.com/arcsymer/pasicard/actions/workflows/ci.yml)\n[![tests: 171 passing](https://img.shields.io/badge/tests-171%20passing-brightgreen)](https://github.com/arcsymer/pasicard/actions)\n[![.NET 10](https://img.shields.io/badge/.NET-10-512BD4?logo=dotnet)](https://dotnet.microsoft.com/)\n\nA native Samsung Tizen NUI app that teaches Polish food vocabulary using Leitner-box\nspaced repetition (SRS). I built it as a portfolio piece to show C#/.NET 10 work for\nSamsung R\u0026D Poland.\n\n---\n\n## Problem\n\nLearning menu vocabulary at a Polish fast-food chain (Pasibus) requires repeated exposure\nto Polish terms at the right time — not all at once. Standard flashcard apps are desktop\nor Android-only and ignore the Tizen platform entirely.\n\n## Solution\n\nPasiCard is a self-contained Tizen app:\n\n- Reviews only cards that are due today, using a 6-box Leitner algorithm.\n- Gives haptic feedback (via `Tizen.System.Feedback`) on each grade tap, with distinct\n  patterns for Again / Good / Easy.\n- Stores each card's next-due date across app restarts using\n  `Tizen.Applications.Preference` (platform key-value store, no SQLite needed).\n- Keeps all SRS logic in a platform-independent Core library with 171 passing unit tests\n  (152 test methods, some `[Theory]`-expanded), so it runs without any Tizen workload installed.\n\n---\n\n## Tech Stack\n\n| Component | Technology |\n|-----------|-----------|\n| Language | C# 13, .NET 10 |\n| UI layer | Tizen NUI (`Tizen.NUI.NUIApplication`, `TextLabel`, `LinearLayout`) |\n| Haptic feedback | TizenFX `Tizen.System.Feedback` API |\n| Persistence | TizenFX `Tizen.Applications.Preference` API |\n| ViewModel | CommunityToolkit.Mvvm 8.4 (`[ObservableProperty]`, `[RelayCommand]`) |\n| Tests | xUnit 2.9, 171 passing tests, net10.0 |\n| Package | .NET `maui-tizen` workload (10.0.x) |\n| Artifact | `.tpk` signed package (`org.pasicard.app-1.0.0.tpk`) |\n\n---\n\n## Architecture\n\n```\ntizen/\n├── PasiCard.sln\n├── src/\n│   ├── PasiCard.Core/               # net10.0 — pure business logic (no platform deps)\n│   │   ├── Card.cs                  # Flashcard model: Front / Back / LeitnerBox / DueDate / IsSuspended / ReviewHistory / RetentionRate\n│   │   ├── Deck.cs                  # Named collection + due-date filtering + GetDueCount + GetDueForecast\n│   │   ├── DeckSerializer.cs        # JSON import/export + TryImport with validation (DeckImportResult)\n│   │   ├── LeitnerConfig.cs         # Configurable per-box intervals (default: 1/3/7/14/30/60 days)\n│   │   ├── ReviewGrade.cs           # Enum: Again / Hard / Good / Easy\n│   │   ├── ReviewSession.cs         # Daily queue + SessionStats with AccuracyPercent; records CardReviewRecord per grade\n│   │   ├── SrsScheduler.cs          # Leitner scheduler — O(1) per grade, config+clock injectable\n│   │   ├── ICardSchedulePersister.cs# Persistence abstraction (NullCardSchedulePersister default)\n│   │   └── ReviewController.cs      # Grade+persist orchestration (testable, fixes graded-card-id bug)\n│   └── PasiCard.Tizen/              # net10.0-tizen — Samsung NUI application\n│       ├── Program.cs               # NUIApplication entry point; binds NUI widgets to VM\n│       ├── ViewModels/\n│       │   └── ReviewViewModel.cs   # MVVM state + RelayCommands; Tizen Preference persister + deck loader\n│       └── tizen-manifest.xml       # Tizen application manifest (org.pasicard.app)\n└── tests/\n    └── PasiCard.Tests/              # net10.0 xUnit — 171 passing tests, no Tizen workload needed\n        ├── SrsSchedulerTests.cs\n        ├── ReviewSessionTests.cs\n        ├── ReviewControllerTests.cs     # grade+persist: schedule saved against the GRADED card's id\n        ├── LeitnerConfigTests.cs\n        ├── CardSuspendResetTests.cs\n        ├── DeckSerializerTests.cs\n        ├── SessionStatsTests.cs\n        ├── CardReviewHistoryTests.cs    # per-card history, RetentionRate, CardReviewRecord\n        ├── DueForecastTests.cs          # GetDueForecast, overdue-priority ordering\n        └── DeckImportValidationTests.cs # TryImport, DeckImportResult, clamping + warnings\n```\n\n### Three-layer architecture\n\n```mermaid\ngraph TD\n    subgraph \"PasiCard.Core  (net10.0 — platform-independent)\"\n        LC[LeitnerConfig\\nconfigurable intervals]\n        S[SrsScheduler\\nO\u0026lpar;1\u0026rpar; per grade]\n        C[Card\\nFront/Back/Box/Due/IsSuspended]\n        D[Deck\\nGetDueCards · GetDueCount]\n        RS[ReviewSession\\nqueue · stats · AgainRequeue]\n        DS[DeckSerializer\\nJSON import/export]\n        LC --\u003e S\n        S --\u003e RS\n        C --\u003e D\n        D --\u003e RS\n    end\n\n    subgraph \"ViewModel  (net10.0-tizen)\"\n        VM[ReviewViewModel\\nObservableObject · RelayCommands\\nTizenFX: Feedback · Preference]\n    end\n\n    subgraph \"NUI UI  (net10.0-tizen)\"\n        UI[Program.cs\\nTextLabel · LinearLayout\\nPropertyChanged listener]\n    end\n\n    RS --\u003e VM\n    S  --\u003e VM\n    VM --\u003e UI\n```\n\n### Leitner box flow\n\n```mermaid\nstateDiagram-v2\n    direction LR\n    [*] --\u003e Box1 : new card\n    Box1 --\u003e Box1  : Hard\n    Box1 --\u003e Box2  : Good (+1)\n    Box1 --\u003e Box3  : Easy (+2)\n    Box2 --\u003e Box1  : Again (reset)\n    Box2 --\u003e Box2  : Hard\n    Box2 --\u003e Box3  : Good\n    Box2 --\u003e Box4  : Easy\n    Box3 --\u003e Box1  : Again\n    Box3 --\u003e Box3  : Hard\n    Box3 --\u003e Box4  : Good\n    Box3 --\u003e Box5  : Easy\n    Box4 --\u003e Box1  : Again\n    Box4 --\u003e Box4  : Hard\n    Box4 --\u003e Box5  : Good\n    Box4 --\u003e Box6  : Easy\n    Box5 --\u003e Box1  : Again\n    Box5 --\u003e Box5  : Hard\n    Box5 --\u003e Box6  : Good / Easy\n    Box6 --\u003e Box1  : Again\n    Box6 --\u003e Box6  : Hard / Good / Easy (retired)\n    Box6 --\u003e [*]   : retired (60 d interval)\n```\n\n| Box | Default interval | Configurable via `LeitnerConfig` |\n|-----|-----------------|----------------------------------|\n| 1 | 1 day | yes |\n| 2 | 3 days | yes |\n| 3 | 7 days | yes |\n| 4 | 14 days | yes |\n| 5 | 30 days | yes |\n| 6 (retired) | 60 days | yes |\n\nGrade rules: Again resets to box 1, due today; Hard stays in the current box;\nGood advances 1 box; Easy advances 2 boxes.\n\n### Layer isolation\n\n| Layer | Knows about | Does NOT know about |\n|-------|-------------|---------------------|\n| `PasiCard.Core` | Domain models, SRS rules, `System.Text.Json` | NUI, TizenFX, MVVM toolkit |\n| `ReviewViewModel` | Core domain, MVVM toolkit | NUI widget types |\n| `Program.cs` | NUI widgets, ViewModel | SRS rules, grading logic |\n\nThe three-layer split means the SRS algorithm is testable on any platform\n(Windows, Linux, CI) without a Tizen SDK or emulator.\n\n---\n\n## Notable bits\n\n- 171 passing unit tests covering all grade paths, edge cases (box clamping, Again re-queue,\n  session completion), clock injection, suspend/reset lifecycle, JSON roundtrip,\n  configurable intervals, accuracy statistics, per-card retention, due forecasting,\n  overdue-priority ordering, validated import with field-level error reporting, and the\n  grade+persist flow (`ReviewController` — schedule result saved against the graded card's id).\n- `LeitnerConfig` makes intervals a first-class injectable value — swap the schedule\n  without recompiling. `SrsScheduler(LeitnerConfig, Func\u003cDateOnly\u003e)` gives full control\n  over both dimensions; the no-arg constructor keeps existing call sites unchanged.\n- `Card.IsSuspended` allows a learner to hide cards from sessions temporarily.\n  `SrsScheduler.ResetCard()` clears the flag, sets box 1, and uses the injected clock.\n- `DeckSerializer` provides portable JSON export and import using only the built-in\n  `System.Text.Json` — no extra NuGet packages. The roundtrip preserves all card fields\n  including `Id`, `LeitnerBox`, `DueDate`, and `IsSuspended`.\n  `DeckSerializer.TryImport` returns a `DeckImportResult` with the parsed deck and a list\n  of human-readable warnings for out-of-range `LeitnerBox` values (clamped), blank\n  `Front`/`Back` text, and empty-Guid `Id` fields. Structural JSON errors still throw.\n- `SessionStats.AccuracyPercent` computes the share of non-Again reviews (0–100 %).\n  `Deck.GetDueCount(DateOnly)` returns a due-card count without allocating a list.\n- `Card.ReviewHistory` is a chronological `List\u003cCardReviewRecord\u003e` appended to by\n  `ReviewSession.Grade`. `Card.RetentionRate` (0.0–1.0) computes the fraction of\n  non-Again reviews across all recorded sessions, enabling per-card difficulty tracking.\n- `Deck.GetDueForecast(DateOnly from, int days)` projects how many cards will become\n  due on each of the next 1–365 calendar days, enabling a load-preview UI widget.\n- `Deck.GetDueCards` now uses a secondary sort by `DueDate` ascending within the same\n  Leitner box, so the most overdue card in each box is always reviewed first.\n- The ViewModel layer (`ReviewViewModel`) uses CommunityToolkit.Mvvm source generators.\n  `Program.cs` subscribes to `INotifyPropertyChanged` and routes touch events to\n  `IRelayCommand`, so there's no business logic in the UI class.\n- Two real TizenFX API integrations, not stubs: `Tizen.System.Feedback` for haptic\n  patterns and `Tizen.Applications.Preference` for persistent schedule storage.\n- Both TizenFX calls are wrapped in `try/catch` so the Core still runs in non-Tizen\n  environments (CI, unit tests, emulators without vibration support).\n\n---\n\n## Build\n\n### Prerequisites\n\n| Requirement | Version | Notes |\n|-------------|---------|-------|\n| .NET SDK | 10.0.300+ | `dotnet --version` |\n| `maui-tizen` workload | 10.0.x | Required only for the Tizen head build |\n| Tizen Studio (emulator) | 5.6+ | Required only to run the `.tpk` |\n\n### Step 1 — Install the Tizen workload (once)\n\nThe NUI head targets `net10.0-tizen`, which the `maui-tizen` workload provides (it carries\nthe Samsung Tizen SDK packs and the `.tpk` packaging targets).\n\n```bash\ndotnet workload install maui-tizen\n```\n\nVerify:\n\n```bash\ndotnet workload list\n# maui-tizen   10.0.20/10.0.100   SDK 10.0.300\n```\n\n### Step 2 — Build the full solution\n\n```bash\ndotnet build PasiCard.sln -c Release\n```\n\nExpected output (verified locally on 2026-06-23, .NET 10.0.300 + `maui-tizen` 10.0.20):\n\n```\nPasiCard.Tizen is signed with Default Certificates!\nPasiCard.Tizen -\u003e .../bin/Release/net10.0-tizen/org.pasicard.app-1.0.0.tpk\nBuild succeeded.\n    0 Warning(s)\n    0 Error(s)\n# Produces a signed ~116 KB .tpk: src/PasiCard.Tizen/bin/Release/net10.0-tizen/org.pasicard.app-1.0.0.tpk\n```\n\nTo build just the Tizen head and its `.tpk`:\n\n```bash\ndotnet build src/PasiCard.Tizen/PasiCard.Tizen.csproj -c Release\n```\n\nThe resulting `.tpk` is a signed Tizen package (a zip) containing `tizen-manifest.xml`,\n`author-signature.xml` / `signature1.xml`, and the compiled `PasiCard.Tizen.dll` +\n`PasiCard.Core.dll` + `CommunityToolkit.Mvvm.dll`. CI builds and uploads this `.tpk` as an\nartifact (`org.pasicard.app-tpk`) on a runner with the `maui-tizen` workload installed.\n\n### Building Core only (no Tizen workload needed)\n\n```bash\ndotnet build src/PasiCard.Core/PasiCard.Core.csproj -c Release\n```\n\n---\n\n## Tests\n\n```bash\ndotnet test -c Release\n```\n\nExpected:\n\n```\nPassed!  - Failed: 0, Passed: 171, Skipped: 0, Total: 171\n```\n\n(152 test methods; some `[Theory]` methods expand into multiple `[InlineData]` cases,\nso the runner reports 171 total cases.)\n\nTests run on `net10.0` and require no Tizen workload. They cover:\n\n- All four grade paths (Again / Hard / Good / Easy) for every box 1–6.\n- Box clamping at min (1) and max (6).\n- Again re-queue behaviour within a session.\n- Session completion detection.\n- Clock injection for deterministic due-date assertions.\n- `LeitnerConfig` validation, custom intervals, and `SrsScheduler` with custom config.\n- `Card.IsSuspended` filtering in `GetDueCards` / `GetDueCount` / `ReviewSession`.\n- `SrsScheduler.ResetCard` — box reset, date reset, flag clear, clock injection.\n- `DeckSerializer` JSON roundtrip (name, all card fields, special chars, empty deck).\n- `SessionStats.AccuracyPercent` at 0%, 50%, 66.7%, 100% and edge cases.\n- `Card.ReviewHistory` populated by `ReviewSession.Grade`; `RetentionRate` across all grades.\n- `CardReviewRecord` date and grade tracking with multi-card session independence.\n- `Deck.GetDueForecast` — 1-day, 7-day, 90-day windows; suspended/overdue card exclusion; guard rails.\n- Overdue-priority secondary sort in `GetDueCards` (most overdue card within a box first).\n- `DeckSerializer.TryImport` — clean decks, `LeitnerBox` clamping (0, negative, 99), blank text,\n  empty-Guid, multi-card accumulated warnings, structural errors still throwing.\n- `ReviewController` grade+persist flow — the schedule result is persisted against the id of\n  the card that was just graded (not the next card in the queue), across single-card \"Again\"\n  re-queues and multi-card sessions; null-argument guards and completed-session throwing.\n\n---\n\n## Running the `.tpk` (emulator / device)\n\nThe `.tpk` is built and signed locally and in CI. **Running** it is an environment blocker,\nnot a code one: it needs Tizen Studio + the Emulator (with hardware acceleration, often admin\nrights) or a real Samsung device with an author/distributor certificate from the Tizen\nCertificate Manager. None of that is installed on the build machine, so the run step is\ndocumented honestly here rather than faked.\n\nTo run once a Tizen environment is available:\n\n1. Install **Tizen Studio** (https://developer.samsung.com/tizen/run.html) with the\n   **Tizen SDK Tools**, a platform profile (Wearable/TV), the **Emulator**, and\n   **Certificate Manager** packages.\n2. Enable hardware acceleration (Intel HAXM or Windows Hypervisor Platform); may need admin.\n3. In **Certificate Manager**, create an author + distributor certificate. (Emulator runs\n   accept the Default Certificate the `.tpk` is already signed with; device deploy needs a\n   Samsung-account-backed certificate.)\n4. Start an image via **Emulator Manager** (e.g. wearable-circle), then install + launch:\n   ```bash\n   sdb install src/PasiCard.Tizen/bin/Release/net10.0-tizen/org.pasicard.app-1.0.0.tpk\n   sdb shell app_launcher -s org.pasicard.app\n   ```\n   Or, from a configured environment, `dotnet build src/PasiCard.Tizen/PasiCard.Tizen.csproj -c Debug -t:Run`.\n\nNotes: haptic feedback (`Tizen.System.Feedback`) may produce a sound effect instead of\nvibration on the emulator; on real Samsung hardware both fire. Schedule persistence\n(`Tizen.Applications.Preference`) survives app restarts on device or emulator.\n\n---\n\n## Screenshots\n\nA live UI capture means deploying the `.tpk` to a Tizen Emulator via Tizen Studio (a\n~2 GB GUI toolkit). For now the signed build artifact (`org.pasicard.app-1.0.0.tpk`) and the\n171 passing Core tests verify the behaviour; the layout below is the reference for the review screen.\n\n### UI layout (review screen)\n\n```\n┌─────────────────────────────────────────┐\n│  PasiCard                    1 / 10     │\n│                                         │\n│              pasztet                    │\n│                                         │\n│         ┌─────────────────┐             │\n│         │   Show Answer   │             │\n│         └─────────────────┘             │\n│                                         │\n│  ─ ─ ─ ─  (after reveal)  ─ ─ ─ ─     │\n│                                         │\n│         pâté / meat paste               │\n│                                         │\n│  ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐   │\n│  │Again │ │ Hard │ │ Good │ │ Easy │   │\n│  └──────┘ └──────┘ └──────┘ └──────┘   │\n│                                         │\n│  Box 2 — due 25 Jun                     │\n└─────────────────────────────────────────┘\n```\n\n---\n\n## TizenFX APIs Used\n\n| API | Integration point | Behaviour |\n|-----|------------------|-----------|\n| `Tizen.System.Feedback` | `ReviewViewModel.TriggerHapticFeedback()` | Vibration pattern per grade: `Error` for Again, `SuccessTap` for Easy, `Tap` for Hard/Good. |\n| `Tizen.Applications.Preference` | `ReviewViewModel.PersistCardSchedule()` | Stores `card_{id}_box` and `card_{id}_due` keys after every grade. |\n\nBoth are wrapped in `try/catch` and silently ignored outside the Tizen runtime.\n\n---\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farcsymer%2Fpasicard","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Farcsymer%2Fpasicard","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farcsymer%2Fpasicard/lists"}