feat(web): add solitaire_web Bevy WASM build targeting play.html canvas
Build and Deploy / build-and-push (push) Failing after 58s
Build and Deploy / build-and-push (push) Failing after 58s
Adds a new `solitaire_web` crate that compiles the full `solitaire_engine` to `wasm32-unknown-unknown` and renders to a `<canvas id="bevy-canvas">` element in `play.html` — the same ECS code path as desktop and Android. Changes to enable the WASM target: - .cargo/config.toml: add wasm32-unknown-unknown rustflags for getrandom - Workspace Cargo.toml: add solitaire_web member - solitaire_data/Cargo.toml: gate tokio/reqwest/dirs/keyring to non-wasm - solitaire_data/src: add wasm32 branch to data_dir() (returns None); cfg-gate sync_client network types, auth_tokens, matomo_client - solitaire_engine/Cargo.toml: gate tokio/reqwest/kira/arboard/dirs/zip to non-wasm (mio/cpal/arboard don't compile for wasm32-unknown-unknown) - solitaire_engine/src/lib.rs: cfg-gate module declarations and re-exports for analytics, audio, sync, sync_setup, avatar, leaderboard plugins - solitaire_engine/src/core_game_plugin.rs: cfg-gate plugin registrations that require TokioRuntime (audio, sync, analytics, leaderboard, avatar) - solitaire_engine/src/resources.rs: cfg-gate TokioRuntimeResource - solitaire_engine/src/game_plugin.rs: cfg-gate std::fs::remove_file (x10) - solitaire_engine/src/theme/mod.rs: cfg-gate importer module (uses dirs+zip) - solitaire_engine/src/settings_plugin.rs: cfg-gate theme ZIP import UI - solitaire_engine/src/assets/sources.rs: cfg-gate FileAssetReader/user_theme_dir - solitaire_engine/src/auto_complete_plugin.rs: cfg-gate audio system - solitaire_engine/src/daily_challenge_plugin.rs: cfg-gate server fetch - solitaire_engine/src/hud_plugin.rs: cfg-gate AvatarResource import - solitaire_engine/src/profile_plugin.rs: cfg-gate AvatarResource import - solitaire_server/web/play.html: minimal HTML canvas shell - solitaire_web/: new crate (Cargo.toml + src/lib.rs) - build_wasm.sh: add Bevy WASM build step (cargo + wasm-bindgen + wasm-opt) All tests pass; clippy --workspace -- -D warnings clean; native build (solitaire_engine, solitaire_app) unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -50,9 +50,11 @@
|
||||
use bevy::asset::AssetApp;
|
||||
use bevy::asset::io::AssetSourceBuilder;
|
||||
use bevy::asset::io::embedded::EmbeddedAssetRegistry;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use bevy::asset::io::file::FileAssetReader;
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::assets::user_dir::user_theme_dir;
|
||||
|
||||
/// `AssetSourceId` of the user-themes asset source. Use it as
|
||||
@@ -235,11 +237,16 @@ const CLASSIC_THEME_SVGS: &[(&str, &[u8])] = &[
|
||||
/// Returns the `&mut App` so the call can be chained from the binary
|
||||
/// entry point.
|
||||
pub fn register_theme_asset_sources(app: &mut App) -> &mut App {
|
||||
let root = user_theme_dir();
|
||||
app.register_asset_source(
|
||||
USER_THEMES,
|
||||
AssetSourceBuilder::new(move || Box::new(FileAssetReader::new(root.clone()))),
|
||||
);
|
||||
// User themes are stored on the filesystem; wasm32 has no filesystem and
|
||||
// `FileAssetReader` is not available on that target.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let root = user_theme_dir();
|
||||
app.register_asset_source(
|
||||
USER_THEMES,
|
||||
AssetSourceBuilder::new(move || Box::new(FileAssetReader::new(root.clone()))),
|
||||
);
|
||||
}
|
||||
app
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
use bevy::prelude::*;
|
||||
use bevy::window::RequestRedraw;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::audio_plugin::{AudioState, SoundLibrary};
|
||||
use crate::events::{MoveRequestEvent, StateChangedEvent};
|
||||
use crate::game_plugin::GameMutation;
|
||||
@@ -97,6 +98,7 @@ fn detect_auto_complete(
|
||||
/// exactly once on the `false → true` edge. The win fanfare is played at half
|
||||
/// volume (`AUTO_COMPLETE_CHIME_VOLUME`) so it is clearly recognisable but does
|
||||
/// not overwhelm the card-place sounds that follow immediately.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn on_auto_complete_start(
|
||||
state: Res<AutoCompleteState>,
|
||||
mut was_active: Local<bool>,
|
||||
@@ -117,6 +119,12 @@ fn on_auto_complete_start(
|
||||
audio.play_sfx_at_volume(&lib.fanfare, AUTO_COMPLETE_CHIME_VOLUME);
|
||||
}
|
||||
|
||||
// No audio on wasm — stub keeps the system registration unconditional.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn on_auto_complete_start(state: Res<AutoCompleteState>, mut was_active: Local<bool>) {
|
||||
*was_active = state.active;
|
||||
}
|
||||
|
||||
/// Fires one `MoveRequestEvent` per `STEP_INTERVAL` while auto-complete is active.
|
||||
fn drive_auto_complete(
|
||||
mut state: ResMut<AutoCompleteState>,
|
||||
|
||||
@@ -13,16 +13,18 @@ use crate::platform::{
|
||||
default_storage_backend,
|
||||
};
|
||||
use crate::{
|
||||
AchievementPlugin, AnalyticsPlugin, AnimationPlugin, AssetSourcesPlugin, AudioPlugin,
|
||||
AutoCompletePlugin, AvatarPlugin, CardAnimationPlugin, CardPlugin, ChallengePlugin,
|
||||
CursorPlugin, DailyChallengePlugin, DiagnosticsHudPlugin, DifficultyPlugin, FeedbackAnimPlugin,
|
||||
FontPlugin, GamePlugin, HelpPlugin, HomePlugin, HudPlugin, InputPlugin, LeaderboardPlugin,
|
||||
OnboardingPlugin, PausePlugin, PlayBySeedPlugin, ProfilePlugin, ProgressPlugin,
|
||||
RadialMenuPlugin, ReplayOverlayPlugin, ReplayPlaybackPlugin, SafeAreaInsetsPlugin,
|
||||
SelectionPlugin, SettingsPlugin, SplashPlugin, StatsPlugin, SyncPlugin, SyncProvider,
|
||||
SyncSetupPlugin, TablePlugin, ThemePlugin, ThemeRegistryPlugin, TimeAttackPlugin,
|
||||
TouchSelectionPlugin, UiFocusPlugin, UiModalPlugin, UiTooltipPlugin, WeeklyGoalsPlugin,
|
||||
WinSummaryPlugin,
|
||||
AchievementPlugin, AnimationPlugin, AssetSourcesPlugin, AutoCompletePlugin,
|
||||
CardAnimationPlugin, CardPlugin, ChallengePlugin, CursorPlugin, DailyChallengePlugin,
|
||||
DiagnosticsHudPlugin, DifficultyPlugin, FeedbackAnimPlugin, FontPlugin, GamePlugin, HelpPlugin,
|
||||
HomePlugin, HudPlugin, InputPlugin, OnboardingPlugin, PausePlugin, PlayBySeedPlugin,
|
||||
ProfilePlugin, ProgressPlugin, RadialMenuPlugin, ReplayOverlayPlugin, ReplayPlaybackPlugin,
|
||||
SafeAreaInsetsPlugin, SelectionPlugin, SettingsPlugin, SplashPlugin, StatsPlugin, SyncProvider,
|
||||
TablePlugin, ThemePlugin, ThemeRegistryPlugin, TimeAttackPlugin, TouchSelectionPlugin,
|
||||
UiFocusPlugin, UiModalPlugin, UiTooltipPlugin, WeeklyGoalsPlugin, WinSummaryPlugin,
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::{
|
||||
AnalyticsPlugin, AudioPlugin, AvatarPlugin, LeaderboardPlugin, SyncPlugin, SyncSetupPlugin,
|
||||
};
|
||||
|
||||
/// Groups all Ferrous Solitaire gameplay plugins.
|
||||
@@ -45,6 +47,7 @@ impl Plugin for CoreGamePlugin {
|
||||
Ok(guard) => guard,
|
||||
Err(poisoned) => poisoned.into_inner(),
|
||||
};
|
||||
#[cfg_attr(target_arch = "wasm32", allow(unused_variables))]
|
||||
let sync_provider = sync_provider
|
||||
.take()
|
||||
.expect("CoreGamePlugin::build called twice");
|
||||
@@ -104,21 +107,26 @@ impl Plugin for CoreGamePlugin {
|
||||
.add_plugins(HudPlugin)
|
||||
.add_plugins(HelpPlugin)
|
||||
.add_plugins(HomePlugin::default())
|
||||
.add_plugins(AvatarPlugin)
|
||||
.add_plugins(ProfilePlugin)
|
||||
.add_plugins(PausePlugin)
|
||||
.add_plugins(SettingsPlugin::default())
|
||||
.add_plugins(AudioPlugin)
|
||||
.add_plugins(OnboardingPlugin)
|
||||
.add_plugins(SyncPlugin::new(sync_provider))
|
||||
.add_plugins(SyncSetupPlugin)
|
||||
.add_plugins(AnalyticsPlugin)
|
||||
.add_plugins(LeaderboardPlugin)
|
||||
.add_plugins(WinSummaryPlugin)
|
||||
.add_plugins(UiModalPlugin)
|
||||
.add_plugins(UiFocusPlugin)
|
||||
.add_plugins(UiTooltipPlugin)
|
||||
.add_plugins(SplashPlugin)
|
||||
.add_plugins(DiagnosticsHudPlugin);
|
||||
|
||||
// Plugins that use kira/cpal audio or multi-threaded Tokio are not
|
||||
// compatible with the single-threaded wasm32 runtime. Gate them out
|
||||
// so the browser build boots silently and without a sync backend.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
app.add_plugins(AvatarPlugin)
|
||||
.add_plugins(AudioPlugin)
|
||||
.add_plugins(SyncPlugin::new(sync_provider))
|
||||
.add_plugins(SyncSetupPlugin)
|
||||
.add_plugins(AnalyticsPlugin)
|
||||
.add_plugins(LeaderboardPlugin);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ use crate::events::{
|
||||
use crate::game_plugin::GameMutation;
|
||||
use crate::progress_plugin::{ProgressResource, ProgressStoragePath, ProgressUpdate};
|
||||
use crate::resources::GameStateResource;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::sync_plugin::SyncProviderResource;
|
||||
|
||||
/// Bonus XP awarded for completing today's daily challenge.
|
||||
@@ -116,17 +117,21 @@ impl Plugin for DailyChallengePlugin {
|
||||
.add_message::<StartDailyChallengeRequestEvent>()
|
||||
.add_message::<WarningToastEvent>()
|
||||
.add_message::<XpAwardedEvent>()
|
||||
.add_systems(Startup, fetch_server_challenge)
|
||||
.add_systems(Update, poll_server_challenge)
|
||||
// record/award after the base ProgressUpdate so we don't fight
|
||||
// ProgressPlugin's add_xp on the same frame.
|
||||
.add_systems(Update, handle_daily_completion.after(ProgressUpdate))
|
||||
.add_systems(Update, handle_start_daily_request.before(GameMutation))
|
||||
.add_systems(Update, check_daily_expiry_warning)
|
||||
.add_systems(Update, check_date_rollover);
|
||||
|
||||
// Server-challenge fetch uses SyncProviderResource (reqwest), not available on wasm.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
app.add_systems(Startup, fetch_server_challenge)
|
||||
.add_systems(Update, poll_server_challenge);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Startup system: spawns an async task to fetch the server's daily challenge.
|
||||
///
|
||||
/// Only runs when `SyncProviderResource` is present (i.e. `SyncPlugin` is
|
||||
@@ -142,6 +147,7 @@ fn fetch_server_challenge(
|
||||
task_res.0 = Some(task);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Update system: polls the server-challenge fetch task.
|
||||
///
|
||||
/// On success, replaces the locally-computed seed in `DailyChallengeResource`
|
||||
|
||||
@@ -1512,6 +1512,7 @@ mod tests {
|
||||
use solitaire_data::load_game_state_from;
|
||||
|
||||
let path = tmp_gs_path("exit_save");
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
let mut app = test_app(7);
|
||||
@@ -1527,6 +1528,7 @@ mod tests {
|
||||
let loaded = load_game_state_from(&path).expect("file should exist after exit");
|
||||
assert_eq!(loaded.seed, 7654);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
@@ -1571,6 +1573,7 @@ mod tests {
|
||||
use solitaire_data::load_game_state_from;
|
||||
|
||||
let path = tmp_gs_path("auto_save_30s");
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
let mut app = test_app(42);
|
||||
@@ -1601,6 +1604,7 @@ mod tests {
|
||||
let loaded = load_game_state_from(&path).expect("file must be loadable");
|
||||
assert_eq!(loaded.seed, 42);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
@@ -1608,6 +1612,7 @@ mod tests {
|
||||
#[test]
|
||||
fn auto_save_skips_when_no_moves() {
|
||||
let path = tmp_gs_path("auto_save_skip");
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
let mut app = test_app(99);
|
||||
@@ -2165,6 +2170,7 @@ mod tests {
|
||||
use solitaire_data::load_replay_history_from;
|
||||
|
||||
let path = std::env::temp_dir().join("engine_test_replay_freeze.json");
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
let mut app = test_app(7654);
|
||||
@@ -2223,6 +2229,7 @@ mod tests {
|
||||
other => panic!("second entry must be a Move, got {other:?}"),
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
@@ -2234,6 +2241,7 @@ mod tests {
|
||||
use solitaire_data::load_replay_history_from;
|
||||
|
||||
let path = std::env::temp_dir().join("engine_test_replay_history_append.json");
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
let mut app = test_app(11);
|
||||
@@ -2270,6 +2278,7 @@ mod tests {
|
||||
assert_eq!(history.replays[0].final_score, 200);
|
||||
assert_eq!(history.replays[1].final_score, 100);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
@@ -2281,6 +2290,7 @@ mod tests {
|
||||
#[test]
|
||||
fn replay_with_empty_recording_skips_save() {
|
||||
let path = std::env::temp_dir().join("engine_test_replay_empty_skip.json");
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
let mut app = test_app(1);
|
||||
|
||||
@@ -13,7 +13,14 @@ use solitaire_core::card::Suit;
|
||||
use solitaire_core::game_state::{DrawMode, GameMode};
|
||||
|
||||
use crate::auto_complete_plugin::AutoCompleteState;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::avatar_plugin::AvatarResource;
|
||||
// On wasm32 AvatarPlugin is gated out; define a placeholder type so the
|
||||
// Option<Res<AvatarResource>> parameters below compile without changes.
|
||||
// The resource is never inserted on wasm, so every call resolves to None.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(bevy::prelude::Resource)]
|
||||
struct AvatarResource(Option<bevy::prelude::Handle<bevy::prelude::Image>>);
|
||||
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
|
||||
use crate::daily_challenge_plugin::DailyChallengeResource;
|
||||
use crate::events::{
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
//! Bevy integration layer for Ferrous Solitaire.
|
||||
|
||||
pub mod achievement_plugin;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod analytics_plugin;
|
||||
#[cfg(target_os = "android")]
|
||||
pub mod android_clipboard;
|
||||
pub mod animation_plugin;
|
||||
pub mod assets;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod audio_plugin;
|
||||
pub mod auto_complete_plugin;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod avatar_plugin;
|
||||
pub mod card_animation;
|
||||
pub mod card_plugin;
|
||||
@@ -26,6 +29,7 @@ pub mod home_plugin;
|
||||
pub mod hud_plugin;
|
||||
pub mod input_plugin;
|
||||
pub mod layout;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod leaderboard_plugin;
|
||||
pub mod onboarding_plugin;
|
||||
pub mod pause_plugin;
|
||||
@@ -43,7 +47,9 @@ pub mod selection_plugin;
|
||||
pub mod settings_plugin;
|
||||
pub mod splash_plugin;
|
||||
pub mod stats_plugin;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod sync_plugin;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod sync_setup_plugin;
|
||||
pub mod table_plugin;
|
||||
pub mod theme;
|
||||
@@ -57,14 +63,17 @@ pub mod weekly_goals_plugin;
|
||||
pub mod win_summary_plugin;
|
||||
|
||||
pub use achievement_plugin::{AchievementPlugin, AchievementsResource, AchievementsScreen};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use analytics_plugin::{AnalyticsPlugin, AnalyticsResource};
|
||||
pub use animation_plugin::{ActiveToast, AnimationPlugin, CardAnim, ToastEntity, ToastQueue};
|
||||
pub use assets::{
|
||||
AssetSourcesPlugin, DARK_THEME_MANIFEST_URL, USER_THEMES, bundled_theme_url,
|
||||
populate_embedded_dark_theme, register_theme_asset_sources,
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use audio_plugin::{AudioPlugin, AudioState, SoundLibrary};
|
||||
pub use auto_complete_plugin::AutoCompletePlugin;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use avatar_plugin::{AvatarFetchEvent, AvatarPlugin, AvatarResource};
|
||||
pub use card_animation::{
|
||||
AnimationChain, AnimationTuning, BufferedInput, CardAnimation, CardAnimationPlugin,
|
||||
@@ -117,6 +126,7 @@ pub use hud_plugin::{
|
||||
};
|
||||
pub use input_plugin::InputPlugin;
|
||||
pub use layout::{Layout, LayoutResource, compute_layout};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use leaderboard_plugin::{LeaderboardPlugin, LeaderboardResource, LeaderboardScreen};
|
||||
pub use onboarding_plugin::{OnboardingPlugin, OnboardingScreen};
|
||||
pub use pause_plugin::{ForfeitConfirmScreen, PausePlugin, PauseScreen, PausedResource};
|
||||
@@ -155,7 +165,9 @@ pub use stats_plugin::{
|
||||
ReplaySelectorCaption, SelectedReplayIndex, StatsPlugin, StatsResource, StatsScreen,
|
||||
StatsUpdate, WatchReplayButton, format_replay_caption,
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use sync_plugin::{SyncPlugin, SyncProviderResource};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use sync_setup_plugin::SyncSetupPlugin;
|
||||
pub use table_plugin::{
|
||||
BackgroundImageSet, HintPileHighlight, PileMarker, TableBackground, TablePlugin,
|
||||
|
||||
@@ -12,7 +12,11 @@ use solitaire_core::achievement::{ALL_ACHIEVEMENTS, achievement_by_id};
|
||||
use solitaire_data::SyncBackend;
|
||||
|
||||
use crate::achievement_plugin::AchievementsResource;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::avatar_plugin::AvatarResource;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(bevy::prelude::Resource)]
|
||||
struct AvatarResource(Option<bevy::prelude::Handle<bevy::prelude::Image>>);
|
||||
use crate::events::ToggleProfileRequestEvent;
|
||||
use crate::font_plugin::FontResource;
|
||||
use crate::progress_plugin::ProgressResource;
|
||||
|
||||
@@ -128,9 +128,16 @@ pub struct GameInputConsumedResource(pub bool);
|
||||
/// multi-threaded runtime is built once at startup and its `Arc` cloned cheaply
|
||||
/// into every network task — safe for concurrent `block_on` calls from multiple
|
||||
/// worker threads.
|
||||
///
|
||||
/// Gated to non-wasm because `tokio::runtime::Builder::new_multi_thread()` uses
|
||||
/// `mio` for OS-level I/O polling which does not compile for wasm32. The
|
||||
/// plugins that depend on this resource (AudioPlugin, SyncPlugin,
|
||||
/// AnalyticsPlugin) are also gated out on wasm32 in `CoreGamePlugin`.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Resource, Clone)]
|
||||
pub struct TokioRuntimeResource(pub Arc<tokio::runtime::Runtime>);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl TokioRuntimeResource {
|
||||
/// Attempts to build the shared multi-threaded Tokio runtime.
|
||||
///
|
||||
|
||||
@@ -24,6 +24,7 @@ use solitaire_data::{
|
||||
|
||||
use solitaire_data::settings::SyncBackend;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::assets::user_theme_dir;
|
||||
use crate::events::{
|
||||
DeleteAccountRequestEvent, InfoToastEvent, ManualSyncRequestEvent, SyncConfigureRequestEvent,
|
||||
@@ -32,9 +33,9 @@ use crate::events::{
|
||||
use crate::font_plugin::FontResource;
|
||||
use crate::progress_plugin::ProgressResource;
|
||||
use crate::resources::{SettingsScrollPos, SyncStatus, SyncStatusResource};
|
||||
use crate::theme::{
|
||||
ImportError, ThemeThumbnailCache, ThemeThumbnailPair, import_theme, refresh_registry,
|
||||
};
|
||||
use crate::theme::{ThemeThumbnailCache, ThemeThumbnailPair, refresh_registry};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::theme::{ImportError, import_theme};
|
||||
use crate::ui_focus::{FocusGroup, FocusRow, Focusable, FocusedButton};
|
||||
use crate::ui_modal::{
|
||||
ButtonVariant, ModalButton, ModalScrim, spawn_modal, spawn_modal_actions, spawn_modal_button,
|
||||
@@ -404,6 +405,7 @@ impl Plugin for SettingsPlugin {
|
||||
sync_settings_panel_visibility,
|
||||
handle_settings_buttons,
|
||||
handle_sync_buttons,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
handle_scan_themes,
|
||||
update_sync_status_text,
|
||||
update_card_back_text,
|
||||
@@ -1857,6 +1859,7 @@ fn spawn_settings_panel(
|
||||
font_res,
|
||||
);
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
import_themes_row(body, font_res);
|
||||
|
||||
// --- Privacy (only shown when a Matomo URL is configured) ---
|
||||
@@ -2641,6 +2644,7 @@ fn value_text_font(font_res: Option<&FontResource>) -> TextFont {
|
||||
/// [`InfoToastEvent`] is fired per imported theme. `IdCollision` errors (theme
|
||||
/// already installed) are silently skipped; all other errors produce a warning
|
||||
/// toast. A final toast tells the player to reopen Settings to see new themes.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn handle_scan_themes(
|
||||
interaction_query: Query<(&Interaction, &SettingsButton), Changed<Interaction>>,
|
||||
mut toast: MessageWriter<InfoToastEvent>,
|
||||
@@ -2759,6 +2763,7 @@ fn pill_button(
|
||||
/// then presses the button. [`handle_scan_themes`] picks them up, validates,
|
||||
/// and installs them. Reopen Settings to see newly imported themes in the
|
||||
/// card-theme picker.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn import_themes_row(parent: &mut ChildSpawnerCommands, font_res: Option<&FontResource>) {
|
||||
let caption_font = TextFont {
|
||||
font: font_res.map(|f| f.0.clone()).unwrap_or_default(),
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
//! handles directly on card entities, so a theme switch propagates on
|
||||
//! the next frame without re-spawning anything.
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod importer;
|
||||
pub mod loader;
|
||||
pub mod manifest;
|
||||
@@ -28,6 +29,7 @@ use thiserror::Error;
|
||||
|
||||
use solitaire_core::card::{Rank, Suit};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use importer::{ImportError, ThemeId, import_theme, import_theme_into};
|
||||
pub use loader::{CardThemeLoader, CardThemeLoaderError};
|
||||
pub use manifest::ThemeManifest;
|
||||
|
||||
Reference in New Issue
Block a user