feat(settings): persist scalar settings across launches
This commit is contained in:
@@ -4,11 +4,14 @@
|
||||
//! reduce what survives a restart, and Private-profile data never
|
||||
//! reaches disk.
|
||||
|
||||
use ely_domain::{
|
||||
AppearanceSettings, FavoriteLimit, HistoryRecordingPolicy, NewTabDestination, SearchEngine,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
CoreError,
|
||||
state::BrowserCore,
|
||||
state::{BrowserCore, SyncObjectPolicies},
|
||||
sync_records::{
|
||||
BookmarkSyncRecord, HistorySyncRecord, NoteSyncRecord, PluginSettingsSyncRecord,
|
||||
ProfileSyncRecord, ReadingListSyncRecord, SNAPSHOT_SCHEMA_REV, SitePermissionSyncRecord,
|
||||
@@ -22,12 +25,35 @@ pub(crate) const LOCAL_STATE_REV: u32 = 1;
|
||||
struct LocalStateDocument {
|
||||
local_rev: u32,
|
||||
body: SyncSnapshotBody,
|
||||
// Scalar settings persist locally; cloud sync of them is a separate,
|
||||
// still-unbuilt concern, so they stay out of the sync wire schema.
|
||||
// `default` keeps older rev-1 files (written before settings existed)
|
||||
// loadable — their settings fall back to defaults.
|
||||
#[serde(default)]
|
||||
settings: LocalSettings,
|
||||
}
|
||||
|
||||
/// Scalar user settings that must survive a restart. Unlike the sync
|
||||
/// snapshot body, these never leave the device yet. `default` on the
|
||||
/// container fills any field a future revision has not written.
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
struct LocalSettings {
|
||||
search_engine: SearchEngine,
|
||||
new_tab_destination: NewTabDestination,
|
||||
history_recording_policy: HistoryRecordingPolicy,
|
||||
favorite_limit: FavoriteLimit,
|
||||
appearance: AppearanceSettings,
|
||||
sync_object_policies: SyncObjectPolicies,
|
||||
}
|
||||
|
||||
impl BrowserCore {
|
||||
pub fn build_local_state_bytes(&self) -> Result<Vec<u8>, CoreError> {
|
||||
let document =
|
||||
LocalStateDocument { local_rev: LOCAL_STATE_REV, body: local_body_from_core(self) };
|
||||
let document = LocalStateDocument {
|
||||
local_rev: LOCAL_STATE_REV,
|
||||
body: local_body_from_core(self),
|
||||
settings: self.local_settings(),
|
||||
};
|
||||
serde_json::to_vec(&document)
|
||||
.map_err(|error| CoreError::LocalState { reason: error.to_string() })
|
||||
}
|
||||
@@ -47,8 +73,29 @@ impl BrowserCore {
|
||||
}
|
||||
self.apply_sync_snapshot_body(document.body)
|
||||
.map_err(|error| CoreError::LocalState { reason: error.to_string() })?;
|
||||
self.apply_local_settings(document.settings);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn local_settings(&self) -> LocalSettings {
|
||||
LocalSettings {
|
||||
search_engine: self.search_engine(),
|
||||
new_tab_destination: self.new_tab_destination(),
|
||||
history_recording_policy: self.history_recording_policy(),
|
||||
favorite_limit: self.favorite_limit(),
|
||||
appearance: self.appearance(),
|
||||
sync_object_policies: self.sync_object_policies(),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_local_settings(&mut self, settings: LocalSettings) {
|
||||
self.set_search_engine(settings.search_engine);
|
||||
self.set_new_tab_destination(settings.new_tab_destination);
|
||||
self.set_history_recording_policy(settings.history_recording_policy);
|
||||
self.set_favorite_limit(settings.favorite_limit);
|
||||
self.set_appearance(settings.appearance);
|
||||
self.set_sync_object_policies(settings.sync_object_policies);
|
||||
}
|
||||
}
|
||||
|
||||
fn local_body_from_core(core: &BrowserCore) -> SyncSnapshotBody {
|
||||
|
||||
@@ -9,7 +9,7 @@ use ely_domain::{
|
||||
};
|
||||
|
||||
use crate::{CoreError, navigation::tab_title};
|
||||
use sync::SyncObjectPolicies;
|
||||
pub(crate) use sync::SyncObjectPolicies;
|
||||
|
||||
mod bookmarks;
|
||||
mod commands;
|
||||
|
||||
@@ -75,6 +75,10 @@ impl BrowserCore {
|
||||
self.appearance = AppearanceSettings::default();
|
||||
}
|
||||
|
||||
pub(crate) fn set_appearance(&mut self, appearance: AppearanceSettings) {
|
||||
self.appearance = appearance;
|
||||
}
|
||||
|
||||
pub fn set_command_query(&mut self, query: impl Into<String>) {
|
||||
self.command_query = query.into();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::time::{Duration, UNIX_EPOCH};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use ely_domain::{
|
||||
ArchivePolicy, BookmarkEntry, BookmarkId, BrowserTab, ProfileId, Space, SpaceId,
|
||||
SyncConnectionState, SyncObjectKind, SyncObjectPolicy, SyncObjectState, SyncObjectStatus,
|
||||
@@ -11,8 +13,9 @@ use super::{BrowserCore, sync_context::SyncSnapshotApplyContext};
|
||||
use crate::sync_engine::SyncSnapshotApplySummary;
|
||||
use crate::sync_records::{BookmarkSyncRecord, SpaceSyncRecord, TabSyncRecord};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct SyncObjectPolicies {
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub(crate) struct SyncObjectPolicies {
|
||||
spaces: SyncObjectPolicy,
|
||||
tabs: SyncObjectPolicy,
|
||||
bookmarks: SyncObjectPolicy,
|
||||
@@ -84,6 +87,14 @@ impl BrowserCore {
|
||||
self.sync_object_policies.get(kind)
|
||||
}
|
||||
|
||||
pub(crate) fn sync_object_policies(&self) -> SyncObjectPolicies {
|
||||
self.sync_object_policies
|
||||
}
|
||||
|
||||
pub(crate) fn set_sync_object_policies(&mut self, policies: SyncObjectPolicies) {
|
||||
self.sync_object_policies = policies;
|
||||
}
|
||||
|
||||
pub fn set_sync_connection_state(&mut self, state: SyncConnectionState) {
|
||||
self.sync_connection_state = state;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,43 @@
|
||||
use std::error::Error;
|
||||
|
||||
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
|
||||
use ely_domain::{SyncObjectKind, SyncObjectPolicy, UrlText};
|
||||
use ely_domain::{
|
||||
FavoriteLimit, HistoryRecordingPolicy, NewTabDestination, SearchEngine, SyncObjectKind,
|
||||
SyncObjectPolicy, ThemeMode, UrlText,
|
||||
};
|
||||
|
||||
fn standard_core() -> Result<BrowserCore, Box<dyn Error>> {
|
||||
Ok(BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_state_persists_scalar_settings() -> Result<(), Box<dyn Error>> {
|
||||
let mut before = standard_core()?;
|
||||
before.set_search_engine(SearchEngine::Google);
|
||||
before.set_new_tab_destination(NewTabDestination::Bookmarks);
|
||||
before.set_favorite_limit(FavoriteLimit::TwentyFour);
|
||||
before.set_history_recording_policy(HistoryRecordingPolicy::Pause);
|
||||
before.set_theme_mode(ThemeMode::Dark);
|
||||
// Privacy-critical: a paused sync toggle must not silently re-enable.
|
||||
before.set_sync_object_policy(SyncObjectKind::History, SyncObjectPolicy::Paused);
|
||||
let bytes = before.build_local_state_bytes()?;
|
||||
|
||||
let mut after = standard_core()?;
|
||||
after.apply_local_state_bytes(&bytes)?;
|
||||
|
||||
assert_eq!(after.search_engine(), SearchEngine::Google);
|
||||
assert_eq!(after.new_tab_destination(), NewTabDestination::Bookmarks);
|
||||
assert_eq!(after.favorite_limit(), FavoriteLimit::TwentyFour);
|
||||
assert_eq!(after.history_recording_policy(), HistoryRecordingPolicy::Pause);
|
||||
assert_eq!(after.appearance().theme_mode(), ThemeMode::Dark);
|
||||
assert_eq!(
|
||||
after.sync_object_policy(SyncObjectKind::History),
|
||||
SyncObjectPolicy::Paused,
|
||||
"a paused sync toggle must survive a restart",
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_state_round_trips_across_a_restart() -> Result<(), Box<dyn Error>> {
|
||||
let mut before = standard_core()?;
|
||||
|
||||
Reference in New Issue
Block a user