mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-09-20 21:20:42 +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"
|
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**_
|
||||||
|
|||||||
+2
-2
@@ -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(¤t_exe.display().to_string());
|
builder.set_app_path(¤t_exe.display().to_string());
|
||||||
#[cfg(target_os = "macos")]
|
#[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")]
|
#[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 });
|
||||||
|
|||||||
+36
-67
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-10
@@ -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)
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ 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 @@
|
|||||||

|

|
||||||
|
|
||||||
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
|
||||||
|
|||||||
+158
-115
@@ -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 {
|
|
||||||
/// 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,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ShowMode {
|
bitflags! {
|
||||||
fn default() -> Self {
|
pub struct StateFlags: u32 {
|
||||||
Self::LastSaved
|
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)]
|
#[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,66 +93,88 @@ 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 {
|
if flags.contains(StateFlags::SIZE) {
|
||||||
width: state.width,
|
self.set_size(LogicalSize {
|
||||||
height: state.height,
|
width: state.width,
|
||||||
})?;
|
height: state.height,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
// restore position to saved value if saved monitor exists
|
if flags.contains(StateFlags::POSITION) {
|
||||||
// otherwise, let the OS decide where to place the window
|
// restore position to saved value if saved monitor exists
|
||||||
for m in self.available_monitors()? {
|
// otherwise, let the OS decide where to place the window
|
||||||
if m.contains((state.x, state.y).into()) {
|
for m in self.available_monitors()? {
|
||||||
self.set_position(PhysicalPosition {
|
if m.contains((state.x, state.y).into()) {
|
||||||
x: state.x,
|
self.set_position(PhysicalPosition {
|
||||||
y: state.y,
|
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 scale_factor = self
|
let mut metadata = WindowState::default();
|
||||||
.current_monitor()?
|
|
||||||
.map(|m| m.scale_factor())
|
if flags.contains(StateFlags::SIZE) {
|
||||||
.unwrap_or(1.);
|
let scale_factor = self
|
||||||
let LogicalSize { width, height } = self.inner_size()?.to_logical(scale_factor);
|
.current_monitor()?
|
||||||
let PhysicalPosition { x, y } = self.outer_position()?;
|
.map(|m| m.scale_factor())
|
||||||
let maximized = self.is_maximized().unwrap_or(false);
|
.unwrap_or(1.);
|
||||||
let visible = self.is_visible().unwrap_or(true);
|
let size = self.inner_size()?.to_logical(scale_factor);
|
||||||
let decorated = self.is_decorated().unwrap_or(true);
|
metadata.width = size.width;
|
||||||
let fullscreen = self.is_fullscreen().unwrap_or(false);
|
metadata.height = size.height;
|
||||||
c.insert(
|
}
|
||||||
self.label().into(),
|
|
||||||
WindowMetadata {
|
if flags.contains(StateFlags::POSITION) {
|
||||||
width,
|
let pos = self.outer_position()?;
|
||||||
height,
|
metadata.x = pos.x;
|
||||||
x,
|
metadata.y = pos.y;
|
||||||
y,
|
}
|
||||||
maximized,
|
|
||||||
visible,
|
if flags.contains(StateFlags::MAXIMIZED) {
|
||||||
decorated,
|
metadata.maximized = self.is_maximized()?;
|
||||||
fullscreen,
|
}
|
||||||
},
|
|
||||||
);
|
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.show()?;
|
||||||
self.set_focus()?;
|
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)]
|
#[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| {
|
||||||
|
if let WindowEvent::CloseRequested { .. } = e {
|
||||||
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) {
|
||||||
let is_maximized = window_clone.is_maximized().unwrap_or(false);
|
let _ = window_clone.update_state(state, flags);
|
||||||
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();
|
|
||||||
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 {
|
if let RunEvent::Exit = event {
|
||||||
let _ = app.save_window_state();
|
let _ = app.save_window_state(flags);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
Generated
+8
-8
@@ -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
|
||||||
|
|||||||
+9
-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)
|
||||||
|
|||||||
Reference in New Issue
Block a user