Port to tauri beta

This commit is contained in:
lights0123 2021-08-30 16:49:54 -04:00
parent 35cc07f0ed
commit eb961b8973
No known key found for this signature in database
GPG key ID: 28F315322E37972F
12 changed files with 2573 additions and 1656 deletions

View file

@ -15,7 +15,7 @@
"feather-icons": "^4.28.0", "feather-icons": "^4.28.0",
"filesize": "^6.1.0", "filesize": "^6.1.0",
"n-link-core": "0.0.0", "n-link-core": "0.0.0",
"tauri": "^0.12.0", "@tauri-apps/api": "^1.0.0-beta.8",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-async-computed": "^3.9.0", "vue-async-computed": "^3.9.0",
"vue-class-component": "^7.2.3", "vue-class-component": "^7.2.3",
@ -38,7 +38,7 @@
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",
"typescript": "~4.0.3", "typescript": "~4.0.3",
"vue-cli-plugin-tailwind": "~1.5.0", "vue-cli-plugin-tailwind": "~1.5.0",
"vue-cli-plugin-tauri": "~0.12.1", "vue-cli-plugin-tauri": "^1.0.0-beta.6",
"vue-template-compiler": "^2.6.11" "vue-template-compiler": "^2.6.11"
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -16,8 +16,7 @@ libnspire = "0.2.2"
lazy_static = "1.4.0" lazy_static = "1.4.0"
rusb = "0.6.4" rusb = "0.6.4"
serde = { version = "1.0", features = [ "derive" ] } serde = { version = "1.0", features = [ "derive" ] }
tauri = { version = "0.11", features = [ "event", "notification", "open" ] } tauri = { version = "1.0.0-beta.8", features = ["dialog-open", "dialog-save", "notification-all", "shell-open"] }
native-dialog = "0.4"
clap = "3.0.0-beta.2" clap = "3.0.0-beta.2"
indicatif = "0.15" indicatif = "0.15"
libusb1-sys = { version = "0.4.2", features = [ "vendored" ] } libusb1-sys = { version = "0.4.2", features = [ "vendored" ] }
@ -27,8 +26,8 @@ hashbrown = "0.11"
winres = "0.1" winres = "0.1"
[features] [features]
embedded-server = [ "tauri/embedded-server" ] custom-protocol = [ "tauri/custom-protocol" ]
no-server = [ "tauri/no-server" ] default = [ "custom-protocol" ]
[[bin]] [[bin]]
name = "n-link" name = "n-link"

View file

