Replaces the 5 per-run tool-install steps (~2m 30s) with a pre-built
container image (git.aleshym.co/funman300/android-builder) that ships
Ubuntu 22.04 + Java 17 + Android SDK/NDK + Rust stable + aarch64 target
+ cargo-ndk + sccache. android-release.yml now runs inside the container
and adds two cache steps instead: Cargo registry and sccache directory.
sccache (RUSTC_WRAPPER) caches at the translation-unit level so partial
hits survive Cargo.lock changes — far more resilient than caching the
full target/ directory.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AndroidManifest.xml had hardcoded versionCode=1 / versionName=1.0, so
every shipped APK looked identical to Android and Obtainium could never
confirm the installed version matched the latest release tag — causing
a persistent false-update notification loop.
VERSION_NAME is now passed into the build script from the CI tag
(e.g. "v0.28.0" → versionCode=2800, versionName="0.28.0") and
forwarded to aapt2 link via --version-code / --version-name, overriding
the manifest without touching the file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaced the curl|bash install_kustomize.sh approach (which makes an
unauthenticated GitHub API call to resolve the latest version) with a
direct pinned tarball download. Eliminates the tar glob failure that
broke run #226.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fetches /api/me with the stored fs_token and renders a 32px circular
avatar in hud-right. Shows the profile photo when set, or the first
letter of the username as initials otherwise. Hidden when not signed in.
Clicking the avatar navigates to /account.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Places a floating "↩ Undo" button at the bottom-right of the green felt
surface so it is visible without looking in the header. Both the board
button and the header button share the same handler; both track
undo_stack_len and disable when nothing can be undone.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Splits the old single "⏮ Restart" button into two: "⏮ Restart" (resets
to step 0 with card fade-in from dealt positions) and "◀ Back" (steps
back one move at a time via fast-forward replay). Both are disabled at
step 0 and enabled after any forward step.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The "⏮ Restart" button now steps back one move at a time instead of
resetting to the beginning. Re-creates the ReplayPlayer and fast-forwards
to (step_idx - 1) without rendering intermediate frames; the CSS transform
transition then animates each card back to its previous position.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The rank+suit text overlay was transparent, letting the card art's
own small corner text show through underneath — giving the appearance
of two sets of labels on each face-up card.
Add AndroidCornerBg, a CARD_FACE_COLOUR sprite child sized at
(2.0 × font_size) × (1.25 × font_size) rendered at z+0.015,
just below the text overlay (z+0.02). This covers the art corner
text so only the large overlay label is visible.
resize_android_corner_labels now also resizes AndroidCornerBg so
both layers stay aligned on orientation change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On Android, face-up cards now render a large rank+suit overlay in
the upper-left corner (FONT_SIZE_FRAC_MOBILE = 0.35 × card_width,
using Anchor::TOP_LEFT) so the rank and suit are legible at phone
scale. The baked-in SVG art corner text is only ~10–15 px physical;
the overlay is ~52 px physical — roughly 3-4× larger.
Accompanying changes:
- H_GAP_DIVISOR on Android raised 8 → 32, widening cards from
112.5 → 124.1 logical px (135 → 149 physical px on Pixel 7 AVD).
- AndroidCornerLabel marker component tracks overlay entities so
resize_android_corner_labels can update font-size + transform
on orientation change without a full card respawn.
- Uses text_colour() for overlay tint so black suits render as
near-white (BLACK_SUIT_COLOUR) on the dark Terminal card face,
matching the existing fallback overlay behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug A: Replace U+21C4 (tofu on FiraMono) with plain ASCII "M" on the
Modes action button.
Bug B: HudAvatar disc was invisible against BG_HUD_BAND (same dark
grey). Switch background to ACCENT_PRIMARY and text to TEXT_PRIMARY so
the disc is clearly visible.
Bug C/D: toggle_hud_on_tap improvements:
- Drain buffered TouchInput events in the early-return path (scrim
present or paused) so the modal-dismiss frame does not replay the
button tap's Started+Ended pair as a spurious toggle.
- Stop clearing start_pos on TouchPhase::Moved — Android fires Moved
even for clean taps (jitter), and the distance check at Ended already
rejects real drags via drag.is_idle(). Clearing it silently swallowed
toggle attempts on physical devices.
- Increase HUD_TAP_SLOP_PX from 15 → 25 for better tap recognition.
Also reduces Android HUD_BAND_HEIGHT from 128 → 80 px now that action
buttons live in the bottom bar rather than the top band.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Right-edge panel shows foundation tops (F: A♠ 7♥ 5♦ K♣) and
stock/waste head (STK:14 WST:7♥) while a replay plays, giving
players a compact game-state readout without scanning the dim tableau.
Architectural changes:
- DespawnWithReplay marker on every sibling root entity so
react_to_state_change uses a single despawn query instead of
one per entity type — future overlay surfaces just add the marker.
- react_to_state_change reduced from 9 args to 5 via the above.
- Two update systems (update_mini_tableau_foundations,
update_mini_tableau_stock_waste) watch GameStateResource.is_changed()
and repaint; split to avoid Bevy B0001 query conflict on &mut Text.
New format helpers: format_rank_short, format_suit_glyph,
format_card_short, format_foundations_row, format_stock_waste_row —
all use FiraMono-covered suit glyphs (U+2660–U+2666, verified Android).
+9 tests (lifecycle + format helper unit coverage).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Obtainium matches overrideSource via runtimeType.toString(), which is the
Dart class name "Codeberg", not the display name "Forgejo (Codeberg)".
The wrong name caused "URL does not match the source" on import.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Obtainium's fromJson calls jsonDecode(json['apkUrls']), so the field must
be a JSON-encoded string ("[]") not a raw array ([]). Passing a raw array
caused the Dart runtime error: List<dynamic> is not a subtype of String.
Also adds allowIdChange and otherAssetUrls fields required by v1.4.3.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The redirect service at apps.obtainium.imranr.dev/redirect parses the
obtainium://app/ payload via JSON.parse(decodeURIComponent(...)), so
base64 caused an "invalid URL" error. Switch to URL-percent-encoded JSON.
Also updates package id to com.ferrousapp.solitaire (renamed package).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Obtainium has no dedicated Gitea source provider. The correct override
is "Forgejo (Codeberg)" which uses the same /api/v1/repos/ API that
Gitea exposes. Previous overrideSource "Gitea" was silently ignored,
falling back to failed auto-detection.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The add/ scheme relies on auto-detection which fails for this self-hosted
Gitea instance. The app/ scheme encodes the full config (including
overrideSource: Gitea) in base64 so no detection is needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The self-hosted Gitea instance's /api/v1/meta endpoint returns 404,
causing Obtainium's auto-detection to fail on custom domains. Rewrite
the install steps to lead with the manual Add App flow (URL + set source
type to Gitea) and demote the one-tap badge to a secondary option.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fires on any v* tag push. Steps:
- Installs Android SDK + NDK 30.0.14904198 (cached by SDK version key)
- Builds release APK for arm64-v8a via scripts/build_android_apk.sh
- Signs with the release keystore stored in RELEASE_KEYSTORE_B64 secret
- Creates (or reuses) the Gitea release for the tag
- Uploads solitaire-quest.apk as a release asset
Obtainium users can track releases by adding:
https://git.aleshym.co/funman300/Rusty_Solitare
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switch docker-build.yml from paths-ignore to an explicit paths allowlist
so the workflow only fires on changes to solitaire_server/, solitaire_sync/,
solitaire_core/, Cargo.toml, Cargo.lock, or the workflow file itself.
Also harden the "commit and push updated kustomization" step:
- exit 0 early when the kustomization has no staged diff (nothing to push)
- retry the pull+push loop up to 3 times with a 5 s delay to handle
concurrent pushes that race the CI commit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All 10 warnings were caused by hotkey/keyboard UI code behind
#[cfg(not(target_os = "android"))] call sites whose definitions lacked
the matching gate. Fixes:
- help_plugin: gate keyboard-chip imports and font_kbd; #[allow(dead_code)]
on ControlRow (keys field is data, not dead)
- hud_plugin/ui_modal: replace cfg shadow pattern with cfg!() expression
so the hotkey parameter is read on every platform
- home_plugin: gate fn hotkey behind not(android)
- onboarding_plugin: gate HotkeyRow, HOTKEYS, spawn_slide_hotkeys and
their exclusive imports behind not(android)
- replay_overlay: gate keybind_footer_hint_text behind not(android)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Delete CLAUDE_PROMPT_PACK/SPEC/WORKFLOW (superseded by CLAUDE.md),
SESSION_HANDOFF files, old android investigation notes, phase-plan docs
under docs/superpowers/, and all docs/ui-mockups/ (HTML+PNG mockups
from the pre-Terminal design pass). Also removes local artifacts:
analytics_impl_prompt.md, review_project.py, delete_runs.sh, ruvector.db
files, and the stray solitaire_wasm/solitaire_server/ build artefact.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Double-clicking or right-clicking a face-up card now auto-places it to
the best valid pile (foundation preferred for single cards, tableau
otherwise). Right-click also suppresses the browser context menu.
Theme button re-render now calls game.state() instead of reusing snap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Standard Klondike allows returning the top card of a foundation pile to a
compatible tableau column. The flag was previously off by default, making the
move impossible for all players.
Changes:
- GameState::new_with_mode: take_from_foundation now initialises to true
- Settings: default changed to true; custom serde default function ensures
older settings.json files without the key also resolve to true
- Tests: rename "blocked_by_default" → "allowed_by_default" (asserts the
move now succeeds); add "blocked_when_disabled" to cover the flag=false path
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add avatar_plugin: AvatarPlugin, AvatarResource, AvatarFetchEvent
- After AvatarFetchEvent fires, spawns an async reqwest download task
- On completion, decodes image bytes via image::load_from_memory →
Image::from_dynamic and inserts into Assets<Image>
- Expand auth task to also call fetch_me_with_token immediately after
login/register so avatar_url is available without a second round-trip
- poll_auth_task fires AvatarFetchEvent when avatar_url is Some, building
the full URL from base_url + relative avatar path
- Profile modal shows 48px circular avatar ImageNode when AvatarResource
is populated, or an initials disc (first letter of username) as fallback
- Add image = "0.25" and reqwest to solitaire_engine deps
- Add fetch_me_with_token helper to SolitaireServerClient for use when
the access token hasn't been persisted to keychain yet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add migration 005: nullable avatar_url column on users table
- Add GET /api/me: returns id, username, avatar_url from DB (fixes UUID-on-profile bug)
- Add PUT /api/me/avatar: accepts raw image bytes (≤1 MB, jpeg/png/webp/gif),
writes to avatars/ dir, updates avatar_url in DB
- Serve /avatars via ServeDir so uploaded images are publicly accessible
- Update account.html: fetch username from /api/me instead of parsing JWT;
add circular avatar display with initials fallback and click-to-upload
- Add SolitaireServerClient::fetch_me() for desktop/Android profile display
- Add avatar_url field to SyncBackend::SolitaireServer settings (serde default None)
- Update sqlx offline query cache for new avatar_url queries
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reorganise card PNGs into assets/cards/faces/{classic,dark}/ and
assets/cards/backs/{classic,dark}/
- Rasterise dark SVG theme alongside existing classic set
- Add "Dark / Classic" toggle button in the game HUD; persists to
localStorage as fs_theme (defaults to classic)
- Preload both themes on page load so switching is instant
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rasterized all 52 classic SVGs via rsvg-convert at 256×384. The web
game was showing dark-background cards; it now shows the traditional
white card face style.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Building locally or via a different pipeline; the self-hosted runner's
resource constraints made the 3-ABI release build unreliable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
workflow_dispatch caused the entire android-release workflow to be silently
dropped; tag-triggered releases (v0.25.6) worked fine before this trigger
was added. Removing it restores tag-based release triggering.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>