Host: 256 feature flags, the onboarding mode (mode_get/mode_set, shell-only), and the one-time move of replaced plugin files to feature flags

- settings: MAX_FEATURES 64 -> 256 (an older app's whole-map backup still validates)
- modes: ModeState {mode, onboarded, existing} in modes.json; existing is decided natively on the first start
- plugins::migrate: PluginHost.superseded (file -> flag); enabled and as shipped -> flag on and the file goes; edited -> kept, flag left off; marker written last, every step repeatable; map_backup for imported enabled lists
- a superseded file is never written again by install_bundled
- create-waybetter: the host command list and PluginHost.superseded
This commit is contained in:
CRTkafa 2026-09-30 18:34:13 +07:00
parent f31b00eec7
commit 365f55f604
9 changed files with 477 additions and 6 deletions

View file

@ -22,6 +22,7 @@ use crate::{
host::{self, emit_shell, emit_tab_state, read_json, shell_only, site_tab_only, tab_only, tab_webview, write_json},
i18n::{self, NativeStrings},
layout,
modes::{ModeState, MODES_FILE},
plugins::{self, PluginHost, PluginStore, ScriptPlugin},
policy,
profiles::{self, AccountSeen, ProfileSettings},
@ -203,6 +204,8 @@ pub struct PageState {
/// A JSON object of booleans ([`settings::validate_features`]).
pub features: serde_json::Value,
pub settings: ShellSettings,
/// The onboarding mode the owner started from and whether the first-run screen was answered.
pub modes: ModeState,
}
struct Profiles(Mutex<ProfileState>);
@ -715,6 +718,25 @@ async fn features_set(webview: Webview, app: AppHandle, features: serde_json::Ma
set_features(&app, features)
}
/// The onboarding mode: what was chosen, whether the first-run screen was answered, and whether
/// this app was set up before modes existed.
#[tauri::command]
async fn mode_get(webview: Webview, app: AppHandle) -> Result<ModeState, String> {
shell_only(&webview)?;
Ok(page(&app)?.modes.clone())
}
/// Remembers the chosen mode (or none) and that the first-run screen was answered. The preset's
/// flags are the shell's to apply (`features_set`); this only records the choice.
#[tauri::command]
async fn mode_set(webview: Webview, app: AppHandle, mode: Option<String>, onboarded: bool) -> Result<ModeState, String> {
shell_only(&webview)?;
let next = page(&app)?.modes.chosen(mode, onboarded)?;
write_json(&app, MODES_FILE, &next)?;
page(&app)?.modes = next.clone();
Ok(next)
}
#[tauri::command]
async fn settings_get(webview: Webview, app: AppHandle) -> Result<ShellSettings, String> {
shell_only(&webview)?;
@ -952,6 +974,8 @@ fn host_handler() -> impl Fn(tauri::ipc::Invoke<Wry>) -> bool + Send + Sync + 's
custom_css_set,
features_get,
features_set,
mode_get,
mode_set,
settings_get,
settings_set,
tab_shortcut,
@ -1019,6 +1043,8 @@ impl App {
}
let directory = tauri_app.path().app_data_dir()?;
std::fs::create_dir_all(&directory)?;
// Someone set this app up before (read before anything below writes a file).
let existing = [PROFILES_FILE, SETTINGS_FILE, FEATURES_FILE, PLUGINS_DIR].iter().any(|name| directory.join(name).exists());
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
@ -1042,10 +1068,28 @@ impl App {
let settings = read_json::<serde_json::Value>(&handle, SETTINGS_FILE)
.and_then(|value| ShellSettings::from_value(value, &home).ok())
.unwrap_or_else(|| ShellSettings::defaults(&home));
let features = read_json::<serde_json::Value>(&handle, FEATURES_FILE)
let mut features = read_json::<serde_json::Value>(&handle, FEATURES_FILE)
.filter(serde_json::Value::is_object)
.unwrap_or_else(|| serde_json::json!({}));
tauri_app.manage(Page(Mutex::new(PageState { plugins, features, settings })));
// Plugin files the app turned into features: what was enabled becomes its flag, once
// (plugins::migrate). The flags are saved before any file is touched.
let migrated = plugins.migrate_superseded(config.plugins.superseded, |flags| {
let Some(map) = features.as_object_mut().filter(|_| !flags.is_empty()) else { return Ok(()) };
for flag in flags {
map.insert(flag.clone(), serde_json::Value::Bool(true));
}
settings::validate_features(map)?;
write_json(&handle, FEATURES_FILE, &*map)
});
if let Err(error) = migrated {
eprintln!("{}: {error}", config.title);
}
let modes = read_json::<ModeState>(&handle, MODES_FILE).and_then(|state| state.validated().ok()).unwrap_or_else(|| {
let state = ModeState::first_run(existing);
let _ = write_json(&handle, MODES_FILE, &state);
state
});
tauri_app.manage(Page(Mutex::new(PageState { plugins, features, settings, modes })));
tauri_app.manage(Host { config, hooks });
if let Some(setup) = setup {
setup(&handle)?;