Merge branch 'dev' into next

pull/257/head
FabianLars 2 years ago
commit 082e05e045
No known key found for this signature in database
GPG Key ID: 3B12BC1DEBF61125

598
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -14,4 +14,4 @@ thiserror = "1"
edition = "2021" edition = "2021"
authors = [ "Tauri Programme within The Commons Conservancy" ] authors = [ "Tauri Programme within The Commons Conservancy" ]
license = "Apache-2.0 OR MIT" license = "Apache-2.0 OR MIT"
rust-version = "1.59" rust-version = "1.64"

@ -7,12 +7,15 @@
| [fs-extra](plugins/fs-extra) | File system methods that aren't included in the core API. | ✅ | ✅ | ✅ | ? | ? | | [fs-extra](plugins/fs-extra) | File system methods that aren't included in the core API. | ✅ | ✅ | ✅ | ? | ? |
| [fs-watch](plugins/fs-watch) | Watch the filesystem for changes. | ✅ | ✅ | ✅ | ? | ? | | [fs-watch](plugins/fs-watch) | Watch the filesystem for changes. | ✅ | ✅ | ✅ | ? | ? |
| [localhost](plugins/localhost) | Use a localhost server in production apps. | ✅ | ✅ | ✅ | ? | ? | | [localhost](plugins/localhost) | Use a localhost server in production apps. | ✅ | ✅ | ✅ | ? | ? |
| [log](plugins/log) | Configurable logging. | ✅ | ✅ | ✅ | ? | ? | | [log](plugins/log) | Configurable logging. | ✅ | ✅ | ✅ | ✅ | ✅ |
| [persisted-scope](plugins/persisted-scope) | Persist runtime scope changes on the filesystem. | ✅ | ✅ | ✅ | ? | ? | | [persisted-scope](plugins/persisted-scope) | Persist runtime scope changes on the filesystem. | ✅ | ✅ | ✅ | ? | ? |
| [positioner](plugins/positioner) | Move windows to common locations. | ✅ | ✅ | ✅ | ? | ? | | [positioner](plugins/positioner) | Move windows to common locations. | ✅ | ✅ | ✅ | ? | ? |
| [single-instance](plugins/single-instance) | Ensure a single instance of your tauri app is running. | ✅ | ? | ✅ | ? | ? |
| [sql](plugins/sql) | Interface with SQL databases. | ✅ | ✅ | ✅ | ? | ? | | [sql](plugins/sql) | Interface with SQL databases. | ✅ | ✅ | ✅ | ? | ? |
| [store](plugins/store) | Persistent key value storage. | ✅ | ✅ | ✅ | ? | ? | | [store](plugins/store) | Persistent key value storage. | ✅ | ✅ | ✅ | ? | ? |
| [stronghold](plugins/stronghold) | Encrypted, secure database. | ✅ | ✅ | ✅ | ? | ? | | [stronghold](plugins/stronghold) | Encrypted, secure database. | ✅ | ✅ | ✅ | ? | ? |
| [upload](plugins/upload) | Tauri plugin for file uploads through HTTP. | ✅ | ✅ | ✅ | ? | ? | | [upload](plugins/upload) | Tauri plugin for file uploads through HTTP. | ✅ | ✅ | ✅ | ? | ? |
| [websocket](plugins/websocket) | | ✅ | ✅ | ✅ | ? | ? | | [websocket](plugins/websocket) | Open a WebSocket connection using a Rust client in JS. | ✅ | ✅ | ✅ | ? | ? |
| [window-state](plugins/window-state) | Persist window sizes and positions. | ✅ | ✅ | ✅ | ? | ? | | [window-state](plugins/window-state) | Persist window sizes and positions. | ✅ | ✅ | ✅ | ? | ? |
_This repo and all plugins require a Rust version of at least **1.64**_

@ -11,13 +11,13 @@
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-terser": "^0.3.0", "@rollup/plugin-terser": "^0.4.0",
"@rollup/plugin-typescript": "^11.0.0", "@rollup/plugin-typescript": "^11.0.0",
"@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.46.1", "@typescript-eslint/parser": "^5.46.1",
"eslint": "^8.0.1", "eslint": "^8.0.1",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-config-standard-with-typescript": "^27.0.0", "eslint-config-standard-with-typescript": "^34.0.0",
"eslint-plugin-import": "^2.25.2", "eslint-plugin-import": "^2.25.2",
"eslint-plugin-n": "^15.0.0", "eslint-plugin-n": "^15.0.0",
"eslint-plugin-promise": "^6.0.0", "eslint-plugin-promise": "^6.0.0",

