feat: in-game theme store — server catalog + verified downloads + install UI
Test / test (pull_request) Successful in 10m15s
Test / test (pull_request) Successful in 10m15s
Phase 1+2 of the theme-store roadmap: a free catalog served by
solitaire_server and an in-app browse/install flow, making custom
themes installable on Android for the first time (the manual
drop-a-zip flow can't reach the app-private themes dir there).
- solitaire_sync: ThemeCatalogEntry/ThemeCatalogResponse wire types
(additive module; SyncPayload and SyncProvider untouched)
- solitaire_server: THEME_STORE_DIR scan at startup (meta-only
theme.ron parse, sha256, 20 MiB cap, best-effort per archive);
public GET /api/themes, /api/themes/{id}/download, /{id}/preview;
compose volume + README_SERVER docs
- solitaire_data: ThemeStoreClient — catalog fetch + download with
mandatory size/sha256 verification before bytes are released
- solitaire_engine: ThemeStorePlugin — 'Browse theme store' button in
Settings → Cosmetic, modal catalog (leaderboard-style rebuild),
download on AsyncComputeTaskPool, atomic .tmp+rename write into
user_theme_dir, then the existing hardened import_theme pipeline and
an in-place registry refresh
New deps: sha2 (workspace, server+data); ron/zip reused in server;
serde_json added to solitaire_sync dev-deps for DTO round-trip tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -407,6 +407,10 @@ pub(super) fn handle_settings_buttons(
|
||||
| SettingsButton::DeleteAccount => {
|
||||
// Handled by `handle_sync_buttons`.
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
SettingsButton::OpenThemeStore => {
|
||||
// Handled by `handle_sync_buttons`.
|
||||
}
|
||||
SettingsButton::Done => {
|
||||
screen.0 = false;
|
||||
}
|
||||
@@ -423,6 +427,9 @@ pub(super) fn handle_sync_buttons(
|
||||
mut configure_sync: MessageWriter<SyncConfigureRequestEvent>,
|
||||
mut logout_sync: MessageWriter<SyncLogoutRequestEvent>,
|
||||
mut delete_account: MessageWriter<DeleteAccountRequestEvent>,
|
||||
#[cfg(not(target_arch = "wasm32"))] mut open_theme_store: MessageWriter<
|
||||
crate::events::ThemeStoreOpenRequestEvent,
|
||||
>,
|
||||
mut screen: ResMut<SettingsScreen>,
|
||||
) {
|
||||
for (interaction, button) in &interaction_query {
|
||||
@@ -439,6 +446,13 @@ pub(super) fn handle_sync_buttons(
|
||||
screen.0 = false;
|
||||
configure_sync.write(SyncConfigureRequestEvent);
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
SettingsButton::OpenThemeStore => {
|
||||
// Same shape as ConnectSync: close settings first so the
|
||||
// store modal's own-scrim guard doesn't block.
|
||||
screen.0 = false;
|
||||
open_theme_store.write(crate::events::ThemeStoreOpenRequestEvent);
|
||||
}
|
||||
SettingsButton::DisconnectSync => {
|
||||
logout_sync.write(SyncLogoutRequestEvent);
|
||||
}
|
||||
|
||||
@@ -250,6 +250,9 @@ enum SettingsButton {
|
||||
/// Scan `user_theme_dir()` for new `.zip` files and import each one.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
ScanThemes,
|
||||
/// Open the in-game theme-store modal (closes Settings first).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
OpenThemeStore,
|
||||
SyncNow,
|
||||
/// Open the sync-server Connect modal (shown when backend = Local).
|
||||
ConnectSync,
|
||||
@@ -313,6 +316,8 @@ impl SettingsButton {
|
||||
SettingsButton::SelectTheme(_) => 85,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
SettingsButton::ScanThemes => 86,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
SettingsButton::OpenThemeStore => 87,
|
||||
// Sync section
|
||||
SettingsButton::SyncNow => 90,
|
||||
SettingsButton::ConnectSync => 91,
|
||||
@@ -371,6 +376,7 @@ impl Plugin for SettingsPlugin {
|
||||
.add_message::<SyncLogoutRequestEvent>()
|
||||
.add_message::<DeleteAccountRequestEvent>()
|
||||
.add_message::<ToggleSettingsRequestEvent>()
|
||||
.add_message::<crate::events::ThemeStoreOpenRequestEvent>()
|
||||
.add_message::<InfoToastEvent>()
|
||||
.add_message::<MouseWheel>()
|
||||
.add_message::<TouchInput>()
|
||||
|
||||
@@ -239,6 +239,8 @@ pub(super) fn spawn_settings_panel(
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
import_themes_row(body, font_res);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
theme_store_row(body, font_res);
|
||||
|
||||
// --- Privacy (only shown when a Matomo URL is configured) ---
|
||||
if settings.matomo_url.is_some() {
|
||||
@@ -1193,6 +1195,31 @@ pub(super) fn import_themes_row(
|
||||
});
|
||||
}
|
||||
|
||||
/// "Theme store" row: one pill button that closes Settings and opens
|
||||
/// the in-game theme-store modal (`theme_store_plugin`). Sits directly
|
||||
/// under the Import row so both install paths live together.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(super) fn theme_store_row(parent: &mut ChildSpawnerCommands, font_res: Option<&FontResource>) {
|
||||
parent
|
||||
.spawn((
|
||||
FocusRow,
|
||||
Node {
|
||||
flex_direction: FlexDirection::Row,
|
||||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
},
|
||||
))
|
||||
.with_children(|row| {
|
||||
pill_button(
|
||||
row,
|
||||
SettingsButton::OpenThemeStore,
|
||||
"Browse theme store",
|
||||
"Download themes from your sync server without leaving the game.",
|
||||
font_res,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn icon_button(
|
||||
parent: &mut ChildSpawnerCommands,
|
||||
label: &str,
|
||||
|
||||
Reference in New Issue
Block a user