use crate::serializable_map::PreferencesSerializableMap;
use crate::storage::{PreferencesStorage, PreferencesStorageResource};
use std::sync::Arc;
use crate::{PreferencesSet, PreferencesStorageType};
use bevy::app::MainScheduleOrder;
use bevy::ecs::component::Tick;
use bevy::ecs::schedule::{ExecutorKind, ScheduleLabel};
use bevy::ecs::system::SystemChangeTick;
use bevy::prelude::*;
use bevy::reflect::TypeRegistryArc;
use std::time::Duration;
#[derive(Clone)]
struct PreferencesStorageBuilder {
pub app_name: Option<&'static str>,
pub org_name: Option<&'static str>,
pub storage_type: PreferencesStorageType,
}
impl PreferencesStorageBuilder {
fn full_app_name(&self) -> Option<String> {
match (self.app_name, self.org_name) {
(None, None) => None,
(Some(app_name), Some(org_unit)) => Some(format!("{}.{}", org_unit, app_name)),
(Some(app_name), None) => Some(app_name.into()),
_ => None,
}
}
#[cfg(not(target_family = "wasm"))]
fn get_storage_parent_path_and_format(
&self,
) -> Option<(std::path::PathBuf, crate::storage::fs::FileStorageFormatFns)> {
let file_storage_path = self.storage_type.file_storage_path()?;
let file_storage_format = self.storage_type.file_storage_format()?;
let app_name = self.full_app_name()?;
Some((file_storage_path.join(app_name), file_storage_format))
}
fn create_storage(&self) -> Option<PreferencesStorageResource> {
if let PreferencesStorageType::Custom(custom) = &self.storage_type {
return Some(PreferencesStorageResource::from_arc(custom.clone()));
}
self.create_native_storage()
}
#[cfg(not(target_family = "wasm"))]
fn create_native_storage(&self) -> Option<PreferencesStorageResource> {
let storage =
self.get_storage_parent_path_and_format()
.and_then(|(parent_path, format)| {
crate::storage::fs::FileStorage::new_with_format(parent_path, format).ok()
});
storage.map(PreferencesStorageResource::new)
}
#[cfg(target_family = "wasm")]
fn create_native_storage(&self) -> Option<PreferencesStorageResource> {
let app_name = self.full_app_name()?;
let storage = self
.storage_type
.gloo_storage(format!("{app_name}_preferences"))?;
Some(PreferencesStorageResource::new(storage))
}
}
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
pub struct LoadPreferences;
pub struct PreferencesPlugin {
pub app_name: Option<&'static str>,
pub org_name: Option<&'static str>,
pub storage_type: PreferencesStorageType,
}
impl PreferencesPlugin {
pub fn persisted_with_app_name(app_name: &'static str) -> Self {
Self {
app_name: Some(app_name),
org_name: None,
storage_type: Default::default(),
}
}
pub fn with_no_persistence() -> Self {
Self {
app_name: None,
org_name: None,
storage_type: PreferencesStorageType::NoStorage,
}
}
pub fn with_storage_type(mut self, storage_type: PreferencesStorageType) -> Self {
self.storage_type = storage_type;
self
}
pub fn with_custom_storage(storage: impl PreferencesStorage) -> Self {
Self {
app_name: None,
org_name: None,
storage_type: PreferencesStorageType::Custom(Arc::new(storage)),
}
}
fn storage_builder(&self) -> PreferencesStorageBuilder {
PreferencesStorageBuilder {
app_name: self.app_name,
org_name: self.org_name,
storage_type: self.storage_type.clone(),
}
}
}
impl Plugin for PreferencesPlugin {
fn build(&self, app: &mut App) {
{
let world = app.world_mut();
let mut main_schedule_order = world.resource_mut::<MainScheduleOrder>();
main_schedule_order
.startup_labels
.insert(0, LoadPreferences.intern());
let mut schedule = Schedule::new(LoadPreferences);
schedule.set_executor_kind(ExecutorKind::Simple);
world.add_schedule(schedule);
}
app.add_event::<PreferencesSaved>()
.add_systems(
LoadPreferences,
load_preferences(self.storage_builder()).in_set(PreferencesSet::Load),
)
.configure_sets(
Last,
PreferencesSet::SetReflectMapValues.before(PreferencesSet::Save),
)
.add_systems(
Last,
save_preferences.in_set(PreferencesSet::Save).run_if(
resource_exists::<PreferencesStorageResource>
.and_then(resource_exists::<PreferencesSerializableMap>),
),
);
}
}
fn load_preferences(
storage_builder: PreferencesStorageBuilder,
) -> impl Fn(Commands, Res<AppTypeRegistry>) {
move |mut commands: Commands, app_type_registry: Res<AppTypeRegistry>| {
let type_registry_arc = TypeRegistryArc::clone(&app_type_registry);
let Some(storage) = storage_builder.create_storage() else {
return;
};
let seed = PreferencesSerializableMap::deserialize_seed(type_registry_arc.clone());
let preferences = match storage.load_preferences(seed) {
Ok(preferences) => preferences,
#[cfg(not(target_family = "wasm"))]
Err(crate::PreferencesError::IoError(io_error)) => {
if io_error.kind() != std::io::ErrorKind::NotFound {
error!("I/O Error loading preferences: {io_error}");
}
PreferencesSerializableMap::empty(type_registry_arc)
}
#[cfg(target_family = "wasm")]
Err(crate::PreferencesError::GlooError(
gloo_storage::errors::StorageError::KeyNotFound(_),
)) => PreferencesSerializableMap::empty(type_registry_arc),
Err(err) => {
error!("Unknown Error loading preferences: {err:?}");
PreferencesSerializableMap::empty(type_registry_arc)
}
};
commands.insert_resource(preferences);
commands.insert_resource(storage);
}
}
#[derive(Event, Copy, Clone, PartialEq, Eq, Hash, Default)]
pub struct PreferencesSaved;
#[allow(clippy::too_many_arguments)]
pub fn save_preferences(
time: Res<Time<Real>>,
preferences: Res<PreferencesSerializableMap>,
storage: Res<PreferencesStorageResource>,
mut last_save_tick: Local<Option<Tick>>,
mut last_save_time: Local<Duration>,
system_change_tick: SystemChangeTick,
mut app_exit: EventReader<AppExit>,
mut preferences_saved: EventWriter<PreferencesSaved>,
) {
let last_save_tick = last_save_tick.get_or_insert_with(|| system_change_tick.last_run());
let is_modified = preferences
.last_changed()
.is_newer_than(*last_save_tick, system_change_tick.this_run());
let mut should_trigger_save = is_modified;
if is_modified {
let duration_since_last_save = time.elapsed() - *last_save_time;
if duration_since_last_save.as_secs() < 1 {
should_trigger_save = false;
}
}
if !app_exit.is_empty() {
app_exit.clear();
if is_modified {
should_trigger_save = true;
}
}
if should_trigger_save {
if let Err(err) = storage.save_preferences(&preferences) {
error!("Error saving preferences: {err}");
} else {
preferences_saved.send_default();
}
*last_save_tick = preferences.last_changed();
*last_save_time = time.elapsed();
}
}