mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-04-21 11:26:15 +02:00
Merge branch 'dev' into next
This commit is contained in:
Generated
+276
-324
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -14,4 +14,4 @@ thiserror = "1"
|
||||
edition = "2021"
|
||||
authors = [ "Tauri Programme within The Commons Conservancy" ]
|
||||
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-watch](plugins/fs-watch) | Watch the filesystem for changes. | ✅ | ✅ | ✅ | ? | ? |
|
||||
| [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. | ✅ | ✅ | ✅ | ? | ? |
|
||||
| [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. | ✅ | ✅ | ✅ | ? | ? |
|
||||
| [store](plugins/store) | Persistent key value storage. | ✅ | ✅ | ✅ | ? | ? |
|
||||
| [stronghold](plugins/stronghold) | Encrypted, secure database. | ✅ | ✅ | ✅ | ? | ? |
|
||||
| [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. | ✅ | ✅ | ✅ | ? | ? |
|
||||
|
||||
_This repo and all plugins require a Rust version of at least **1.64**_
|
||||
|
||||
+2
-2
@@ -11,13 +11,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.46.1",
|
||||
"eslint": "^8.0.1",
|
||||
"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-n": "^15.0.0",
|
||||
"eslint-plugin-promise": "^6.0.0",
|
||||
|
||||
@@ -4,6 +4,8 @@ Use hardware security-keys in your Tauri App.
|
||||
|
||||
## Install
|
||||
|
||||
_This plugin requires a Rust version of at least **1.64**_
|
||||
|
||||
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)
|
||||
|
||||
@@ -154,7 +154,7 @@ pub fn sign(
|
||||
|
||||
let sig = encode_config(sign_data, URL_SAFE_NO_PAD);
|
||||
|
||||
println!("Sign result: {}", sig);
|
||||
println!("Sign result: {sig}");
|
||||
println!(
|
||||
"Key handle used: {}",
|
||||
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) {
|
||||
let d = format!(
|
||||
r#"{{"challenge": "{}", "version": "U2F_V2", "appId": "{}"}}"#,
|
||||
challenge, application
|
||||
);
|
||||
let d =
|
||||
format!(r#"{{"challenge": "{challenge}", "version": "U2F_V2", "appId": "{application}"}}"#);
|
||||
let mut challenge = Sha256::new();
|
||||
challenge.update(d.as_bytes());
|
||||
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();
|
||||
Challenge {
|
||||
challenge: encode_config(challenge_bytes, URL_SAFE_NO_PAD),
|
||||
timestamp: format!("{:?}", utc),
|
||||
timestamp: format!("{utc:?}"),
|
||||
app_id: app_id.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ Automatically launch your application at startup. Supports Windows, Mac (via App
|
||||
|
||||
## Install
|
||||
|
||||
_This plugin requires a Rust version of at least **1.64**_
|
||||
|
||||
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)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use auto_launch::{AutoLaunch, AutoLaunchBuilder};
|
||||
use log::info;
|
||||
use serde::{ser::Serializer, Serialize};
|
||||
use tauri::{
|
||||
command,
|
||||
@@ -98,7 +99,6 @@ pub fn init<R: Runtime>(
|
||||
.invoke_handler(tauri::generate_handler![enable, disable, is_enabled])
|
||||
.setup(move |app| {
|
||||
let mut builder = AutoLaunchBuilder::new();
|
||||
|
||||
builder.set_app_name(&app.package_info().name);
|
||||
if let Some(args) = args {
|
||||
builder.set_args(&args);
|
||||
@@ -110,7 +110,26 @@ pub fn init<R: Runtime>(
|
||||
#[cfg(windows)]
|
||||
builder.set_app_path(¤t_exe.display().to_string());
|
||||
#[cfg(target_os = "macos")]
|
||||
builder.set_app_path(¤t_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")]
|
||||
if let Some(appimage) = app
|
||||
.env()
|
||||
|
||||
@@ -4,6 +4,8 @@ Additional file system methods not included in the core API.
|
||||
|
||||
## Install
|
||||
|
||||
_This plugin requires a Rust version of at least **1.64**_
|
||||
|
||||
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)
|
||||
|
||||
@@ -15,4 +15,5 @@ serde_json.workspace = true
|
||||
tauri.workspace = true
|
||||
log.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
|
||||
|
||||
_This plugin requires a Rust version of at least **1.64**_
|
||||
|
||||
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)
|
||||
|
||||
@@ -12,22 +12,31 @@ export interface DebouncedWatchOptions extends WatchOptions {
|
||||
delayMs?: number;
|
||||
}
|
||||
|
||||
export interface RawEvent {
|
||||
path: string | null;
|
||||
operation: number;
|
||||
cookie: number | null;
|
||||
}
|
||||
export type RawEvent = {
|
||||
type: RawEventKind;
|
||||
paths: string[];
|
||||
attrs: unknown;
|
||||
};
|
||||
|
||||
type RawEventKind =
|
||||
| "any "
|
||||
| {
|
||||
access?: unknown;
|
||||
}
|
||||
| {
|
||||
create?: unknown;
|
||||
}
|
||||
| {
|
||||
modify?: unknown;
|
||||
}
|
||||
| {
|
||||
remove?: unknown;
|
||||
}
|
||||
| "other";
|
||||
|
||||
export type DebouncedEvent =
|
||||
| { type: "NoticeWrite"; payload: string }
|
||||
| { type: "NoticeRemove"; payload: 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 } };
|
||||
| { kind: "any"; path: string }
|
||||
| { kind: "AnyContinous"; path: string };
|
||||
|
||||
async function unwatch(id: number): Promise<void> {
|
||||
await invoke("plugin:fs-watch|unwatch", { id });
|
||||
|
||||
+36
-67
@@ -1,7 +1,5 @@
|
||||
use notify::{
|
||||
raw_watcher, watcher, DebouncedEvent, Op, RawEvent, RecommendedWatcher, RecursiveMode,
|
||||
Watcher as _,
|
||||
};
|
||||
use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use notify_debouncer_mini::{new_debouncer, DebounceEventResult, Debouncer};
|
||||
use serde::{ser::Serializer, Deserialize, Serialize};
|
||||
use tauri::{
|
||||
command,
|
||||
@@ -39,72 +37,33 @@ impl Serialize for Error {
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct WatcherCollection(Mutex<HashMap<Id, (RecommendedWatcher, Vec<PathBuf>)>>);
|
||||
struct WatcherCollection(Mutex<HashMap<Id, (WatcherKind, Vec<PathBuf>)>>);
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
struct RawEventWrapper {
|
||||
path: Option<PathBuf>,
|
||||
operation: u32,
|
||||
cookie: Option<u32>,
|
||||
enum WatcherKind {
|
||||
Debouncer(Debouncer<RecommendedWatcher>),
|
||||
Watcher(RecommendedWatcher),
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[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) {
|
||||
fn watch_raw<R: Runtime>(window: Window<R>, rx: Receiver<notify::Result<Event>>, id: Id) {
|
||||
spawn(move || {
|
||||
let event_name = format!("watcher://raw-event/{}", id);
|
||||
let event_name = format!("watcher://raw-event/{id}");
|
||||
while let Ok(event) = rx.recv() {
|
||||
let _ = window.emit(
|
||||
&event_name,
|
||||
RawEventWrapper {
|
||||
path: event.path,
|
||||
operation: event.op.unwrap_or_else(|_| Op::empty()).bits(),
|
||||
cookie: event.cookie,
|
||||
},
|
||||
);
|
||||
if let Ok(event) = event {
|
||||
// TODO: Should errors be emitted too?
|
||||
let _ = window.emit(&event_name, event);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 || {
|
||||
let event_name = format!("watcher://debounced-event/{}", id);
|
||||
let event_name = format!("watcher://debounced-event/{id}");
|
||||
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 (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 {
|
||||
watcher.watch(path, mode)?;
|
||||
}
|
||||
watch_debounced(window, rx, id);
|
||||
watcher
|
||||
WatcherKind::Debouncer(debouncer)
|
||||
} else {
|
||||
let (tx, rx) = channel();
|
||||
let mut watcher = raw_watcher(tx)?;
|
||||
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
|
||||
for path in &paths {
|
||||
watcher.watch(path, mode)?;
|
||||
}
|
||||
watch_raw(window, rx, id);
|
||||
watcher
|
||||
WatcherKind::Watcher(watcher)
|
||||
};
|
||||
|
||||
watchers.0.lock().unwrap().insert(id, (watcher, paths));
|
||||
@@ -155,10 +115,19 @@ async fn watch<R: Runtime>(
|
||||
|
||||
#[command]
|
||||
async fn unwatch(watchers: State<'_, WatcherCollection>, id: Id) -> Result<()> {
|
||||
if let Some((mut watcher, paths)) = watchers.0.lock().unwrap().remove(&id) {
|
||||
for path in paths {
|
||||
watcher.unwatch(path)?;
|
||||
}
|
||||
if let Some((watcher, paths)) = watchers.0.lock().unwrap().remove(&id) {
|
||||
match watcher {
|
||||
WatcherKind::Debouncer(mut debouncer) => {
|
||||
for path in paths {
|
||||
debouncer.watcher().unwatch(&path)?
|
||||
}
|
||||
}
|
||||
WatcherKind::Watcher(mut watcher) => {
|
||||
for path in paths {
|
||||
watcher.unwatch(&path)?
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ Expose your apps assets through a localhost server instead of the default custom
|
||||
|
||||
## Install
|
||||
|
||||
_This plugin requires a Rust version of at least **1.64**_
|
||||
|
||||
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)
|
||||
|
||||
@@ -62,8 +62,8 @@ impl Builder {
|
||||
.setup(move |app| {
|
||||
let asset_resolver = app.asset_resolver();
|
||||
std::thread::spawn(move || {
|
||||
let server = Server::http(&format!("localhost:{}", port))
|
||||
.expect("Unable to spawn server");
|
||||
let server =
|
||||
Server::http(&format!("localhost:{port}")).expect("Unable to spawn server");
|
||||
for req in server.incoming_requests() {
|
||||
let path = req
|
||||
.url()
|
||||
|
||||
@@ -4,6 +4,8 @@ Configurable logging for your Tauri app.
|
||||
|
||||
## Install
|
||||
|
||||
_This plugin requires a Rust version of at least **1.64**_
|
||||
|
||||
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)
|
||||
|
||||
@@ -351,7 +351,7 @@ fn get_log_file_path(
|
||||
rotation_strategy: &RotationStrategy,
|
||||
max_file_size: u128,
|
||||
) -> 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() {
|
||||
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
|
||||
|
||||
_This plugin requires a Rust version of at least **1.64**_
|
||||
|
||||
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)
|
||||
|
||||
@@ -6,6 +6,8 @@ This plugin is a port of [electron-positioner](https://github.com/jenslind/elect
|
||||
|
||||
## Install
|
||||
|
||||
_This plugin requires a Rust version of at least **1.64**_
|
||||
|
||||
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)
|
||||
|
||||
@@ -4,6 +4,8 @@ Ensure a single instance of your tauri app is running.
|
||||
|
||||
## Install
|
||||
|
||||
_This plugin requires a Rust version of at least **1.64**_
|
||||
|
||||
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)
|
||||
|
||||
@@ -38,8 +38,8 @@ pub fn init<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
|
||||
callback: f,
|
||||
app_handle: app.clone(),
|
||||
};
|
||||
let dbus_name = format!("org.{}.SingleInstance", id);
|
||||
let dbus_path = format!("/org/{}/SingleInstance", id);
|
||||
let dbus_name = format!("org.{id}.SingleInstance");
|
||||
let dbus_path = format!("/org/{id}/SingleInstance");
|
||||
|
||||
match ConnectionBuilder::session()
|
||||
.unwrap()
|
||||
|
||||
@@ -31,9 +31,9 @@ pub fn init<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
|
||||
.setup(|app| {
|
||||
let id = &app.config().tauri.bundle.identifier;
|
||||
|
||||
let class_name = encode_wide(format!("{}-sic", id));
|
||||
let window_name = encode_wide(format!("{}-siw", id));
|
||||
let mutex_name = encode_wide(format!("{}-sim", id));
|
||||
let class_name = encode_wide(format!("{id}-sic"));
|
||||
let window_name = encode_wide(format!("{id}-siw"));
|
||||
let mutex_name = encode_wide(format!("{id}-sim"));
|
||||
|
||||
let hmutex =
|
||||
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;
|
||||
if (*cds_ptr).dwData == WMCOPYDATA_SINGLE_INSTANCE_DATA {
|
||||
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 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
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ Interface with SQL databases through [sqlx](https://github.com/launchbadge/sqlx)
|
||||
|
||||
## Install
|
||||
|
||||
_This plugin requires a Rust version of at least **1.64**_
|
||||
|
||||
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)
|
||||
@@ -42,7 +44,7 @@ First you need to register the core plugin with Tauri:
|
||||
```rust
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_sql::Builder::default())
|
||||
.plugin(tauri_plugin_sql::Builder::default().build())
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
+22
-10
@@ -10,7 +10,7 @@ use sqlx::{
|
||||
migrate::{
|
||||
MigrateDatabase, Migration as SqlxMigration, MigrationSource, MigrationType, Migrator,
|
||||
},
|
||||
Column, Pool, Row, TypeInfo,
|
||||
Column, Pool, Row, TypeInfo, ValueRef,
|
||||
};
|
||||
use tauri::{
|
||||
command,
|
||||
@@ -44,6 +44,8 @@ pub enum Error {
|
||||
Migration(#[from] sqlx::migrate::MigrateError),
|
||||
#[error("database {0} not loaded")]
|
||||
DatabaseNotLoaded(String),
|
||||
#[error("unsupported datatype: {0}")]
|
||||
UnsupportedDatatype(String),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
@@ -246,12 +248,16 @@ async fn select(
|
||||
for row in rows {
|
||||
let mut value = HashMap::default();
|
||||
for (i, column) in row.columns().iter().enumerate() {
|
||||
let info = column.type_info();
|
||||
let v = if info.is_null() {
|
||||
let v = row.try_get_raw(i)?;
|
||||
|
||||
let v = if v.is_null() {
|
||||
JsonValue::Null
|
||||
} else {
|
||||
match info.name() {
|
||||
"VARCHAR" | "STRING" | "TEXT" | "DATETIME" => {
|
||||
// TODO: postgresql's JSON type
|
||||
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) {
|
||||
JsonValue::String(s)
|
||||
} else {
|
||||
@@ -266,22 +272,25 @@ async fn select(
|
||||
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) {
|
||||
JsonValue::Number(n.into())
|
||||
} else {
|
||||
JsonValue::Null
|
||||
}
|
||||
}
|
||||
"REAL" => {
|
||||
"REAL" | "FLOAT" | "DOUBLE" | "FLOAT4" | "FLOAT8" => {
|
||||
if let Ok(n) = row.try_get::<f64, usize>(i) {
|
||||
JsonValue::from(n)
|
||||
} else {
|
||||
JsonValue::Null
|
||||
}
|
||||
}
|
||||
// "JSON" => JsonValue::Object(row.get(i)),
|
||||
"BLOB" => {
|
||||
"BLOB" | "TINYBLOB" | "MEDIUMBLOB" | "LONGBLOB" | "BINARY" | "VARBINARY"
|
||||
| "BYTEA" => {
|
||||
if let Ok(n) = row.try_get::<Vec<u8>, usize>(i) {
|
||||
JsonValue::Array(
|
||||
n.into_iter().map(|n| JsonValue::Number(n.into())).collect(),
|
||||
@@ -290,13 +299,16 @@ async fn select(
|
||||
JsonValue::Null
|
||||
}
|
||||
}
|
||||
_ => JsonValue::Null,
|
||||
_ => return Err(Error::UnsupportedDatatype(v.type_info().name().to_string())),
|
||||
}
|
||||
};
|
||||
|
||||
value.insert(column.name().to_string(), v);
|
||||
}
|
||||
|
||||
values.push(value);
|
||||
}
|
||||
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ Simple, persistent key-value store.
|
||||
|
||||
## Install
|
||||
|
||||
_This plugin requires a Rust version of at least **1.64**_
|
||||
|
||||
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)
|
||||
|
||||
@@ -4,6 +4,8 @@ Store secrets and keys using the [IOTA Stronghold](https://github.com/iotaledger
|
||||
|
||||
## Install
|
||||
|
||||
_This plugin requires a Rust version of at least **1.64**_
|
||||
|
||||
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)
|
||||
|
||||
@@ -18,5 +18,5 @@ thiserror.workspace = true
|
||||
tokio = { version = "1", features = [ "fs" ] }
|
||||
tokio-util = { version = "0.7", features = [ "codec" ] }
|
||||
reqwest = { version = "0.11", features = [ "json", "stream" ] }
|
||||
futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
read-progress-stream = "1.0.0"
|
||||
@@ -4,6 +4,8 @@ Upload files from disk to a remote server over HTTP.
|
||||
|
||||
## Install
|
||||
|
||||
_This plugin requires a Rust version of at least **1.64**_
|
||||
|
||||
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)
|
||||
|
||||
@@ -11,12 +11,12 @@ type ProgressHandler = (progress: number, total: number) => void;
|
||||
const handlers: Map<number, ProgressHandler> = new Map();
|
||||
let listening = false;
|
||||
|
||||
async function listenToUploadEventIfNeeded(): Promise<void> {
|
||||
async function listenToEventIfNeeded(event: string): Promise<void> {
|
||||
if (listening) {
|
||||
return await Promise.resolve();
|
||||
}
|
||||
return await appWindow
|
||||
.listen<ProgressPayload>("upload://progress", ({ payload }) => {
|
||||
.listen<ProgressPayload>(event, ({ payload }) => {
|
||||
const handler = handlers.get(payload.id);
|
||||
if (handler != null) {
|
||||
handler(payload.progress, payload.total);
|
||||
@@ -27,7 +27,7 @@ async function listenToUploadEventIfNeeded(): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
export default async function upload(
|
||||
async function upload(
|
||||
url: string,
|
||||
filePath: string,
|
||||
progressHandler?: ProgressHandler,
|
||||
@@ -41,7 +41,7 @@ export default async function upload(
|
||||
handlers.set(id, progressHandler);
|
||||
}
|
||||
|
||||
await listenToUploadEventIfNeeded();
|
||||
await listenToEventIfNeeded("upload://progress");
|
||||
|
||||
await invoke("plugin:upload|upload", {
|
||||
id,
|
||||
@@ -50,3 +50,30 @@ export default async function upload(
|
||||
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: MIT
|
||||
|
||||
use futures::TryStreamExt;
|
||||
use futures_util::TryStreamExt;
|
||||
use serde::{ser::Serializer, Serialize};
|
||||
use tauri::{
|
||||
command,
|
||||
plugin::{Builder as PluginBuilder, TauriPlugin},
|
||||
Runtime, Window,
|
||||
};
|
||||
use tokio::fs::File;
|
||||
use tokio::{fs::File, io::AsyncWriteExt};
|
||||
use tokio_util::codec::{BytesCodec, FramedRead};
|
||||
|
||||
use read_progress_stream::ReadProgressStream;
|
||||
@@ -24,6 +24,8 @@ pub enum Error {
|
||||
Io(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
Request(#[from] reqwest::Error),
|
||||
#[error("{0}")]
|
||||
ContentLength(String),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
@@ -42,6 +44,46 @@ struct ProgressPayload {
|
||||
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]
|
||||
async fn upload<R: Runtime>(
|
||||
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> {
|
||||
PluginBuilder::new("upload")
|
||||
.invoke_handler(tauri::generate_handler![upload])
|
||||
.invoke_handler(tauri::generate_handler![download, upload])
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
## Install
|
||||
|
||||
_This plugin requires a Rust version of at least **1.64**_
|
||||
|
||||
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)
|
||||
|
||||
@@ -15,4 +15,5 @@ serde_json.workspace = true
|
||||
tauri.workspace = true
|
||||
log.workspace = true
|
||||
thiserror.workspace = true
|
||||
bincode = "1.3"
|
||||
bincode = "1.3"
|
||||
bitflags = "1"
|
||||
@@ -1,9 +1,11 @@
|
||||

|
||||
|
||||
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
|
||||
|
||||
_This plugin requires a Rust version of at least **1.64**_
|
||||
|
||||
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)
|
||||
@@ -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:
|
||||
|
||||
```rust
|
||||
use tauri_plugin_window_state::AppHandleExt;
|
||||
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
||||
|
||||
// `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:
|
||||
|
||||
```rust
|
||||
use tauri_plugin_window_state::{WindowExt, ShowMode};
|
||||
use tauri_plugin_window_state::{WindowExt, StateFlags};
|
||||
|
||||
// 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
|
||||
|
||||
+158
-115
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use bitflags::bitflags;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{
|
||||
plugin::{Builder as PluginBuilder, TauriPlugin},
|
||||
@@ -30,27 +31,27 @@ pub enum Error {
|
||||
Bincode(#[from] Box<bincode::ErrorKind>),
|
||||
}
|
||||
|
||||
/// Defines how the window visibility should be restored.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum ShowMode {
|
||||
/// The window will always be shown, regardless of what the last stored state was.
|
||||
Always,
|
||||
/// The window will be automatically shown if the last stored state for visibility was `true`.
|
||||
LastSaved,
|
||||
/// The window will not be automatically shown by this plugin.
|
||||
Never,
|
||||
}
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
impl Default for ShowMode {
|
||||
fn default() -> Self {
|
||||
Self::LastSaved
|
||||
bitflags! {
|
||||
pub struct StateFlags: u32 {
|
||||
const SIZE = 1 << 0;
|
||||
const POSITION = 1 << 1;
|
||||
const MAXIMIZED = 1 << 2;
|
||||
const VISIBLE = 1 << 3;
|
||||
const DECORATIONS = 1 << 4;
|
||||
const FULLSCREEN = 1 << 5;
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
impl Default for StateFlags {
|
||||
fn default() -> Self {
|
||||
Self::all()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
struct WindowMetadata {
|
||||
struct WindowState {
|
||||
width: f64,
|
||||
height: f64,
|
||||
x: i32,
|
||||
@@ -61,17 +62,23 @@ struct WindowMetadata {
|
||||
fullscreen: bool,
|
||||
}
|
||||
|
||||
struct WindowStateCache(Arc<Mutex<HashMap<String, WindowMetadata>>>);
|
||||
struct WindowStateCache(Arc<Mutex<HashMap<String, WindowState>>>);
|
||||
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> {
|
||||
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() {
|
||||
let state_path = app_dir.join(STATE_FILENAME);
|
||||
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)
|
||||
.map_err(Error::Io)
|
||||
.and_then(|_| File::create(state_path).map_err(Into::into))
|
||||
@@ -86,66 +93,88 @@ impl<R: Runtime> AppHandleExt for tauri::AppHandle<R> {
|
||||
}
|
||||
|
||||
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> {
|
||||
fn restore_state(&self, show_mode: ShowMode) -> tauri::Result<()> {
|
||||
fn restore_state(&self, flags: StateFlags) -> tauri::Result<()> {
|
||||
let cache = self.state::<WindowStateCache>();
|
||||
let mut c = cache.0.lock().unwrap();
|
||||
let mut should_show = true;
|
||||
|
||||
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,
|
||||
height: state.height,
|
||||
})?;
|
||||
}
|
||||
|
||||
// restore position to saved value if saved monitor exists
|
||||
// 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 flags.contains(StateFlags::POSITION) {
|
||||
// restore position to saved value if saved monitor exists
|
||||
// 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.set_fullscreen(state.fullscreen)?;
|
||||
|
||||
if flags.contains(StateFlags::FULLSCREEN) {
|
||||
self.set_fullscreen(state.fullscreen)?;
|
||||
}
|
||||
|
||||
should_show = state.visible;
|
||||
} else {
|
||||
let scale_factor = self
|
||||
.current_monitor()?
|
||||
.map(|m| m.scale_factor())
|
||||
.unwrap_or(1.);
|
||||
let LogicalSize { width, height } = self.inner_size()?.to_logical(scale_factor);
|
||||
let PhysicalPosition { x, y } = self.outer_position()?;
|
||||
let maximized = self.is_maximized().unwrap_or(false);
|
||||
let visible = self.is_visible().unwrap_or(true);
|
||||
let decorated = self.is_decorated().unwrap_or(true);
|
||||
let fullscreen = self.is_fullscreen().unwrap_or(false);
|
||||
c.insert(
|
||||
self.label().into(),
|
||||
WindowMetadata {
|
||||
width,
|
||||
height,
|
||||
x,
|
||||
y,
|
||||
maximized,
|
||||
visible,
|
||||
decorated,
|
||||
fullscreen,
|
||||
},
|
||||
);
|
||||
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 show_mode == ShowMode::Always || (show_mode == ShowMode::LastSaved && should_show) {
|
||||
if flags.contains(StateFlags::VISIBLE) && should_show {
|
||||
self.show()?;
|
||||
self.set_focus()?;
|
||||
}
|
||||
@@ -154,19 +183,73 @@ impl<R: Runtime> WindowExt for Window<R> {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
.current_monitor()?
|
||||
.map(|m| m.scale_factor())
|
||||
.unwrap_or(1.);
|
||||
let size = self.inner_size()?.to_logical(scale_factor);
|
||||
|
||||
// It doesn't make sense to save a self with 0 height or width
|
||||
if size.width > 0. && size.height > 0. && !is_maximized {
|
||||
state.width = size.width;
|
||||
state.height = size.height;
|
||||
}
|
||||
}
|
||||
|
||||
if flags.contains(StateFlags::POSITION) {
|
||||
let position = self.inner_position()?;
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Builder {
|
||||
show_mode: ShowMode,
|
||||
denylist: HashSet<String>,
|
||||
skip_initial_state: HashSet<String>,
|
||||
state_flags: StateFlags,
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
/// Sets how the window visibility should be restored.
|
||||
///
|
||||
/// The default is [`ShowMode::LastSaved`]
|
||||
pub fn with_show_mode(mut self, show_mode: ShowMode) -> Self {
|
||||
self.show_mode = show_mode;
|
||||
/// Sets the state flags to control what state gets restored and saved.
|
||||
pub fn with_state_flags(mut self, flags: StateFlags) -> Self {
|
||||
self.state_flags = flags;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -184,9 +267,10 @@ impl Builder {
|
||||
}
|
||||
|
||||
pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
|
||||
let flags = self.state_flags;
|
||||
PluginBuilder::new("window-state")
|
||||
.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()
|
||||
{
|
||||
let state_path = app_dir.join(STATE_FILENAME);
|
||||
@@ -212,67 +296,26 @@ impl Builder {
|
||||
}
|
||||
|
||||
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 = cache.0.clone();
|
||||
let label = window.label().to_string();
|
||||
let window_clone = window.clone();
|
||||
window.on_window_event(move |e| match e {
|
||||
WindowEvent::Moved(position) => {
|
||||
let flags = self.state_flags;
|
||||
window.on_window_event(move |e| {
|
||||
if let WindowEvent::CloseRequested { .. } = e {
|
||||
let mut c = cache.lock().unwrap();
|
||||
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;
|
||||
};
|
||||
};
|
||||
let _ = window_clone.update_state(state, flags);
|
||||
}
|
||||
}
|
||||
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();
|
||||
if let Some(state) = c.get_mut(&label) {
|
||||
state.visible = window_clone.is_visible().unwrap_or(true);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
})
|
||||
.on_event(|app, event| {
|
||||
.on_event(move |app, event| {
|
||||
if let RunEvent::Exit = event {
|
||||
let _ = app.save_window_state();
|
||||
let _ = app.save_window_state(flags);
|
||||
}
|
||||
})
|
||||
.build()
|
||||
|
||||
Generated
+8
-8
@@ -5,13 +5,13 @@ importers:
|
||||
.:
|
||||
specifiers:
|
||||
'@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
|
||||
'@typescript-eslint/eslint-plugin': ^5.0.0
|
||||
'@typescript-eslint/parser': ^5.46.1
|
||||
eslint: ^8.0.1
|
||||
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-n: ^15.0.0
|
||||
eslint-plugin-promise: ^6.0.0
|
||||
@@ -20,13 +20,13 @@ importers:
|
||||
typescript: ^4.9.4
|
||||
devDependencies:
|
||||
'@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
|
||||
'@typescript-eslint/eslint-plugin': 5.46.1_imrg37k3svwu377c6q7gkarwmi
|
||||
'@typescript-eslint/parser': 5.46.1_ha6vam6werchizxrnqvarmz2zu
|
||||
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-n: 15.6.0_eslint@8.29.0
|
||||
eslint-plugin-promise: 6.1.1_eslint@8.29.0
|
||||
@@ -479,8 +479,8 @@ packages:
|
||||
rollup: 3.7.4
|
||||
dev: true
|
||||
|
||||
/@rollup/plugin-terser/0.3.0_rollup@3.7.4:
|
||||
resolution: {integrity: sha512-mYTkNW9KjOscS/3QWU5LfOKsR3/fAAVDaqcAe2TZ7ng6pN46f+C7FOZbITuIW/neA+PhcjoKl7yMyB3XcmA4gw==}
|
||||
/@rollup/plugin-terser/0.4.0_rollup@3.7.4:
|
||||
resolution: {integrity: sha512-Ipcf3LPNerey1q9ZMjiaWHlNPEHNU/B5/uh9zXLltfEQ1lVSLLeZSgAtTPWGyw8Ip1guOeq+mDtdOlEj/wNxQw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
rollup: ^2.x || ^3.x
|
||||
@@ -1218,8 +1218,8 @@ packages:
|
||||
eslint: 8.29.0
|
||||
dev: true
|
||||
|
||||
/eslint-config-standard-with-typescript/27.0.1_f7skxvi3ubnnb5utlsc5vholvm:
|
||||
resolution: {integrity: sha512-jJVyJldqqiCu3uSA/FP0x9jCDMG+Bbl73twTSnp0aysumJrz4iaVqLl+tGgmPrv0R829GVs117Nmn47M1DDDXA==}
|
||||
/eslint-config-standard-with-typescript/34.0.0_f7skxvi3ubnnb5utlsc5vholvm:
|
||||
resolution: {integrity: sha512-zhCsI4/A0rJ1ma8sf3RLXYc0gc7yPmdTWRVXMh9dtqeUx3yBQyALH0wosHhk1uQ9QyItynLdNOtcHKNw8G7lQw==}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/eslint-plugin': ^5.0.0
|
||||
eslint: ^8.0.1
|
||||
|
||||
+9
-1
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"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
|
||||
|
||||
_This plugin requires a Rust version of at least **1.64**_
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user