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" build = "build.rs"
[dependencies] [dependencies]
web-view = { version = "0.7", features = ["edge"] } anyhow = "^1"
wry = "0.12"
tinyfiledialogs = "3.8" tinyfiledialogs = "3.8"
hyper = "0.11.27" 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()); 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. // Explicitly hint that we want the servers instance until here.
drop(servers); drop(servers);

View file

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

View file

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

View file

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