@ -5,7 +5,7 @@ extern crate winres;
fn main() { fn main() {
if std::path::Path::new("icons/icon.ico").exists() { if std::path::Path::new("icons/icon.ico").exists() {
let mut res = winres::WindowsResource::new(); let mut res = winres::WindowsResource::new();
res.set_icon("icons/icon.ico"); res.set_icon_with_id("icons/icon.ico", "32512");
res.compile().expect("Unable to find visual studio tools"); res.compile().expect("Unable to find visual studio tools");
} else { } else {
panic!("No Icon.ico found. Please add one or check the path"); panic!("No Icon.ico found. Please add one or check the path");

View file

@ -46,14 +46,13 @@ fn get_dev() -> Option<libnspire::Handle<rusb::GlobalContext>> {
rusb::devices() rusb::devices()
.unwrap() .unwrap()
.iter() .iter()
.filter(|dev| { .find(|dev| {
let descriptor = match dev.device_descriptor() { let descriptor = match dev.device_descriptor() {
Ok(d) => d, Ok(d) => d,
Err(_) => return false, Err(_) => return false,
}; };
descriptor.vendor_id() == VID && matches!(descriptor.product_id(), PID | PID_CX2) descriptor.vendor_id() == VID && matches!(descriptor.product_id(), PID | PID_CX2)
}) })
.next()
.map(|dev| libnspire::Handle::new(dev.open().unwrap()).unwrap()) .map(|dev| libnspire::Handle::new(dev.open().unwrap()).unwrap())
} }

View file

@ -4,16 +4,10 @@ use std::time::Duration;
use libnspire::{PID, PID_CX2, VID}; use libnspire::{PID, PID_CX2, VID};
use rusb::GlobalContext; use rusb::GlobalContext;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tauri::{Runtime, Window};
use crate::{Device, DeviceState}; use crate::{Device, DeviceState, SerializedError};
use tauri::WebviewMut;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Promise {
pub callback: String,
pub error: String,
}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)] #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
@ -21,131 +15,6 @@ pub struct DevId {
pub bus_number: u8, pub bus_number: u8,
pub address: u8, pub address: u8,
} }
#[derive(Debug, Deserialize)]
#[serde(tag = "cmd", rename_all = "camelCase")]
pub enum Cmd {
// your custom commands
// multiple arguments are allowed
// note that rename_all = "camelCase": you need to use "myCustomCommand" on JS
Enumerate {
#[serde(flatten)]
promise: Promise,
},
#[serde(rename_all = "camelCase")]
OpenDevice {
#[serde(flatten)]
promise: Promise,
#[serde(flatten)]
dev: DevId,
},
#[serde(rename_all = "camelCase")]
CloseDevice {
#[serde(flatten)]
promise: Promise,
#[serde(flatten)]
dev: DevId,
},
#[serde(rename_all = "camelCase")]
UpdateDevice {
#[serde(flatten)]
promise: Promise,
#[serde(flatten)]
dev: DevId,
},
#[serde(rename_all = "camelCase")]
ListDir {
#[serde(flatten)]
promise: Promise,
#[serde(flatten)]
dev: DevId,
path: String,
},
#[serde(rename_all = "camelCase")]
DownloadFile {
#[serde(flatten)]
promise: Promise,
#[serde(flatten)]
dev: DevId,
path: (String, u64),
dest: String,
},
#[serde(rename_all = "camelCase")]
UploadFile {
#[serde(flatten)]
promise: Promise,
#[serde(flatten)]
dev: DevId,
path: String,
src: String,
},
#[serde(rename_all = "camelCase")]
UploadOs {
#[serde(flatten)]
promise: Promise,
#[serde(flatten)]
dev: DevId,
src: String,
},
#[serde(rename_all = "camelCase")]
DeleteFile {
#[serde(flatten)]
promise: Promise,
#[serde(flatten)]
dev: DevId,
path: String,
},
#[serde(rename_all = "camelCase")]
DeleteDir {
#[serde(flatten)]
promise: Promise,
#[serde(flatten)]
dev: DevId,
path: String,
},
#[serde(rename_all = "camelCase")]
CreateNspireDir {
#[serde(flatten)]
promise: Promise,
#[serde(flatten)]
dev: DevId,
path: String,
},
#[serde(rename_all = "camelCase")]
Move {
#[serde(flatten)]
promise: Promise,
#[serde(flatten)]
dev: DevId,
src: String,
dest: String,
},
#[serde(rename_all = "camelCase")]
Copy {
#[serde(flatten)]
promise: Promise,
#[serde(flatten)]
dev: DevId,
src: String,
dest: String,
},
#[serde(rename_all = "camelCase")]
SelectFile {
#[serde(flatten)]
promise: Promise,
filter: Vec<String>,
},
#[serde(rename_all = "camelCase")]
SelectFiles {
#[serde(flatten)]
promise: Promise,
filter: Vec<String>,
},
#[serde(rename_all = "camelCase")]
SelectFolder {
#[serde(flatten)]
promise: Promise,
},
}
pub fn add_device(dev: Arc<rusb::Device<GlobalContext>>) -> rusb::Result<((u8, u8), Device)> { pub fn add_device(dev: Arc<rusb::Device<GlobalContext>>) -> rusb::Result<((u8, u8), Device)> {
let descriptor = dev.device_descriptor()?; let descriptor = dev.device_descriptor()?;
@ -185,7 +54,8 @@ pub fn add_device(dev: Arc<rusb::Device<GlobalContext>>) -> rusb::Result<((u8, u
)) ))
} }
pub fn enumerate(handle: &mut WebviewMut) -> Result<Vec<AddDevice>, libnspire::Error> { #[tauri::command]
pub fn enumerate<R: Runtime>(handle: Window<R>) -> Result<Vec<AddDevice>, SerializedError> {
let devices: Vec<_> = rusb::devices()?.iter().collect(); let devices: Vec<_> = rusb::devices()?.iter().collect();
let mut map = crate::DEVICES.write().unwrap(); let mut map = crate::DEVICES.write().unwrap();
map map
@ -195,13 +65,12 @@ pub fn enumerate(handle: &mut WebviewMut) -> Result<Vec<AddDevice>, libnspire::E
.all(|d| d.bus_number() != k.0 || d.address() != k.1) .all(|d| d.bus_number() != k.0 || d.address() != k.1)
}) })
.for_each(|d| { .for_each(|d| {
if let Err(msg) = tauri::event::emit( if let Err(msg) = handle.emit(
handle,
"removeDevice", "removeDevice",
Some(DevId { DevId {
bus_number: (d.0).0, bus_number: (d.0).0,
address: (d.0).1, address: (d.0).1,
}), },
) { ) {
eprintln!("{}", msg); eprintln!("{}", msg);
} }

View file

@ -3,25 +3,20 @@
windows_subsystem = "windows" windows_subsystem = "windows"
)] )]
use hashbrown::HashMap; use std::sync::atomic::{AtomicBool, Ordering};
use std::fs::File;
use std::io::{Read, Write};
use std::path::PathBuf;
use std::sync::{Arc, Mutex, RwLock}; use std::sync::{Arc, Mutex, RwLock};
use std::time::Duration; use std::time::Duration;
use libnspire::dir::EntryType; use hashbrown::HashMap;
use libnspire::{PID_CX2, VID}; use libnspire::{PID_CX2, VID};
use native_dialog::Dialog;
use rusb::{GlobalContext, Hotplug, UsbContext}; use rusb::{GlobalContext, Hotplug, UsbContext};
use tauri::WebviewMut; use serde::Serialize;
use tauri::{Runtime, Window};
use crate::cmd::{add_device, AddDevice, DevId, FileInfo, ProgressUpdate}; use crate::cmd::{add_device, AddDevice, DevId, ProgressUpdate};
use crate::promise::promise_fn;
mod cli; mod cli;
mod cmd; mod cmd;
mod promise;
pub enum DeviceState { pub enum DeviceState {
Open( Open(
@ -40,13 +35,13 @@ pub struct Device {
lazy_static::lazy_static! { lazy_static::lazy_static! {
static ref DEVICES: RwLock<HashMap<(u8, u8), Device>> = RwLock::new(HashMap::new()); static ref DEVICES: RwLock<HashMap<(u8, u8), Device>> = RwLock::new(HashMap::new());
} }
struct DeviceMon { struct DeviceMon<R: Runtime> {
handle: WebviewMut, window: Window<R>,
} }
impl Hotplug<GlobalContext> for DeviceMon { impl<R: Runtime> Hotplug<GlobalContext> for DeviceMon<R> {
fn device_arrived(&mut self, device: rusb::Device<GlobalContext>) { fn device_arrived(&mut self, device: rusb::Device<GlobalContext>) {
let mut handle = self.handle.clone(); let handle = self.window.clone();
let is_cx_ii = device let is_cx_ii = device
.device_descriptor() .device_descriptor()
.map(|d| d.product_id() == PID_CX2) .map(|d| d.product_id() == PID_CX2)
@ -58,10 +53,9 @@ impl Hotplug<GlobalContext> for DeviceMon {
let name = (dev.1).name.clone(); let name = (dev.1).name.clone();
let needs_drivers = (dev.1).needs_drivers; let needs_drivers = (dev.1).needs_drivers;
DEVICES.write().unwrap().insert(dev.0, dev.1); DEVICES.write().unwrap().insert(dev.0, dev.1);
if let Err(msg) = tauri::event::emit( if let Err(msg) = handle.emit(
&mut handle,
"addDevice", "addDevice",
Some(AddDevice { AddDevice {
dev: DevId { dev: DevId {
bus_number: (dev.0).0, bus_number: (dev.0).0,
address: (dev.0).1, address: (dev.0).1,
@ -69,7 +63,7 @@ impl Hotplug<GlobalContext> for DeviceMon {
name, name,
is_cx_ii, is_cx_ii,
needs_drivers, needs_drivers,
}), },
) { ) {
eprintln!("{}", msg); eprintln!("{}", msg);
}; };
@ -93,13 +87,12 @@ impl Hotplug<GlobalContext> for DeviceMon {
.unwrap() .unwrap()
.remove_entry(&(device.bus_number(), device.address())) .remove_entry(&(device.bus_number(), device.address()))
{ {
if let Err(msg) = tauri::event::emit( if let Err(msg) = self.window.emit(
&mut self.handle,
"removeDevice", "removeDevice",
Some(DevId { DevId {
bus_number: dev.0, bus_number: dev.0,
address: dev.1, address: dev.1,
}), },
) { ) {
eprintln!("{}", msg); eprintln!("{}", msg);
}; };
@ -107,42 +100,41 @@ impl Hotplug<GlobalContext> for DeviceMon {
} }
} }
fn err_wrap<T>( fn err_wrap<T, R: Runtime>(
res: Result<T, libnspire::Error>, res: Result<T, libnspire::Error>,
dev: DevId, dev: DevId,
handle: &mut WebviewMut, window: &Window<R>,
) -> Result<T, libnspire::Error> { ) -> Result<T, libnspire::Error> {
if let Err(libnspire::Error::NoDevice) = res { if let Err(libnspire::Error::NoDevice) = res {
DEVICES DEVICES
.write() .write()
.unwrap() .unwrap()
.remove(&(dev.bus_number, dev.address)); .remove(&(dev.bus_number, dev.address));
if let Err(msg) = tauri::event::emit(handle, "removeDevice", Some(dev)) { if let Err(msg) = window.emit("removeDevice", dev) {
eprintln!("{}", msg); eprintln!("{}", msg);
}; };
} }
res res
} }
fn progress_sender<'a>( fn progress_sender<R: Runtime>(
handle: &'a mut WebviewMut, window: &Window<R>,
dev: DevId, dev: DevId,
total: usize, total: usize,
) -> impl FnMut(usize) + 'a { ) -> impl FnMut(usize) + '_ {
let mut i = 0; let mut i = 0;
move |remaining| { move |remaining| {
if i > 5 { if i > 5 {
i = 0; i = 0;
} }
if i == 0 || remaining == 0 { if i == 0 || remaining == 0 {
if let Err(msg) = tauri::event::emit( if let Err(msg) = window.emit(
handle,
"progress", "progress",
Some(ProgressUpdate { ProgressUpdate {
dev, dev,
remaining, remaining,
total, total,
}), },
) { ) {
eprintln!("{}", msg); eprintln!("{}", msg);
}; };
@ -164,107 +156,92 @@ fn get_open_dev(
} }
} }
fn main() { #[derive(Serialize)]
if cli::run() { pub struct SerializedError(String);
return;
impl<T: std::fmt::Display> From<T> for SerializedError {
fn from(f: T) -> Self {
SerializedError(f.to_string())
} }
let mut has_registered_callback = false; }
tauri::AppBuilder::new()
.invoke_handler(move |webview, arg| { mod invoked {
use cmd::Cmd::*; use std::fs::File;
match serde_json::from_str(arg) { use std::io::{Read, Write};
Err(e) => Err(e.to_string()), use std::path::PathBuf;
Ok(command) => { use std::sync::{Arc, Mutex};
let mut wv_handle = webview.as_mut();
match command { use libnspire::dir::EntryType;
Enumerate { promise } => { use serde::Serialize;
if !has_registered_callback { use tauri::{Runtime, Window};
has_registered_callback = true;
if rusb::has_hotplug() { use crate::cmd::{DevId, FileInfo};
if let Err(msg) = GlobalContext::default().register_callback( use crate::{err_wrap, get_open_dev, progress_sender, DeviceState, SerializedError};
Some(VID),
None, use super::DEVICES;
None,
Box::new(DeviceMon { #[tauri::command]
handle: webview.as_mut(), pub fn open_device(bus_number: u8, address: u8) -> Result<impl Serialize, SerializedError> {
}), let device = if let Some(dev) = DEVICES.read().unwrap().get(&(bus_number, address)) {
) { if !matches!(dev.state, DeviceState::Closed) {
eprintln!("{}", msg); return Err("Already open".into());
}; };
std::thread::spawn(|| loop {
GlobalContext::default().handle_events(None).unwrap();
});
} else {
println!("no hotplug");
}
}
promise_fn(
webview,
move || Ok(cmd::enumerate(&mut wv_handle)?),
promise,
);
}
OpenDevice { promise, dev } => {
promise_fn(
webview,
move || {
let device = if let Some(dev) =
DEVICES.read().unwrap().get(&(dev.bus_number, dev.address))
{
anyhow::ensure!(matches!(dev.state, DeviceState::Closed), "Already open");
dev.device.clone() dev.device.clone()
} else { } else {
anyhow::bail!("Failed to find device"); return Err("Failed to find device".into());
}; };
let handle = libnspire::Handle::new(device.open()?)?; let handle = libnspire::Handle::new(device.open()?)?;
let info = handle.info()?; let info = handle.info()?;
{ {
let mut guard = DEVICES.write().unwrap(); let mut guard = DEVICES.write().unwrap();
let device = guard let device = guard
.get_mut(&(dev.bus_number, dev.address)) .get_mut(&(bus_number, address))
.ok_or_else(|| anyhow::anyhow!("Device lost"))?; .ok_or_else(|| anyhow::anyhow!("Device lost"))?;
device.state = DeviceState::Open(Arc::new(Mutex::new(handle)), info.clone()); device.state = DeviceState::Open(Arc::new(Mutex::new(handle)), info.clone());
} }
Ok(info) Ok(info)
},
promise,
);
} }
CloseDevice { promise, dev } => {
promise_fn( #[tauri::command]
webview, pub fn close_device(bus_number: u8, address: u8) -> Result<impl Serialize, SerializedError> {
move || {
{
let mut guard = DEVICES.write().unwrap(); let mut guard = DEVICES.write().unwrap();
let device = guard let device = guard
.get_mut(&(dev.bus_number, dev.address)) .get_mut(&(bus_number, address))
.ok_or_else(|| anyhow::anyhow!("Device lost"))?; .ok_or_else(|| anyhow::anyhow!("Device lost"))?;
device.state = DeviceState::Closed; device.state = DeviceState::Closed;
}
Ok(()) Ok(())
},
promise,
);
} }
UpdateDevice { promise, dev } => {
promise_fn( #[tauri::command]
webview, pub fn update_device<R: Runtime>(
move || { bus_number: u8,
address: u8,
window: Window<R>,
) -> Result<impl Serialize, SerializedError> {
let dev = DevId {
bus_number,
address,
};
let handle = get_open_dev(&dev)?; let handle = get_open_dev(&dev)?;
let handle = handle.lock().unwrap(); let handle = handle.lock().unwrap();
let info = err_wrap(handle.info(), dev, &mut wv_handle)?; let info = err_wrap(handle.info(), dev, &window)?;
Ok(info) Ok(info)
},
promise,
);
} }
ListDir { promise, dev, path } => {
promise_fn( #[tauri::command]
webview, pub fn list_dir<R: Runtime>(
move || { bus_number: u8,
address: u8,
path: String,
window: Window<R>,
) -> Result<impl Serialize, SerializedError> {
let dev = DevId {
bus_number,
address,
};
let handle = get_open_dev(&dev)?; let handle = get_open_dev(&dev)?;
let handle = handle.lock().unwrap(); let handle = handle.lock().unwrap();
let dir = err_wrap(handle.list_dir(&path), dev, &mut wv_handle)?; let dir = err_wrap(handle.list_dir(&path), dev, &window)?;
Ok( Ok(
dir dir
@ -277,19 +254,21 @@ fn main() {
}) })
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
) )
},
promise,
);
} }
DownloadFile {
promise, #[tauri::command]
dev, pub fn download_file<R: Runtime>(
path: (file, size), bus_number: u8,
dest, address: u8,
} => { path: (String, u64),
promise_fn( dest: String,
webview, window: Window<R>,
move || { ) -> Result<impl Serialize, SerializedError> {
let dev = DevId {
bus_number,
address,
};
let (file, size) = path;
let dest = PathBuf::from(dest); let dest = PathBuf::from(dest);
let handle = get_open_dev(&dev)?; let handle = get_open_dev(&dev)?;
let handle = handle.lock().unwrap(); let handle = handle.lock().unwrap();
@ -298,28 +277,29 @@ fn main() {
handle.read_file( handle.read_file(
&file, &file,
&mut buf, &mut buf,
&mut progress_sender(&mut wv_handle.clone(), dev, size as usize), &mut progress_sender(&window, dev, size as usize),
), ),
dev, dev,
&mut wv_handle, &window,
)?; )?;
if let Some(name) = file.split('/').last() { if let Some(name) = file.split('/').last() {
File::create(dest.join(name))?.write_all(&buf)?; File::create(dest.join(name))?.write_all(&buf)?;
} }
Ok(()) Ok(())
},
promise,
);
} }
UploadFile {
promise, #[tauri::command]
dev, pub fn upload_file<R: Runtime>(
path, bus_number: u8,
src, address: u8,
} => { path: String,
promise_fn( src: String,
webview, window: Window<R>,
move || { ) -> Result<impl Serialize, SerializedError> {
let dev = DevId {
bus_number,
address,
};
let file = PathBuf::from(src); let file = PathBuf::from(src);
let handle = get_open_dev(&dev)?; let handle = get_open_dev(&dev)?;
let handle = handle.lock().unwrap(); let handle = handle.lock().unwrap();
@ -334,151 +314,165 @@ fn main() {
handle.write_file( handle.write_file(
&format!("{}/{}", path, name), &format!("{}/{}", path, name),
&buf, &buf,
&mut progress_sender(&mut wv_handle.clone(), dev, buf.len()), &mut progress_sender(&window, dev, buf.len()),
), ),
dev, dev,
&mut wv_handle, &window,
)?; )?;
Ok(()) Ok(())
},
promise,
);
} }
UploadOs { promise, dev, src } => {
promise_fn( #[tauri::command]
webview, pub fn upload_os<R: Runtime>(
move || { bus_number: u8,
address: u8,
src: String,
window: Window<R>,
) -> Result<impl Serialize, SerializedError> {
let dev = DevId {
bus_number,
address,
};
let handle = get_open_dev(&dev)?; let handle = get_open_dev(&dev)?;
let handle = handle.lock().unwrap(); let handle = handle.lock().unwrap();
let mut buf = vec![]; let mut buf = vec![];
File::open(&src)?.read_to_end(&mut buf)?; File::open(&src)?.read_to_end(&mut buf)?;
err_wrap( err_wrap(
handle.send_os( handle.send_os(&buf, &mut progress_sender(&window, dev, buf.len())),
&buf,
&mut progress_sender(&mut wv_handle.clone(), dev, buf.len()),
),
dev, dev,
&mut wv_handle, &window,
)?; )?;
Ok(()) Ok(())
},
promise,
);
} }
DeleteFile { promise, dev, path } => {
promise_fn( #[tauri::command]
webview, pub fn delete_file<R: Runtime>(
move || { bus_number: u8,
address: u8,
path: String,
window: Window<R>,
) -> Result<impl Serialize, SerializedError> {
let dev = DevId {
bus_number,
address,
};
let handle = get_open_dev(&dev)?; let handle = get_open_dev(&dev)?;
let handle = handle.lock().unwrap(); let handle = handle.lock().unwrap();
err_wrap(handle.delete_file(&path), dev, &mut wv_handle)?; err_wrap(handle.delete_file(&path), dev, &window)?;
Ok(()) Ok(())
},
promise,
);
} }
DeleteDir { promise, dev, path } => {
promise_fn( #[tauri::command]
webview, pub fn delete_dir<R: Runtime>(
move || { bus_number: u8,
address: u8,
path: String,
window: Window<R>,
) -> Result<impl Serialize, SerializedError> {
let dev = DevId {
bus_number,
address,
};
let handle = get_open_dev(&dev)?; let handle = get_open_dev(&dev)?;
let handle = handle.lock().unwrap(); let handle = handle.lock().unwrap();
err_wrap(handle.delete_dir(&path), dev, &mut wv_handle)?; err_wrap(handle.delete_dir(&path), dev, &window)?;
Ok(()) Ok(())
},
promise,
);
} }
CreateNspireDir { promise, dev, path } => {
promise_fn( #[tauri::command]
webview, pub fn create_nspire_dir<R: Runtime>(
move || { bus_number: u8,
address: u8,
path: String,
window: Window<R>,
) -> Result<impl Serialize, SerializedError> {
let dev = DevId {
bus_number,
address,
};
let handle = get_open_dev(&dev)?; let handle = get_open_dev(&dev)?;
let handle = handle.lock().unwrap(); let handle = handle.lock().unwrap();
err_wrap(handle.create_dir(&path), dev, &mut wv_handle)?; err_wrap(handle.create_dir(&path), dev, &window)?;
Ok(()) Ok(())
},
promise,
);
} }
Move {
promise, #[tauri::command]
dev, pub fn move_file<R: Runtime>(
src, bus_number: u8,
dest, address: u8,
} => { src: String,
promise_fn( dest: String,
webview, window: Window<R>,
move || { ) -> Result<impl Serialize, SerializedError> {
let dev = DevId {
bus_number,
address,
};
let handle = get_open_dev(&dev)?; let handle = get_open_dev(&dev)?;
let handle = handle.lock().unwrap(); let handle = handle.lock().unwrap();
err_wrap(handle.move_file(&src, &dest), dev, &mut wv_handle)?; err_wrap(handle.move_file(&src, &dest), dev, &window)?;
Ok(()) Ok(())
},
promise,
);
} }
Copy {
promise, #[tauri::command]
dev, pub fn copy<R: Runtime>(
src, bus_number: u8,
dest, address: u8,
} => { src: String,
promise_fn( dest: String,
webview, window: Window<R>,
move || { ) -> Result<impl Serialize, SerializedError> {
let dev = DevId {
bus_number,
address,
};
let handle = get_open_dev(&dev)?; let handle = get_open_dev(&dev)?;
let handle = handle.lock().unwrap(); let handle = handle.lock().unwrap();
err_wrap(handle.copy_file(&src, &dest), dev, &mut wv_handle)?; err_wrap(handle.copy_file(&src, &dest), dev, &window)?;
Ok(())
},
promise,
);
}
SelectFile { promise, filter } => {
promise_fn(
webview,
move || {
let filter = filter.iter().map(|t| t.as_str()).collect::<Vec<_>>();
Ok(
(native_dialog::OpenSingleFile {
filter: Some(&filter),
dir: None,
})
.show()?,
)
},
promise,
);
}
SelectFiles { promise, filter } => {
promise_fn(
webview,
move || {
let filter = filter.iter().map(|t| t.as_str()).collect::<Vec<_>>();
Ok(
(native_dialog::OpenMultipleFile {
filter: Some(&filter),
dir: None,
})
.show()?,
)
},
promise,
);
}
SelectFolder { promise } => {
promise_fn(
webview,
move || Ok((native_dialog::OpenSingleDir { dir: None }).show()?),
promise,
);
}
}
Ok(()) Ok(())
} }
} }
})
.build() fn main() {
.run(); if cli::run() {
return;
}
let has_registered_callback = AtomicBool::new(false);
tauri::Builder::default()
.on_page_load(move |window, _p| {
if !has_registered_callback.swap(true, Ordering::SeqCst) {
if rusb::has_hotplug() {
if let Err(msg) = GlobalContext::default().register_callback(
Some(VID),
None,
None,
Box::new(DeviceMon { window }),
) {
eprintln!("{}", msg);
};
std::thread::spawn(|| loop {
GlobalContext::default().handle_events(None).unwrap();
});
} else {
println!("no hotplug");
}
}
})
.invoke_handler(tauri::generate_handler![
cmd::enumerate,
invoked::open_device,
invoked::close_device,
invoked::update_device,
invoked::list_dir,
invoked::download_file,
invoked::upload_file,
invoked::upload_os,
invoked::delete_file,
invoked::delete_dir,
invoked::create_nspire_dir,
invoked::move_file,
invoked::copy,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
} }

View file

@ -1,28 +0,0 @@
use crate::cmd::Promise;
use serde::Serialize;
use tauri::api::rpc::{format_callback, format_callback_result};
use tauri::Webview;
pub fn promise_fn<R: Serialize, F: FnOnce() -> tauri::Result<R> + Send + 'static>(
webview: &mut Webview<'_>,
task: F,
Promise {
callback: success_callback,
error: error_callback,
}: Promise,
) {
let mut webview = webview.as_mut();
std::thread::spawn(move || {
let callback_string = match format_callback_result(
task().map_err(|err| err.to_string()),
success_callback,
error_callback.clone(),
) {
Ok(callback_string) => callback_string,
Err(e) => format_callback(error_callback, e.to_string()),
};
webview
.dispatch(move |webview_ref| webview_ref.eval(callback_string.as_str()))
.expect("Failed to dispatch promise callback");
});
}

View file

@ -1,9 +1,5 @@
{ {
"ctx": {},
"tauri": { "tauri": {
"embeddedServer": {
"active": true
},
"bundle": { "bundle": {
"active": true, "active": true,
"targets": ["deb", "msi", "appimage", "dmg"], "targets": ["deb", "msi", "appimage", "dmg"],
@ -21,30 +17,42 @@
"category": "Utility", "category": "Utility",
"shortDescription": "Free, cross-platform, CX-II compatible computer linking program for the TI-Nspire", "shortDescription": "Free, cross-platform, CX-II compatible computer linking program for the TI-Nspire",
"longDescription": "Free, cross-platform, CX-II compatible computer linking program for the TI-Nspire", "longDescription": "Free, cross-platform, CX-II compatible computer linking program for the TI-Nspire",
"osx": { "macOS": {
"frameworks": [], "frameworks": [],
"minimumSystemVersion": "", "minimumSystemVersion": "",
"useBootstrapper": false "useBootstrapper": false,
"exceptionDomain": "",
"signingIdentity": null,
"entitlements": null
}, },
"exceptionDomain": "" "windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"updater": {
"active": false
}, },
"allowlist": { "allowlist": {
"event": true, "shell": {
"notification": true,
"open": true "open": true
}, },
"window": { "dialog": {
"open": true
}
},
"windows": [
{
"title": "N-Link", "title": "N-Link",
"width": 800, "width": 800,
"height": 600, "height": 600,
"resizable": true, "resizable": true,
"fullscreen": false "fullscreen": false
}, }
],
"security": { "security": {
"csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'" "csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'"
},
"inliner": {
"active": true
} }
} }
} }

View file

@ -1,5 +1,6 @@
import {promisified} from 'tauri/api/tauri'; import {invoke} from '@tauri-apps/api/tauri';
import {listen} from 'tauri/api/event'; import {listen} from '@tauri-apps/api/event';
import {open as openDialog} from '@tauri-apps/api/dialog';
import {Component, Vue} from 'vue-property-decorator'; import {Component, Vue} from 'vue-property-decorator';
import type {GenericDevices} from 'n-link-core/components/devices'; import type {GenericDevices} from 'n-link-core/components/devices';
@ -55,47 +56,47 @@ export type Device = { name: string; isCxIi: boolean; needsDrivers: boolean; inf
async function downloadFile(dev: DevId | string, path: [string, number], dest: string) { async function downloadFile(dev: DevId | string, path: [string, number], dest: string) {
if (typeof dev === 'string') dev = stringToDev(dev); if (typeof dev === 'string') dev = stringToDev(dev);
await promisified({...dev, cmd: 'downloadFile', path, dest}); await invoke('download_file', {...dev, path, dest});
} }
async function uploadFile(dev: DevId | string, path: string, src: string) { async function uploadFile(dev: DevId | string, path: string, src: string) {
if (typeof dev === 'string') dev = stringToDev(dev); if (typeof dev === 'string') dev = stringToDev(dev);
await promisified({...dev, cmd: 'uploadFile', path, src}); await invoke('upload_file', {...dev, path, src});
} }
async function uploadOs(dev: DevId | string, src: string) { async function uploadOs(dev: DevId | string, src: string) {
if (typeof dev === 'string') dev = stringToDev(dev); if (typeof dev === 'string') dev = stringToDev(dev);
await promisified({...dev, cmd: 'uploadOs', src}); await invoke('upload_os', {...dev, src});
} }
async function deleteFile(dev: DevId | string, path: string) { async function deleteFile(dev: DevId | string, path: string) {
if (typeof dev === 'string') dev = stringToDev(dev); if (typeof dev === 'string') dev = stringToDev(dev);
await promisified({...dev, cmd: 'deleteFile', path}); await invoke('delete_file', {...dev, path});
} }
async function deleteDir(dev: DevId | string, path: string) { async function deleteDir(dev: DevId | string, path: string) {
if (typeof dev === 'string') dev = stringToDev(dev); if (typeof dev === 'string') dev = stringToDev(dev);
await promisified({...dev, cmd: 'deleteDir', path}); await invoke('delete_dir', {...dev, path});
} }
async function createDir(dev: DevId | string, path: string) { async function createDir(dev: DevId | string, path: string) {
if (typeof dev === 'string') dev = stringToDev(dev); if (typeof dev === 'string') dev = stringToDev(dev);
await promisified({...dev, cmd: 'createNspireDir', path}); await invoke('create_nspire_dir', {...dev, path});
} }
async function move(dev: DevId | string, src: string, dest: string) { async function move(dev: DevId | string, src: string, dest: string) {
if (typeof dev === 'string') dev = stringToDev(dev); if (typeof dev === 'string') dev = stringToDev(dev);
await promisified({...dev, cmd: 'move', src, dest}); await invoke('move_file', {...dev, src, dest});
} }
async function copy(dev: DevId | string, src: string, dest: string) { async function copy(dev: DevId | string, src: string, dest: string) {
if (typeof dev === 'string') dev = stringToDev(dev); if (typeof dev === 'string') dev = stringToDev(dev);
await promisified({...dev, cmd: 'copy', src, dest}); await invoke('copy', {...dev, src, dest});
} }
async function listDir(dev: DevId | string, path: string) { async function listDir(dev: DevId | string, path: string) {
if (typeof dev === 'string') dev = stringToDev(dev); if (typeof dev === 'string') dev = stringToDev(dev);
return await promisified({...dev, cmd: 'listDir', path}) as FileInfo[]; return await invoke('list_dir', {...dev, path}) as FileInfo[];
} }
async function listAll(dev: DevId | string, path: FileInfo): Promise<FileInfo[]> { async function listAll(dev: DevId | string, path: FileInfo): Promise<FileInfo[]> {
@ -195,7 +196,7 @@ class Devices extends Vue implements GenericDevices {
async enumerate() { async enumerate() {
this.enumerating = true; this.enumerating = true;
try { try {
for (const dev of await promisified({cmd: 'enumerate'}) as (Device & DevId)[]) { for (const dev of await invoke('enumerate') as (Device & DevId)[]) {
this.$set(this.devices, devToString(dev as DevId), dev); this.$set(this.devices, devToString(dev as DevId), dev);
} }
} finally { } finally {
@ -205,19 +206,19 @@ class Devices extends Vue implements GenericDevices {
async open(dev: DevId | string) { async open(dev: DevId | string) {
if (typeof dev === 'string') dev = stringToDev(dev); if (typeof dev === 'string') dev = stringToDev(dev);
const info = await promisified({...dev, cmd: 'openDevice'}); const info = await invoke('open_device', {...dev});
this.$set(this.devices[devToString(dev)], 'info', info); this.$set(this.devices[devToString(dev)], 'info', info);
} }
async close(dev: DevId | string) { async close(dev: DevId | string) {
if (typeof dev === 'string') dev = stringToDev(dev); if (typeof dev === 'string') dev = stringToDev(dev);
await promisified({...dev, cmd: 'closeDevice'}); await invoke('close_device', {...dev});
this.$delete(this.devices[devToString(dev)], 'info'); this.$delete(this.devices[devToString(dev)], 'info');
} }
async update(dev: DevId | string) { async update(dev: DevId | string) {
if (typeof dev === 'string') dev = stringToDev(dev); if (typeof dev === 'string') dev = stringToDev(dev);
const info = await promisified({...dev, cmd: 'updateDevice'}); const info = await invoke('update_device', {...dev});
this.$set(this.devices[devToString(dev)], 'info', info); this.$set(this.devices[devToString(dev)], 'info', info);
} }
@ -227,7 +228,7 @@ class Devices extends Vue implements GenericDevices {
async promptUploadFiles(dev: DevId | string, path: string) { async promptUploadFiles(dev: DevId | string, path: string) {
if (typeof dev !== 'string') dev = devToString(dev); if (typeof dev !== 'string') dev = devToString(dev);
const files = await promisified({cmd: 'selectFiles', filter: ['tns']}) as string[]; const files = await openDialog({filters:[{extensions:['tns'], name:'TNS files'}], multiple: true});
for (const src of files) { for (const src of files) {
this.addToQueue(dev, {action: 'upload', path, src}); this.addToQueue(dev, {action: 'upload', path, src});
} }
@ -235,14 +236,14 @@ class Devices extends Vue implements GenericDevices {
async uploadOs(dev: DevId | string, filter: string) { async uploadOs(dev: DevId | string, filter: string) {
if (typeof dev !== 'string') dev = devToString(dev); if (typeof dev !== 'string') dev = devToString(dev);
const src = await promisified({cmd: 'selectFile', filter: [filter]}) as string | null; const src = await openDialog({filters:[{extensions:[filter], name:'TI Nspire OS upgrade files'}]}) as string | null;
if (!src) return; if (!src) return;
this.addToQueue(dev, {action: 'uploadOs', src}); this.addToQueue(dev, {action: 'uploadOs', src});
} }
async downloadFiles(dev: DevId | string, files: [string, number][]) { async downloadFiles(dev: DevId | string, files: [string, number][]) {
if (typeof dev !== 'string') dev = devToString(dev); if (typeof dev !== 'string') dev = devToString(dev);
const dest = await promisified({cmd: 'selectFolder'}) as string | null; const dest = await openDialog({directory: true}) as string | null;
if (!dest) return; if (!dest) return;
for (const path of files) { for (const path of files) {
this.addToQueue(dev, {action: 'download', path, dest}); this.addToQueue(dev, {action: 'download', path, dest});

View file

@ -42,7 +42,7 @@
<script lang="ts"> <script lang="ts">
import {Component, Vue, Watch} from 'vue-property-decorator'; import {Component, Vue, Watch} from 'vue-property-decorator';
import {open} from 'tauri/api/window'; import {open} from '@tauri-apps/api/shell';
import CalcInfo from 'n-link-core/components/CalcInfo.vue'; import CalcInfo from 'n-link-core/components/CalcInfo.vue';
import FileBrowser from 'n-link-core/components/FileBrowser.vue'; import FileBrowser from 'n-link-core/components/FileBrowser.vue';
import DeviceSelect from "n-link-core/components/DeviceSelect.vue"; import DeviceSelect from "n-link-core/components/DeviceSelect.vue";

1307
yarn.lock

File diff suppressed because it is too large Load diff