feat(ui): migrate UI/Web framework to WRY

This commit is contained in:
liushuyu 2021-10-15 04:35:47 -06:00
parent 0d4022d348
commit 6e7d045794
No known key found for this signature in database
GPG key ID: 23D1CE4534419437
7 changed files with 1099 additions and 198 deletions

1153
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,8 @@ description = "An adaptable installer for your application."
build = "build.rs"
[dependencies]
web-view = { version = "0.7", features = ["edge"] }
anyhow = "^1"
wry = "0.12"
tinyfiledialogs = "3.8"
hyper = "0.11.27"

View file

@ -16,7 +16,7 @@ pub fn launch(app_name: &str, is_launcher: bool, framework: InstallerFramework)
let (servers, address) = rest::server::spawn_servers(framework.clone());
ui::start_ui(app_name, &address, is_launcher);
ui::start_ui(app_name, &address, is_launcher).log_expect("Failed to start UI");
// Explicitly hint that we want the servers instance until here.
drop(servers);

View file

@ -2,9 +2,16 @@
//!
//! Provides a web-view UI.
use web_view::Content;
use crate::logging::LoggingErrors;
use anyhow::Result;
use wry::{
application::{
dpi::LogicalSize,
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
},
webview::{RpcResponse, WebViewBuilder},
};
use log::Level;
@ -16,55 +23,56 @@ enum CallbackType {
}
/// Starts the main web UI. Will return when UI is closed.
pub fn start_ui(app_name: &str, http_address: &str, is_launcher: bool) {
pub fn start_ui(app_name: &str, http_address: &str, is_launcher: bool) -> Result<()> {
let size = if is_launcher { (600, 300) } else { (1024, 600) };
info!("Spawning web view instance");
web_view::builder()
.title(&format!("{} Installer", app_name))
.content(Content::Url(http_address))
.size(size.0, size.1)
.resizable(false)
.debug(cfg!(debug_assertions))
.user_data(())
.invoke_handler(|wv, msg| {
let mut cb_result = Ok(());
let command: CallbackType =
serde_json::from_str(msg).log_expect(&format!("Unable to parse string: {:?}", msg));
debug!("Incoming payload: {:?}", command);
match command {
CallbackType::SelectInstallDir { callback_name } => {
let result =
tinyfiledialogs::select_folder_dialog("Select a install directory...", "");
if let Some(new_path) = result {
if !new_path.is_empty() {
let result = serde_json::to_string(&new_path)
.log_expect("Unable to serialize response");
let command = format!("window.{}({});", callback_name, result);
debug!("Injecting response: {}", command);
cb_result = wv.eval(&command);
let event_loop = EventLoop::new();
let window = WindowBuilder::new()
.with_title(format!("{} Installer", app_name))
.with_inner_size(LogicalSize::new(size.0, size.1))
.with_resizable(false)
.build(&event_loop)?;
let _webview = WebViewBuilder::new(window)?
.with_url(http_address)?
.with_rpc_handler(|_, mut event| {
debug!("Incoming payload: {:?}", event);
match event.method.as_str() {
"Test" => (),
"Log" => {
if let Some(msg) = event.params.take() {
if let Ok(msg) = serde_json::from_value::<(String, String)>(msg) {
let kind = match msg.0.as_str() {
"info" | "log" => Level::Info,
"warn" => Level::Warn,
_ => Level::Error,
};
log!(target: "liftinstall::frontend::js", kind, "{}", msg.1);
}
}
}
CallbackType::Log { msg, kind } => {
let kind = match kind.as_ref() {
"info" | "log" => Level::Info,
"warn" => Level::Warn,
"error" => Level::Error,
_ => Level::Error,
};
log!(target: "liftinstall::frontend::js", kind, "{}", msg);
"SelectInstallDir" => {
let result =
tinyfiledialogs::select_folder_dialog("Select a install directory...", "")
.and_then(|v| serde_json::to_value(v).ok());
return Some(RpcResponse::new_result(event.id, result));
}
CallbackType::Test {} => {}
_ => warn!("Unknown RPC method: {}", event.method),
}
cb_result
None
})
.run()
.log_expect("Unable to launch Web UI!");
.build()?;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::NewEvents(StartCause::Init) => info!("Webview started"),
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
_ => (),
}
});
}

View file

@ -7,7 +7,7 @@
#![deny(unsafe_code)]
#![deny(missing_docs)]
extern crate web_view;
extern crate wry;
extern crate futures;
extern crate hyper;

View file

@ -25,13 +25,8 @@ export const i18n = new VueI18n({
function intercept (method) {
console[method] = function () {
const message = Array.prototype.slice.apply(arguments).join(' ')
window.external.invoke(
JSON.stringify({
Log: {
kind: method,
msg: message
}
})
window.rpc.notify(
'Log', method, message
)
}
}
@ -39,9 +34,7 @@ function intercept (method) {
// See if we have access to the JSON interface
let hasExternalInterface = false
try {
window.external.invoke(JSON.stringify({
Test: {}
}))
window.rpc.notify('Test')
hasExternalInterface = true
} catch (e) {
console.warn('Running without JSON interface - unexpected behaviour may occur!')
@ -50,13 +43,8 @@ try {
// Overwrite loggers with the logging backend
if (hasExternalInterface) {
window.onerror = function (msg, url, line) {
window.external.invoke(
JSON.stringify({
Log: {
kind: 'error',
msg: msg + ' @ ' + url + ':' + line
}
})
window.rpc.notify(
'Log', 'error', msg + ' @ ' + url + ':' + line
)
}
@ -91,12 +79,6 @@ axios.get('/api/attrs').then(function (resp) {
console.error(err)
})
function selectFileCallback (name) {
app.install_location = name
}
window.selectFileCallback = selectFileCallback
const app = new Vue({
i18n: i18n,
router: router,

View file

@ -80,7 +80,7 @@ export default {
data: function () {
return {
publicPath: process.env.BASE_URL,
advanced: false,
advanced: true,
repair: false,
installDesktopShortcut: true
}
@ -99,11 +99,12 @@ export default {
},
methods: {
select_file: function () {
window.external.invoke(JSON.stringify({
SelectInstallDir: {
callback_name: 'selectFileCallback'
const that = this
window.rpc.call('SelectInstallDir').then(function (name) {
if (name) {
that.$root.$data.install_location = name
}
}))
})
},
show_overwrite_dialog: function (confirmCallback) {
this.$buefy.dialog.confirm({