@ -4,6 +4,8 @@ Use hardware security-keys in your Tauri App.
## Install ## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend. There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) 1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -154,7 +154,7 @@ pub fn sign(
let sig = encode_config(sign_data, URL_SAFE_NO_PAD); let sig = encode_config(sign_data, URL_SAFE_NO_PAD);
println!("Sign result: {}", sig); println!("Sign result: {sig}");
println!( println!(
"Key handle used: {}", "Key handle used: {}",
encode_config(&handle_used, URL_SAFE_NO_PAD) encode_config(&handle_used, URL_SAFE_NO_PAD)
@ -173,10 +173,8 @@ pub fn sign(
} }
fn format_client_data(application: &str, challenge: &str) -> (Vec<u8>, Vec<u8>, String) { fn format_client_data(application: &str, challenge: &str) -> (Vec<u8>, Vec<u8>, String) {
let d = format!( let d =
r#"{{"challenge": "{}", "version": "U2F_V2", "appId": "{}"}}"#, format!(r#"{{"challenge": "{challenge}", "version": "U2F_V2", "appId": "{application}"}}"#);
challenge, application
);
let mut challenge = Sha256::new(); let mut challenge = Sha256::new();
challenge.update(d.as_bytes()); challenge.update(d.as_bytes());
let chall_bytes = challenge.finalize().to_vec(); let chall_bytes = challenge.finalize().to_vec();

@ -16,7 +16,7 @@ pub fn make_challenge(app_id: &str, challenge_bytes: Vec<u8>) -> Challenge {
let utc: DateTime<Utc> = Utc::now(); let utc: DateTime<Utc> = Utc::now();
Challenge { Challenge {
challenge: encode_config(challenge_bytes, URL_SAFE_NO_PAD), challenge: encode_config(challenge_bytes, URL_SAFE_NO_PAD),
timestamp: format!("{:?}", utc), timestamp: format!("{utc:?}"),
app_id: app_id.to_string(), app_id: app_id.to_string(),
} }
} }

@ -4,6 +4,8 @@ Automatically launch your application at startup. Supports Windows, Mac (via App
## Install ## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend. There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) 1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use auto_launch::{AutoLaunch, AutoLaunchBuilder}; use auto_launch::{AutoLaunch, AutoLaunchBuilder};
use log::info;
use serde::{ser::Serializer, Serialize}; use serde::{ser::Serializer, Serialize};
use tauri::{ use tauri::{
command, command,
@ -98,7 +99,6 @@ pub fn init<R: Runtime>(
.invoke_handler(tauri::generate_handler![enable, disable, is_enabled]) .invoke_handler(tauri::generate_handler![enable, disable, is_enabled])
.setup(move |app| { .setup(move |app| {
let mut builder = AutoLaunchBuilder::new(); let mut builder = AutoLaunchBuilder::new();
builder.set_app_name(&app.package_info().name); builder.set_app_name(&app.package_info().name);
if let Some(args) = args { if let Some(args) = args {
builder.set_args(&args); builder.set_args(&args);
@ -110,7 +110,26 @@ pub fn init<R: Runtime>(
#[cfg(windows)] #[cfg(windows)]
builder.set_app_path(&current_exe.display().to_string()); builder.set_app_path(&current_exe.display().to_string());
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
builder.set_app_path(&current_exe.canonicalize()?.display().to_string()); {
// on macOS, current_exe gives path to /Applications/Example.app/MacOS/Example
// but this results in seeing a Unix Executable in macOS login items
// It must be: /Applications/Example.app
// If it didn't find exactly a single occurance of .app, it will default to
// exe path to not break it.
let exe_path = current_exe.canonicalize()?.display().to_string();
let parts: Vec<&str> = exe_path.split(".app/").collect();
let app_path = if parts.len() == 2 {
format!(
"{}{}",
parts.get(0).unwrap().to_string(),
".app"
)
} else {
exe_path
};
info!("auto_start path {}", &app_path);
builder.set_app_path(&app_path);
}
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
if let Some(appimage) = app if let Some(appimage) = app
.env() .env()

@ -4,6 +4,8 @@ Additional file system methods not included in the core API.
## Install ## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend. There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) 1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -15,4 +15,5 @@ serde_json.workspace = true
tauri.workspace = true tauri.workspace = true
log.workspace = true log.workspace = true
thiserror.workspace = true thiserror.workspace = true
notify = "4.0" notify = { version = "5" , features = ["serde"] }
notify-debouncer-mini = { version = "0.2.1" , features = ["serde"] }

@ -4,6 +4,8 @@ Watch files and directories for changes using [notify](https://github.com/notify
## Install ## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend. There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) 1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -12,22 +12,31 @@ export interface DebouncedWatchOptions extends WatchOptions {
delayMs?: number; delayMs?: number;
} }
export interface RawEvent { export type RawEvent = {
path: string | null; type: RawEventKind;
operation: number; paths: string[];
cookie: number | null; attrs: unknown;
} };
type RawEventKind =
| "any "
| {
access?: unknown;
}
| {
create?: unknown;
}
| {
modify?: unknown;
}
| {
remove?: unknown;
}
| "other";
export type DebouncedEvent = export type DebouncedEvent =
| { type: "NoticeWrite"; payload: string } | { kind: "any"; path: string }
| { type: "NoticeRemove"; payload: string } | { kind: "AnyContinous"; path: string };
| { type: "Create"; payload: string }
| { type: "Write"; payload: string }
| { type: "Chmod"; payload: string }
| { type: "Remove"; payload: string }
| { type: "Rename"; payload: string }
| { type: "Rescan"; payload: null }
| { type: "Error"; payload: { error: string; path: string | null } };
async function unwatch(id: number): Promise<void> { async function unwatch(id: number): Promise<void> {
await invoke("plugin:fs-watch|unwatch", { id }); await invoke("plugin:fs-watch|unwatch", { id });

@ -1,7 +1,5 @@
use notify::{ use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
raw_watcher, watcher, DebouncedEvent, Op, RawEvent, RecommendedWatcher, RecursiveMode, use notify_debouncer_mini::{new_debouncer, DebounceEventResult, Debouncer};
Watcher as _,
};
use serde::{ser::Serializer, Deserialize, Serialize}; use serde::{ser::Serializer, Deserialize, Serialize};
use tauri::{ use tauri::{
command, command,
@ -39,72 +37,33 @@ impl Serialize for Error {
} }
#[derive(Default)] #[derive(Default)]
struct WatcherCollection(Mutex<HashMap<Id, (RecommendedWatcher, Vec<PathBuf>)>>); struct WatcherCollection(Mutex<HashMap<Id, (WatcherKind, Vec<PathBuf>)>>);
#[derive(Clone, Serialize)] enum WatcherKind {
struct RawEventWrapper { Debouncer(Debouncer<RecommendedWatcher>),
path: Option<PathBuf>, Watcher(RecommendedWatcher),
operation: u32,
cookie: Option<u32>,
} }
#[derive(Clone, Serialize)] fn watch_raw<R: Runtime>(window: Window<R>, rx: Receiver<notify::Result<Event>>, id: Id) {
#[serde(tag = "type", content = "payload")]
enum DebouncedEventWrapper {
NoticeWrite(PathBuf),
NoticeRemove(PathBuf),
Create(PathBuf),
Write(PathBuf),
Chmod(PathBuf),
Remove(PathBuf),
Rename(PathBuf, PathBuf),
Rescan,
Error {
error: String,
path: Option<PathBuf>,
},
}
impl From<DebouncedEvent> for DebouncedEventWrapper {
fn from(event: DebouncedEvent) -> Self {
match event {
DebouncedEvent::NoticeWrite(path) => Self::NoticeWrite(path),
DebouncedEvent::NoticeRemove(path) => Self::NoticeRemove(path),
DebouncedEvent::Create(path) => Self::Create(path),
DebouncedEvent::Write(path) => Self::Write(path),
DebouncedEvent::Chmod(path) => Self::Chmod(path),
DebouncedEvent::Remove(path) => Self::Remove(path),
DebouncedEvent::Rename(from, to) => Self::Rename(from, to),
DebouncedEvent::Rescan => Self::Rescan,
DebouncedEvent::Error(error, path) => Self::Error {
error: error.to_string(),
path,
},
}
}
}
fn watch_raw<R: Runtime>(window: Window<R>, rx: Receiver<RawEvent>, id: Id) {
spawn(move || { spawn(move || {
let event_name = format!("watcher://raw-event/{}", id); let event_name = format!("watcher://raw-event/{id}");
while let Ok(event) = rx.recv() { while let Ok(event) = rx.recv() {
let _ = window.emit( if let Ok(event) = event {
&event_name, // TODO: Should errors be emitted too?
RawEventWrapper { let _ = window.emit(&event_name, event);
path: event.path, }
operation: event.op.unwrap_or_else(|_| Op::empty()).bits(),
cookie: event.cookie,
},
);
} }
}); });
} }
fn watch_debounced<R: Runtime>(window: Window<R>, rx: Receiver<DebouncedEvent>, id: Id) { fn watch_debounced<R: Runtime>(window: Window<R>, rx: Receiver<DebounceEventResult>, id: Id) {
spawn(move || { spawn(move || {
let event_name = format!("watcher://debounced-event/{}", id); let event_name = format!("watcher://debounced-event/{id}");
while let Ok(event) = rx.recv() { while let Ok(event) = rx.recv() {
let _ = window.emit(&event_name, DebouncedEventWrapper::from(event)); if let Ok(event) = event {
// TODO: Should errors be emitted too?
let _ = window.emit(&event_name, event);
}
} }
}); });
} }
@ -132,20 +91,21 @@ async fn watch<R: Runtime>(
let watcher = if let Some(delay) = options.delay_ms { let watcher = if let Some(delay) = options.delay_ms {
let (tx, rx) = channel(); let (tx, rx) = channel();
let mut watcher = watcher(tx, Duration::from_millis(delay))?; let mut debouncer = new_debouncer(Duration::from_millis(delay), None, tx)?;
let watcher = debouncer.watcher();
for path in &paths { for path in &paths {
watcher.watch(path, mode)?; watcher.watch(path, mode)?;
} }
watch_debounced(window, rx, id); watch_debounced(window, rx, id);
watcher WatcherKind::Debouncer(debouncer)
} else { } else {
let (tx, rx) = channel(); let (tx, rx) = channel();
let mut watcher = raw_watcher(tx)?; let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
for path in &paths { for path in &paths {
watcher.watch(path, mode)?; watcher.watch(path, mode)?;
} }
watch_raw(window, rx, id); watch_raw(window, rx, id);
watcher WatcherKind::Watcher(watcher)
}; };
watchers.0.lock().unwrap().insert(id, (watcher, paths)); watchers.0.lock().unwrap().insert(id, (watcher, paths));
@ -155,10 +115,19 @@ async fn watch<R: Runtime>(
#[command] #[command]
async fn unwatch(watchers: State<'_, WatcherCollection>, id: Id) -> Result<()> { async fn unwatch(watchers: State<'_, WatcherCollection>, id: Id) -> Result<()> {
if let Some((mut watcher, paths)) = watchers.0.lock().unwrap().remove(&id) { if let Some((watcher, paths)) = watchers.0.lock().unwrap().remove(&id) {
for path in paths { match watcher {
watcher.unwatch(path)?; WatcherKind::Debouncer(mut debouncer) => {
} for path in paths {
debouncer.watcher().unwatch(&path)?
}
}
WatcherKind::Watcher(mut watcher) => {
for path in paths {
watcher.unwatch(&path)?
}
}
};
} }
Ok(()) Ok(())
} }

@ -6,6 +6,8 @@ Expose your apps assets through a localhost server instead of the default custom
## Install ## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend. There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) 1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -62,8 +62,8 @@ impl Builder {
.setup(move |app| { .setup(move |app| {
let asset_resolver = app.asset_resolver(); let asset_resolver = app.asset_resolver();
std::thread::spawn(move || { std::thread::spawn(move || {
let server = Server::http(&format!("localhost:{}", port)) let server =
.expect("Unable to spawn server"); Server::http(&format!("localhost:{port}")).expect("Unable to spawn server");
for req in server.incoming_requests() { for req in server.incoming_requests() {
let path = req let path = req
.url() .url()

@ -4,6 +4,8 @@ Configurable logging for your Tauri app.
## Install ## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend. There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) 1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -351,7 +351,7 @@ fn get_log_file_path(
rotation_strategy: &RotationStrategy, rotation_strategy: &RotationStrategy,
max_file_size: u128, max_file_size: u128,
) -> plugin::Result<PathBuf> { ) -> plugin::Result<PathBuf> {
let path = dir.as_ref().join(format!("{}.log", app_name)); let path = dir.as_ref().join(format!("{app_name}.log"));
if path.exists() { if path.exists() {
let log_size = File::open(&path)?.metadata()?.len() as u128; let log_size = File::open(&path)?.metadata()?.len() as u128;

@ -4,6 +4,8 @@ Save filesystem and asset scopes and restore them when the app is reopened.
## Install ## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend. There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) 1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -6,6 +6,8 @@ This plugin is a port of [electron-positioner](https://github.com/jenslind/elect
## Install ## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend. There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) 1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -4,6 +4,8 @@ Ensure a single instance of your tauri app is running.
## Install ## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend. There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) 1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -38,8 +38,8 @@ pub fn init<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
callback: f, callback: f,
app_handle: app.clone(), app_handle: app.clone(),
}; };
let dbus_name = format!("org.{}.SingleInstance", id); let dbus_name = format!("org.{id}.SingleInstance");
let dbus_path = format!("/org/{}/SingleInstance", id); let dbus_path = format!("/org/{id}/SingleInstance");
match ConnectionBuilder::session() match ConnectionBuilder::session()
.unwrap() .unwrap()

@ -31,9 +31,9 @@ pub fn init<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
.setup(|app| { .setup(|app| {
let id = &app.config().tauri.bundle.identifier; let id = &app.config().tauri.bundle.identifier;
let class_name = encode_wide(format!("{}-sic", id)); let class_name = encode_wide(format!("{id}-sic"));
let window_name = encode_wide(format!("{}-siw", id)); let window_name = encode_wide(format!("{id}-siw"));
let mutex_name = encode_wide(format!("{}-sim", id)); let mutex_name = encode_wide(format!("{id}-sim"));
let hmutex = let hmutex =
unsafe { CreateMutexW(std::ptr::null(), true.into(), mutex_name.as_ptr()) }; unsafe { CreateMutexW(std::ptr::null(), true.into(), mutex_name.as_ptr()) };
@ -113,10 +113,10 @@ unsafe extern "system" fn single_instance_window_proc<R: Runtime>(
let cds_ptr = lparam as *const COPYDATASTRUCT; let cds_ptr = lparam as *const COPYDATASTRUCT;
if (*cds_ptr).dwData == WMCOPYDATA_SINGLE_INSTANCE_DATA { if (*cds_ptr).dwData == WMCOPYDATA_SINGLE_INSTANCE_DATA {
let data = CStr::from_ptr((*cds_ptr).lpData as _).to_string_lossy(); let data = CStr::from_ptr((*cds_ptr).lpData as _).to_string_lossy();
let mut s = data.split("|"); let mut s = data.split('|');
let cwd = s.next().unwrap(); let cwd = s.next().unwrap();
let args = s.into_iter().map(|s| s.to_string()).collect(); let args = s.into_iter().map(|s| s.to_string()).collect();
callback(&app_handle, args, cwd.to_string()); callback(app_handle, args, cwd.to_string());
} }
1 1
} }

@ -4,6 +4,8 @@ Interface with SQL databases through [sqlx](https://github.com/launchbadge/sqlx)
## Install ## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend. There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) 1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)
@ -42,7 +44,7 @@ First you need to register the core plugin with Tauri:
```rust ```rust
fn main() { fn main() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_sql::Builder::default()) .plugin(tauri_plugin_sql::Builder::default().build())
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

@ -10,7 +10,7 @@ use sqlx::{
migrate::{ migrate::{
MigrateDatabase, Migration as SqlxMigration, MigrationSource, MigrationType, Migrator, MigrateDatabase, Migration as SqlxMigration, MigrationSource, MigrationType, Migrator,
}, },
Column, Pool, Row, TypeInfo, Column, Pool, Row, TypeInfo, ValueRef,
}; };
use tauri::{ use tauri::{
command, command,
@ -44,6 +44,8 @@ pub enum Error {
Migration(#[from] sqlx::migrate::MigrateError), Migration(#[from] sqlx::migrate::MigrateError),
#[error("database {0} not loaded")] #[error("database {0} not loaded")]
DatabaseNotLoaded(String), DatabaseNotLoaded(String),
#[error("unsupported datatype: {0}")]
UnsupportedDatatype(String),
} }
impl Serialize for Error { impl Serialize for Error {
@ -246,12 +248,16 @@ async fn select(
for row in rows { for row in rows {
let mut value = HashMap::default(); let mut value = HashMap::default();
for (i, column) in row.columns().iter().enumerate() { for (i, column) in row.columns().iter().enumerate() {
let info = column.type_info(); let v = row.try_get_raw(i)?;
let v = if info.is_null() {
let v = if v.is_null() {
JsonValue::Null JsonValue::Null
} else { } else {
match info.name() { // TODO: postgresql's JSON type
"VARCHAR" | "STRING" | "TEXT" | "DATETIME" => { match v.type_info().name() {
"VARCHAR" | "STRING" | "TEXT" | "TINYTEXT" | "LONGTEXT" | "NVARCHAR"
| "BIGVARCHAR" | "CHAR" | "BIGCHAR" | "NCHAR" | "DATETIME" | "DATE"
| "TIME" | "YEAR" | "TIMESTAMP" => {
if let Ok(s) = row.try_get(i) { if let Ok(s) = row.try_get(i) {
JsonValue::String(s) JsonValue::String(s)
} else { } else {
@ -266,22 +272,25 @@ async fn select(
JsonValue::Bool(x.to_lowercase() == "true") JsonValue::Bool(x.to_lowercase() == "true")
} }
} }
"INT" | "NUMBER" | "INTEGER" | "BIGINT" | "INT8" => { "INT" | "NUMBER" | "INTEGER" | "BIGINT" | "INT2" | "INT4" | "INT8"
| "NUMERIC" | "TINYINT" | "SMALLINT" | "MEDIUMINT" | "TINYINT UNSINGED"
| "SMALLINT UNSINGED" | "INT UNSINGED" | "MEDIUMINT UNSINGED"
| "BIGINT UNSINGED" => {
if let Ok(n) = row.try_get::<i64, usize>(i) { if let Ok(n) = row.try_get::<i64, usize>(i) {
JsonValue::Number(n.into()) JsonValue::Number(n.into())
} else { } else {
JsonValue::Null JsonValue::Null
} }
} }
"REAL" => { "REAL" | "FLOAT" | "DOUBLE" | "FLOAT4" | "FLOAT8" => {
if let Ok(n) = row.try_get::<f64, usize>(i) { if let Ok(n) = row.try_get::<f64, usize>(i) {
JsonValue::from(n) JsonValue::from(n)
} else { } else {
JsonValue::Null JsonValue::Null
} }
} }
// "JSON" => JsonValue::Object(row.get(i)), "BLOB" | "TINYBLOB" | "MEDIUMBLOB" | "LONGBLOB" | "BINARY" | "VARBINARY"
"BLOB" => { | "BYTEA" => {
if let Ok(n) = row.try_get::<Vec<u8>, usize>(i) { if let Ok(n) = row.try_get::<Vec<u8>, usize>(i) {
JsonValue::Array( JsonValue::Array(
n.into_iter().map(|n| JsonValue::Number(n.into())).collect(), n.into_iter().map(|n| JsonValue::Number(n.into())).collect(),
@ -290,13 +299,16 @@ async fn select(
JsonValue::Null JsonValue::Null
} }
} }
_ => JsonValue::Null, _ => return Err(Error::UnsupportedDatatype(v.type_info().name().to_string())),
} }
}; };
value.insert(column.name().to_string(), v); value.insert(column.name().to_string(), v);
} }
values.push(value); values.push(value);
} }
Ok(values) Ok(values)
} }

@ -4,6 +4,8 @@ Simple, persistent key-value store.
## Install ## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend. There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) 1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -4,6 +4,8 @@ Store secrets and keys using the [IOTA Stronghold](https://github.com/iotaledger
## Install ## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend. There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) 1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -18,5 +18,5 @@ thiserror.workspace = true
tokio = { version = "1", features = [ "fs" ] } tokio = { version = "1", features = [ "fs" ] }
tokio-util = { version = "0.7", features = [ "codec" ] } tokio-util = { version = "0.7", features = [ "codec" ] }
reqwest = { version = "0.11", features = [ "json", "stream" ] } reqwest = { version = "0.11", features = [ "json", "stream" ] }
futures = "0.3" futures-util = "0.3"
read-progress-stream = "1.0.0" read-progress-stream = "1.0.0"

@ -4,6 +4,8 @@ Upload files from disk to a remote server over HTTP.
## Install ## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend. There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) 1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -11,12 +11,12 @@ type ProgressHandler = (progress: number, total: number) => void;
const handlers: Map<number, ProgressHandler> = new Map(); const handlers: Map<number, ProgressHandler> = new Map();
let listening = false; let listening = false;
async function listenToUploadEventIfNeeded(): Promise<void> { async function listenToEventIfNeeded(event: string): Promise<void> {
if (listening) { if (listening) {
return await Promise.resolve(); return await Promise.resolve();
} }
return await appWindow return await appWindow
.listen<ProgressPayload>("upload://progress", ({ payload }) => { .listen<ProgressPayload>(event, ({ payload }) => {
const handler = handlers.get(payload.id); const handler = handlers.get(payload.id);
if (handler != null) { if (handler != null) {
handler(payload.progress, payload.total); handler(payload.progress, payload.total);
@ -27,7 +27,7 @@ async function listenToUploadEventIfNeeded(): Promise<void> {
}); });
} }
export default async function upload( async function upload(
url: string, url: string,
filePath: string, filePath: string,
progressHandler?: ProgressHandler, progressHandler?: ProgressHandler,
@ -41,7 +41,7 @@ export default async function upload(
handlers.set(id, progressHandler); handlers.set(id, progressHandler);
} }
await listenToUploadEventIfNeeded(); await listenToEventIfNeeded("upload://progress");
await invoke("plugin:upload|upload", { await invoke("plugin:upload|upload", {
id, id,
@ -50,3 +50,30 @@ export default async function upload(
headers: headers ?? {}, headers: headers ?? {},
}); });
} }
async function download(
url: string,
filePath: string,
progressHandler?: ProgressHandler,
headers?: Map<string, string>
): Promise<void> {
const ids = new Uint32Array(1);
window.crypto.getRandomValues(ids);
const id = ids[0];
if (progressHandler != null) {
handlers.set(id, progressHandler);
}
await listenToEventIfNeeded("download://progress");
await invoke("plugin:upload|upload", {
id,
url,
filePath,
headers: headers ?? {},
});
}
export default upload;
export { download, upload };

@ -2,14 +2,14 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use futures::TryStreamExt; use futures_util::TryStreamExt;
use serde::{ser::Serializer, Serialize}; use serde::{ser::Serializer, Serialize};
use tauri::{ use tauri::{
command, command,
plugin::{Builder as PluginBuilder, TauriPlugin}, plugin::{Builder as PluginBuilder, TauriPlugin},
Runtime, Window, Runtime, Window,
}; };
use tokio::fs::File; use tokio::{fs::File, io::AsyncWriteExt};
use tokio_util::codec::{BytesCodec, FramedRead}; use tokio_util::codec::{BytesCodec, FramedRead};
use read_progress_stream::ReadProgressStream; use read_progress_stream::ReadProgressStream;
@ -24,6 +24,8 @@ pub enum Error {
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error(transparent)] #[error(transparent)]
Request(#[from] reqwest::Error), Request(#[from] reqwest::Error),
#[error("{0}")]
ContentLength(String),
} }
impl Serialize for Error { impl Serialize for Error {
@ -42,6 +44,46 @@ struct ProgressPayload {
total: u64, total: u64,
} }
#[command]
async fn download<R: Runtime>(
window: Window<R>,
id: u32,
url: &str,
file_path: &str,
headers: HashMap<String, String>,
) -> Result<u32> {
let client = reqwest::Client::new();
let mut request = client.get(url);
// Loop trought the headers keys and values
// and add them to the request object.
for (key, value) in headers {
request = request.header(&key, value);
}
let response = request.send().await?;
let total = response.content_length().ok_or_else(|| {
Error::ContentLength(format!("Failed to get content length from '{}'", url))
})?;
let mut file = File::create(file_path).await?;
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.try_next().await? {
file.write_all(&chunk).await?;
let _ = window.emit(
"download://progress",
ProgressPayload {
id,
progress: chunk.len() as u64,
total,
},
);
}
Ok(id)
}
#[command] #[command]
async fn upload<R: Runtime>( async fn upload<R: Runtime>(
window: Window<R>, window: Window<R>,
@ -88,6 +130,6 @@ fn file_to_body<R: Runtime>(id: u32, window: Window<R>, file: File) -> reqwest::
pub fn init<R: Runtime>() -> TauriPlugin<R> { pub fn init<R: Runtime>() -> TauriPlugin<R> {
PluginBuilder::new("upload") PluginBuilder::new("upload")
.invoke_handler(tauri::generate_handler![upload]) .invoke_handler(tauri::generate_handler![download, upload])
.build() .build()
} }

@ -4,6 +4,8 @@
## Install ## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend. There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) 1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

@ -15,4 +15,5 @@ serde_json.workspace = true
tauri.workspace = true tauri.workspace = true
log.workspace = true log.workspace = true
thiserror.workspace = true thiserror.workspace = true
bincode = "1.3" bincode = "1.3"
bitflags = "1"

@ -1,9 +1,11 @@
![plugin-window-state](banner.png) ![plugin-window-state](banner.png)
Save window positions and sizse and restore them when the app is reopened. Save window positions and sizes and restore them when the app is reopened.
## Install ## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend. There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) 1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)
@ -39,19 +41,19 @@ Afterwards all windows will remember their state when the app is being closed an
Optionally you can also tell the plugin to save the state of all open window to disk my using the `save_window_state()` method exposed by the `AppHandleExt` trait: Optionally you can also tell the plugin to save the state of all open window to disk my using the `save_window_state()` method exposed by the `AppHandleExt` trait:
```rust ```rust
use tauri_plugin_window_state::AppHandleExt; use tauri_plugin_window_state::{AppHandleExt, StateFlags};
// `tauri::AppHandle` now has the following additional method // `tauri::AppHandle` now has the following additional method
app.save_window_state(); // will save the state of all open windows to disk app.save_window_state(StateFlags::all()); // will save the state of all open windows to disk
``` ```
To manually restore a windows state from disk you can call the `restore_state()` method exposed by the `WindowExt` trait: To manually restore a windows state from disk you can call the `restore_state()` method exposed by the `WindowExt` trait:
```rust ```rust
use tauri_plugin_window_state::{WindowExt, ShowMode}; use tauri_plugin_window_state::{WindowExt, StateFlags};
// all `Window` types now have the following additional method // all `Window` types now have the following additional method
window.restore_state(ShowMode::LastSaved); // will restore the windows state from disk window.restore_state(StateFlags::all()); // will restore the windows state from disk
``` ```
## Contributing ## Contributing

@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use bitflags::bitflags;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tauri::{ use tauri::{
plugin::{Builder as PluginBuilder, TauriPlugin}, plugin::{Builder as PluginBuilder, TauriPlugin},
@ -30,27 +31,27 @@ pub enum Error {
Bincode(#[from] Box<bincode::ErrorKind>), Bincode(#[from] Box<bincode::ErrorKind>),
} }
/// Defines how the window visibility should be restored. pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum ShowMode { bitflags! {
/// The window will always be shown, regardless of what the last stored state was. pub struct StateFlags: u32 {
Always, const SIZE = 1 << 0;
/// The window will be automatically shown if the last stored state for visibility was `true`. const POSITION = 1 << 1;
LastSaved, const MAXIMIZED = 1 << 2;
/// The window will not be automatically shown by this plugin. const VISIBLE = 1 << 3;
Never, const DECORATIONS = 1 << 4;
const FULLSCREEN = 1 << 5;
}
} }
impl Default for ShowMode { impl Default for StateFlags {
fn default() -> Self { fn default() -> Self {
Self::LastSaved Self::all()
} }
} }
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Default, Deserialize, Serialize)] #[derive(Debug, Default, Deserialize, Serialize)]
struct WindowMetadata { struct WindowState {
width: f64, width: f64,
height: f64, height: f64,
x: i32, x: i32,
@ -61,17 +62,23 @@ struct WindowMetadata {
fullscreen: bool, fullscreen: bool,
} }
struct WindowStateCache(Arc<Mutex<HashMap<String, WindowMetadata>>>); struct WindowStateCache(Arc<Mutex<HashMap<String, WindowState>>>);
pub trait AppHandleExt { pub trait AppHandleExt {
fn save_window_state(&self) -> Result<()>; fn save_window_state(&self, flags: StateFlags) -> Result<()>;
} }
impl<R: Runtime> AppHandleExt for tauri::AppHandle<R> { impl<R: Runtime> AppHandleExt for tauri::AppHandle<R> {
fn save_window_state(&self) -> Result<()> { fn save_window_state(&self, flags: StateFlags) -> Result<()> {
if let Some(app_dir) = self.path_resolver().app_config_dir() { if let Some(app_dir) = self.path_resolver().app_config_dir() {
let state_path = app_dir.join(STATE_FILENAME); let state_path = app_dir.join(STATE_FILENAME);
let cache = self.state::<WindowStateCache>(); let cache = self.state::<WindowStateCache>();
let state = cache.0.lock().unwrap(); let mut state = cache.0.lock().unwrap();
for (label, s) in state.iter_mut() {
if let Some(window) = self.get_window(label) {
window.update_state(s, flags)?;
}
}
create_dir_all(&app_dir) create_dir_all(&app_dir)
.map_err(Error::Io) .map_err(Error::Io)
.and_then(|_| File::create(state_path).map_err(Into::into)) .and_then(|_| File::create(state_path).map_err(Into::into))
@ -86,68 +93,146 @@ impl<R: Runtime> AppHandleExt for tauri::AppHandle<R> {
} }
pub trait WindowExt { pub trait WindowExt {
fn restore_state(&self, show_mode: ShowMode) -> tauri::Result<()>; fn restore_state(&self, flags: StateFlags) -> tauri::Result<()>;
} }
impl<R: Runtime> WindowExt for Window<R> { impl<R: Runtime> WindowExt for Window<R> {
fn restore_state(&self, show_mode: ShowMode) -> tauri::Result<()> { fn restore_state(&self, flags: StateFlags) -> tauri::Result<()> {
let cache = self.state::<WindowStateCache>(); let cache = self.state::<WindowStateCache>();
let mut c = cache.0.lock().unwrap(); let mut c = cache.0.lock().unwrap();
let mut should_show = true; let mut should_show = true;
if let Some(state) = c.get(self.label()) { if let Some(state) = c.get(self.label()) {
self.set_decorations(state.decorated)?; if flags.contains(StateFlags::DECORATIONS) {
self.set_decorations(state.decorated)?;
self.set_size(LogicalSize { }
width: state.width,
height: state.height, if flags.contains(StateFlags::SIZE) {
})?; self.set_size(LogicalSize {
width: state.width,
// restore position to saved value if saved monitor exists height: state.height,
// otherwise, let the OS decide where to place the window })?;
for m in self.available_monitors()? { }
if m.contains((state.x, state.y).into()) {
self.set_position(PhysicalPosition { if flags.contains(StateFlags::POSITION) {
x: state.x, // restore position to saved value if saved monitor exists
y: state.y, // otherwise, let the OS decide where to place the window
})?; for m in self.available_monitors()? {
if m.contains((state.x, state.y).into()) {
self.set_position(PhysicalPosition {
x: state.x,
y: state.y,
})?;
}
} }
} }
if state.maximized { if flags.contains(StateFlags::MAXIMIZED) && state.maximized {
self.maximize()?; self.maximize()?;
} }
self.set_fullscreen(state.fullscreen)?;
if flags.contains(StateFlags::FULLSCREEN) {
self.set_fullscreen(state.fullscreen)?;
}
should_show = state.visible; should_show = state.visible;
} else { } else {
let mut metadata = WindowState::default();
if flags.contains(StateFlags::SIZE) {
let scale_factor = self
.current_monitor()?
.map(|m| m.scale_factor())
.unwrap_or(1.);
let size = self.inner_size()?.to_logical(scale_factor);
metadata.width = size.width;
metadata.height = size.height;
}
if flags.contains(StateFlags::POSITION) {
let pos = self.outer_position()?;
metadata.x = pos.x;
metadata.y = pos.y;
}
if flags.contains(StateFlags::MAXIMIZED) {
metadata.maximized = self.is_maximized()?;
}
if flags.contains(StateFlags::VISIBLE) {
metadata.visible = self.is_visible()?;
}
if flags.contains(StateFlags::DECORATIONS) {
metadata.visible = self.is_visible()?;
}
if flags.contains(StateFlags::FULLSCREEN) {
metadata.fullscreen = self.is_fullscreen()?;
}
c.insert(self.label().into(), metadata);
}
if flags.contains(StateFlags::VISIBLE) && should_show {
self.show()?;
self.set_focus()?;
}
Ok(())
}
}
trait WindowExtInternal {
fn update_state(&self, state: &mut WindowState, flags: StateFlags) -> tauri::Result<()>;
}
impl<R: Runtime> WindowExtInternal for Window<R> {
fn update_state(&self, state: &mut WindowState, flags: StateFlags) -> tauri::Result<()> {
let is_maximized = match flags.intersects(StateFlags::MAXIMIZED | StateFlags::SIZE) {
true => self.is_maximized()?,
false => false,
};
if flags.contains(StateFlags::MAXIMIZED) {
state.maximized = is_maximized;
}
if flags.contains(StateFlags::FULLSCREEN) {
state.fullscreen = self.is_fullscreen()?;
}
if flags.contains(StateFlags::DECORATIONS) {
state.decorated = self.is_decorated()?;
}
if flags.contains(StateFlags::VISIBLE) {
state.visible = self.is_visible()?;
}
if flags.contains(StateFlags::SIZE) {
let scale_factor = self let scale_factor = self
.current_monitor()? .current_monitor()?
.map(|m| m.scale_factor()) .map(|m| m.scale_factor())
.unwrap_or(1.); .unwrap_or(1.);
let LogicalSize { width, height } = self.inner_size()?.to_logical(scale_factor); let size = self.inner_size()?.to_logical(scale_factor);
let PhysicalPosition { x, y } = self.outer_position()?;
let maximized = self.is_maximized().unwrap_or(false); // It doesn't make sense to save a self with 0 height or width
let visible = self.is_visible().unwrap_or(true); if size.width > 0. && size.height > 0. && !is_maximized {
let decorated = self.is_decorated().unwrap_or(true); state.width = size.width;
let fullscreen = self.is_fullscreen().unwrap_or(false); state.height = size.height;
c.insert( }
self.label().into(),
WindowMetadata {
width,
height,
x,
y,
maximized,
visible,
decorated,
fullscreen,
},
);
} }
if show_mode == ShowMode::Always || (show_mode == ShowMode::LastSaved && should_show) { if flags.contains(StateFlags::POSITION) {
self.show()?; let position = self.inner_position()?;
self.set_focus()?; if let Ok(Some(monitor)) = self.current_monitor() {
// save only window positions that are inside the current monitor
if monitor.contains(position) && !is_maximized {
state.x = position.x;
state.y = position.y;
}
}
} }
Ok(()) Ok(())
@ -156,17 +241,15 @@ impl<R: Runtime> WindowExt for Window<R> {
#[derive(Default)] #[derive(Default)]
pub struct Builder { pub struct Builder {
show_mode: ShowMode,
denylist: HashSet<String>, denylist: HashSet<String>,
skip_initial_state: HashSet<String>, skip_initial_state: HashSet<String>,
state_flags: StateFlags,
} }
impl Builder { impl Builder {
/// Sets how the window visibility should be restored. /// Sets the state flags to control what state gets restored and saved.
/// pub fn with_state_flags(mut self, flags: StateFlags) -> Self {
/// The default is [`ShowMode::LastSaved`] self.state_flags = flags;
pub fn with_show_mode(mut self, show_mode: ShowMode) -> Self {
self.show_mode = show_mode;
self self
} }
@ -184,9 +267,10 @@ impl Builder {
} }
pub fn build<R: Runtime>(self) -> TauriPlugin<R> { pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
let flags = self.state_flags;
PluginBuilder::new("window-state") PluginBuilder::new("window-state")
.setup(|app| { .setup(|app| {
let cache: Arc<Mutex<HashMap<String, WindowMetadata>>> = if let Some(app_dir) = let cache: Arc<Mutex<HashMap<String, WindowState>>> = if let Some(app_dir) =
app.path_resolver().app_config_dir() app.path_resolver().app_config_dir()
{ {
let state_path = app_dir.join(STATE_FILENAME); let state_path = app_dir.join(STATE_FILENAME);
@ -212,67 +296,26 @@ impl Builder {
} }
if !self.skip_initial_state.contains(window.label()) { if !self.skip_initial_state.contains(window.label()) {
let _ = window.restore_state(self.show_mode); let _ = window.restore_state(self.state_flags);
} }
let cache = window.state::<WindowStateCache>(); let cache = window.state::<WindowStateCache>();
let cache = cache.0.clone(); let cache = cache.0.clone();
let label = window.label().to_string(); let label = window.label().to_string();
let window_clone = window.clone(); let window_clone = window.clone();
window.on_window_event(move |e| match e { let flags = self.state_flags;
WindowEvent::Moved(position) => { window.on_window_event(move |e| {
let mut c = cache.lock().unwrap(); if let WindowEvent::CloseRequested { .. } = e {
if let Some(state) = c.get_mut(&label) {
let is_maximized = window_clone.is_maximized().unwrap_or(false);
state.maximized = is_maximized;
if let Some(monitor) = window_clone.current_monitor().unwrap() {
let monitor_position = monitor.position();
// save only window positions that are inside the current monitor
if position.x > monitor_position.x
&& position.y > monitor_position.y
&& !is_maximized
{
state.x = position.x;
state.y = position.y;
};
};
}
}
WindowEvent::Resized(size) => {
let scale_factor = window_clone
.current_monitor()
.ok()
.map(|m| m.map(|m| m.scale_factor()).unwrap_or(1.))
.unwrap_or(1.);
let size = size.to_logical(scale_factor);
let mut c = cache.lock().unwrap();
if let Some(state) = c.get_mut(&label) {
let is_maximized = window_clone.is_maximized().unwrap_or(false);
let is_fullscreen = window_clone.is_fullscreen().unwrap_or(false);
state.decorated = window_clone.is_decorated().unwrap_or(true);
state.maximized = is_maximized;
state.fullscreen = is_fullscreen;
// It doesn't make sense to save a window with 0 height or width
if size.width > 0. && size.height > 0. && !is_maximized {
state.width = size.width;
state.height = size.height;
}
}
}
WindowEvent::CloseRequested { .. } => {
let mut c = cache.lock().unwrap(); let mut c = cache.lock().unwrap();
if let Some(state) = c.get_mut(&label) { if let Some(state) = c.get_mut(&label) {
state.visible = window_clone.is_visible().unwrap_or(true); let _ = window_clone.update_state(state, flags);
} }
} }
_ => {}
}); });
}) })
.on_event(|app, event| { .on_event(move |app, event| {
if let RunEvent::Exit = event { if let RunEvent::Exit = event {
let _ = app.save_window_state(); let _ = app.save_window_state(flags);
} }
}) })
.build() .build()

@ -5,13 +5,13 @@ importers:
.: .:
specifiers: specifiers:
'@rollup/plugin-node-resolve': ^15.0.1 '@rollup/plugin-node-resolve': ^15.0.1
'@rollup/plugin-terser': ^0.3.0 '@rollup/plugin-terser': ^0.4.0
'@rollup/plugin-typescript': ^11.0.0 '@rollup/plugin-typescript': ^11.0.0
'@typescript-eslint/eslint-plugin': ^5.0.0 '@typescript-eslint/eslint-plugin': ^5.0.0
'@typescript-eslint/parser': ^5.46.1 '@typescript-eslint/parser': ^5.46.1
eslint: ^8.0.1 eslint: ^8.0.1
eslint-config-prettier: ^8.5.0 eslint-config-prettier: ^8.5.0
eslint-config-standard-with-typescript: ^27.0.0 eslint-config-standard-with-typescript: ^34.0.0
eslint-plugin-import: ^2.25.2 eslint-plugin-import: ^2.25.2
eslint-plugin-n: ^15.0.0 eslint-plugin-n: ^15.0.0
eslint-plugin-promise: ^6.0.0 eslint-plugin-promise: ^6.0.0
@ -20,13 +20,13 @@ importers:
typescript: ^4.9.4 typescript: ^4.9.4
devDependencies: devDependencies:
'@rollup/plugin-node-resolve': 15.0.1_rollup@3.7.4 '@rollup/plugin-node-resolve': 15.0.1_rollup@3.7.4
'@rollup/plugin-terser': 0.3.0_rollup@3.7.4 '@rollup/plugin-terser': 0.4.0_rollup@3.7.4
'@rollup/plugin-typescript': 11.0.0_fhibmf72xnv5tve6nwed265eae '@rollup/plugin-typescript': 11.0.0_fhibmf72xnv5tve6nwed265eae
'@typescript-eslint/eslint-plugin': 5.46.1_imrg37k3svwu377c6q7gkarwmi '@typescript-eslint/eslint-plugin': 5.46.1_imrg37k3svwu377c6q7gkarwmi
'@typescript-eslint/parser': 5.46.1_ha6vam6werchizxrnqvarmz2zu '@typescript-eslint/parser': 5.46.1_ha6vam6werchizxrnqvarmz2zu
eslint: 8.29.0 eslint: 8.29.0
eslint-config-prettier: 8.5.0_eslint@8.29.0 eslint-config-prettier: 8.5.0_eslint@8.29.0
eslint-config-standard-with-typescript: 27.0.1_f7skxvi3ubnnb5utlsc5vholvm eslint-config-standard-with-typescript: 34.0.0_f7skxvi3ubnnb5utlsc5vholvm
eslint-plugin-import: 2.26.0_z7hwuz3w5sq2sbhy7d4iqrnsvq eslint-plugin-import: 2.26.0_z7hwuz3w5sq2sbhy7d4iqrnsvq
eslint-plugin-n: 15.6.0_eslint@8.29.0 eslint-plugin-n: 15.6.0_eslint@8.29.0
eslint-plugin-promise: 6.1.1_eslint@8.29.0 eslint-plugin-promise: 6.1.1_eslint@8.29.0
@ -479,8 +479,8 @@ packages:
rollup: 3.7.4 rollup: 3.7.4
dev: true dev: true
/@rollup/plugin-terser/0.3.0_rollup@3.7.4: /@rollup/plugin-terser/0.4.0_rollup@3.7.4:
resolution: {integrity: sha512-mYTkNW9KjOscS/3QWU5LfOKsR3/fAAVDaqcAe2TZ7ng6pN46f+C7FOZbITuIW/neA+PhcjoKl7yMyB3XcmA4gw==} resolution: {integrity: sha512-Ipcf3LPNerey1q9ZMjiaWHlNPEHNU/B5/uh9zXLltfEQ1lVSLLeZSgAtTPWGyw8Ip1guOeq+mDtdOlEj/wNxQw==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
peerDependencies: peerDependencies:
rollup: ^2.x || ^3.x rollup: ^2.x || ^3.x
@ -1218,8 +1218,8 @@ packages:
eslint: 8.29.0 eslint: 8.29.0
dev: true dev: true
/eslint-config-standard-with-typescript/27.0.1_f7skxvi3ubnnb5utlsc5vholvm: /eslint-config-standard-with-typescript/34.0.0_f7skxvi3ubnnb5utlsc5vholvm:
resolution: {integrity: sha512-jJVyJldqqiCu3uSA/FP0x9jCDMG+Bbl73twTSnp0aysumJrz4iaVqLl+tGgmPrv0R829GVs117Nmn47M1DDDXA==} resolution: {integrity: sha512-zhCsI4/A0rJ1ma8sf3RLXYc0gc7yPmdTWRVXMh9dtqeUx3yBQyALH0wosHhk1uQ9QyItynLdNOtcHKNw8G7lQw==}
peerDependencies: peerDependencies:
'@typescript-eslint/eslint-plugin': ^5.0.0 '@typescript-eslint/eslint-plugin': ^5.0.0
eslint: ^8.0.1 eslint: ^8.0.1

@ -1,4 +1,12 @@
{ {
"extends": ["config:base"], "extends": ["config:base"],
"enabledManagers": ["cargo", "npm"] "enabledManagers": ["cargo", "npm"],
"packageRules": [
{
"description": "Disable node/pnpm version updates",
"matchPackageNames": ["node", "pnpm"],
"matchDepTypes": ["engines", "packageManager"],
"enabled": false
}
]
} }

@ -4,6 +4,8 @@
## Install ## Install
_This plugin requires a Rust version of at least **1.64**_
There are three general methods of installation that we can recommend. There are three general methods of installation that we can recommend.
1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked) 1. Use crates.io and npm (easiest, and requires you to trust that our publishing pipeline worked)

Loading…
Cancel
Save