Host: the store's commands (install links, dropped files, the signed Store, the install screen) are served for every app that sets AppConfig::with_store; commands::STORE, testkit::assert_shell_only_group and assert_store_guards
This commit is contained in:
parent
d67d55886d
commit
005a953162
5 changed files with 230 additions and 6 deletions
158
crates/waybetter-host/src/store_commands.rs
Normal file
158
crates/waybetter-host/src/store_commands.rs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
//! The store's Tauri commands for every app that sets [`crate::app::AppConfig::store`]: the
|
||||
//! `waybetter://install` deep link, dropped files, the signed wayBetter Store and the install
|
||||
//! screen's commands ([`crate::commands::STORE`]). Every command is shell-only; site tabs and web
|
||||
//! pages have no way to install anything. A deep link only *proposes* an install: the shell shows
|
||||
//! the confirmation screen and nothing is written until the owner confirms there.
|
||||
//!
|
||||
//! The rules, the catalog format and the install state are [`crate::store`]'s; this module only
|
||||
//! wraps them. An app without a store configuration registers nothing and every command here
|
||||
//! answers that it is not available.
|
||||
|
||||
use crate::app::AppConfig;
|
||||
use crate::host::{emit_shell, eval_in_site_tabs, shell_only};
|
||||
use crate::plugins::ScriptPlugin;
|
||||
use crate::policy;
|
||||
use crate::store::{InstallOutcome, InstallPreview, InstallRecords, LinkEvent, Store, StoreItemRef, StoreView, EVENT_LINK};
|
||||
use tauri::{AppHandle, Manager, State, Webview};
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
|
||||
/// Makes the [`Store`] for the app's configuration and listens for install links (launch argument,
|
||||
/// open-URL event, a second launch forwarded by single-instance).
|
||||
pub(crate) fn init(app: &AppHandle, config: &AppConfig) {
|
||||
let Some(store) = config.store else { return };
|
||||
let dir = app.path().app_data_dir().unwrap_or_default();
|
||||
app.manage(Store::new(store, config.plugins, dir));
|
||||
// Windows installs register the scheme in the NSIS installer and macOS in Info.plist; a Linux
|
||||
// AppImage and a Windows development build register it here (HKCU / xdg, current user only).
|
||||
#[cfg(any(target_os = "linux", all(windows, debug_assertions)))]
|
||||
if let Err(error) = app.deep_link().register_all() {
|
||||
eprintln!("{}: deep link registration failed: {error}", config.title);
|
||||
}
|
||||
let handle = app.clone();
|
||||
app.deep_link().on_open_url(move |event| {
|
||||
for url in event.urls() {
|
||||
received(&handle, url.as_str());
|
||||
}
|
||||
});
|
||||
// Launched by a link (Windows/Linux pass it as an argument; macOS as an open-URL event).
|
||||
if let Ok(Some(urls)) = app.deep_link().get_current() {
|
||||
for url in urls {
|
||||
received(app, url.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A `waybetter://` link reached the app: validate it, queue it for the shell and bring the window
|
||||
/// forward. Nothing is downloaded or installed until the shell's screen asks for it.
|
||||
fn received(app: &AppHandle, raw: &str) {
|
||||
let Some(store) = app.try_state::<Store>() else { return };
|
||||
emit_shell(app, EVENT_LINK, store.receive_link(raw));
|
||||
if let Some(window) = app.get_window(policy::MAIN_LABEL) {
|
||||
let _ = window.unminimize();
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
|
||||
fn store(app: &AppHandle) -> Result<State<'_, Store>, String> {
|
||||
app.try_state::<Store>().ok_or_else(|| "UNAVAILABLE: this app has no store".to_owned())
|
||||
}
|
||||
|
||||
fn eval_in_site(app: &AppHandle, script: &str) {
|
||||
eval_in_site_tabs(app, crate::app::config(app).site, script);
|
||||
}
|
||||
|
||||
/// Links that arrived before the shell listened (drained).
|
||||
#[tauri::command]
|
||||
pub(crate) async fn install_pending(webview: Webview, app: AppHandle) -> Result<Vec<LinkEvent>, String> {
|
||||
shell_only(&webview)?;
|
||||
Ok(store(&app)?.take_pending())
|
||||
}
|
||||
|
||||
/// Downloads what a validated install link points at and stages it for the confirmation screen.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn install_stage_link(webview: Webview, app: AppHandle, kind: String, url: String) -> Result<InstallPreview, String> {
|
||||
shell_only(&webview)?;
|
||||
store(&app)?.stage_link(&kind, &url).await
|
||||
}
|
||||
|
||||
/// A file dropped on the window (the shell read it with the File API; native code validates it).
|
||||
#[tauri::command]
|
||||
pub(crate) async fn install_stage_file(webview: Webview, app: AppHandle, name: String, text: String) -> Result<InstallPreview, String> {
|
||||
shell_only(&webview)?;
|
||||
store(&app)?.stage_file(&name, text)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn install_cancel(webview: Webview, app: AppHandle, token: String) -> Result<(), String> {
|
||||
shell_only(&webview)?;
|
||||
store(&app)?.cancel(&token);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The owner confirmed the screen for `token`. Plugins are written (validated again) and, when
|
||||
/// `enable` (or they were enabled before an update), started in every site tab. Themes are handed
|
||||
/// back for the shell to add under `theme_id`.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn install_commit(
|
||||
webview: Webview,
|
||||
app: AppHandle,
|
||||
token: String,
|
||||
enable: bool,
|
||||
theme_id: Option<String>,
|
||||
) -> Result<InstallOutcome, String> {
|
||||
shell_only(&webview)?;
|
||||
store(&app)?.commit(&token, enable, theme_id, |script| eval_in_site(&app, script))
|
||||
}
|
||||
|
||||
/// Deletes a plugin (stopped in every site tab first). Shell-only, like every plugin command.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn plugin_remove(webview: Webview, app: AppHandle, file: String) -> Result<Vec<ScriptPlugin>, String> {
|
||||
shell_only(&webview)?;
|
||||
store(&app)?.remove_plugin(&file, |script| eval_in_site(&app, script))
|
||||
}
|
||||
|
||||
/// Where installed plugins and themes came from (for badges and update checks).
|
||||
#[tauri::command]
|
||||
pub(crate) async fn install_records(webview: Webview, app: AppHandle) -> Result<InstallRecords, String> {
|
||||
shell_only(&webview)?;
|
||||
Ok(store(&app)?.records())
|
||||
}
|
||||
|
||||
/// A theme was removed in Appearance: forget where it came from.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn install_forget(webview: Webview, app: AppHandle, kind: String, name: String) -> Result<(), String> {
|
||||
shell_only(&webview)?;
|
||||
store(&app)?.forget(&kind, &name)
|
||||
}
|
||||
|
||||
/// The signed catalog, this app's items only (fetched at most every 10 minutes unless `refresh`),
|
||||
/// with what is installed.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn store_catalog(webview: Webview, app: AppHandle, refresh: bool) -> Result<StoreView, String> {
|
||||
shell_only(&webview)?;
|
||||
Ok(store(&app)?.catalog(refresh).await)
|
||||
}
|
||||
|
||||
/// Downloads a store item, checks it against the signed catalog (size + sha256) and stages it.
|
||||
#[tauri::command]
|
||||
pub(crate) async fn store_stage(webview: Webview, app: AppHandle, item: StoreItemRef) -> Result<InstallPreview, String> {
|
||||
shell_only(&webview)?;
|
||||
store(&app)?.stage_item(&item).await
|
||||
}
|
||||
|
||||
/// The store's commands ([`crate::commands::STORE`]).
|
||||
pub(crate) fn handler() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
|
||||
tauri::generate_handler![
|
||||
install_pending,
|
||||
install_stage_link,
|
||||
install_stage_file,
|
||||
install_cancel,
|
||||
install_commit,
|
||||
plugin_remove,
|
||||
install_records,
|
||||
install_forget,
|
||||
store_catalog,
|
||||
store_stage
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue