CLI self-updater, warn on version mismatch, direct install.sh option, androidmanifest and info.plist customization, bump to 0.7-alpha.0 (#4095)

* wip.... bun installer

* - warn on incompatible versions
- clean up some logs
- add coloring to log levels

* add toasts to desktop/mobile

* Auto open simulators

* auto boot android

* preserve url

* pull out some of the warnings

* add self-updater

* wip: mcp

* add install.sh and adjust binstall path

* add some cuter styling to logs!

* update "input.css"

* update other input.css

* style a bit more

* change getting started

* add windows installer

* move dioxus-mobile into dioxus-desktop
add sentry wip to cli

* shorten timeout of toast

* fix cli display

* wire up customization of android manifest

* udid is a real word

* add 429 to lychee cache

* accept all 200-204 and 429
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-05-14 08:43:11 +02:00
1 parent 53ef78823d
commit 4d3d55a7a3
56 files changed
+1560 -548

No files matched your search

+91 -13
View File
@@ -7,7 +7,8 @@ use crate::{
shortcut::ShortcutRegistry,
webview::WebviewInstance,
};
use dioxus_core::{ElementId, VirtualDom};
use dioxus_core::{ElementId, ScopeId, VirtualDom};
use dioxus_history::History;
use dioxus_html::PlatformEventData;
use std::{
any::Any,
@@ -15,6 +16,7 @@ use std::{
collections::HashMap,
rc::Rc,
sync::Arc,
time::Duration,
};
use tao::{
dpi::PhysicalSize,
@@ -168,12 +170,10 @@ impl App {
#[cfg(all(feature = "devtools", debug_assertions))]
pub fn connect_hotreload(&self) {
if let Some(endpoint) = dioxus_cli_config::devserver_ws_endpoint() {
let proxy = self.shared.proxy.clone();
dioxus_devtools::connect(endpoint, move |msg| {
_ = proxy.send_event(UserWindowEvent::HotReloadEvent(msg));
})
}
let proxy = self.shared.proxy.clone();
dioxus_devtools::connect(move |msg| {
_ = proxy.send_event(UserWindowEvent::HotReloadEvent(msg));
})
}
pub fn handle_new_window(&mut self) {
@@ -322,8 +322,14 @@ impl App {
#[cfg(all(feature = "devtools", debug_assertions))]
pub fn handle_hot_reload_msg(&mut self, msg: dioxus_devtools::DevserverMsg) {
use std::time::Duration;
use dioxus_devtools::DevserverMsg;
// Amount of time that toats should be displayed.
const TOAST_TIMEOUT: Duration = Duration::from_secs(2);
const TOAST_TIMEOUT_LONG: Duration = Duration::from_secs(3600); // Duration::MAX is too long for JS.
match msg {
DevserverMsg::HotReload(hr_msg) => {
for webview in self.webviews.values_mut() {
@@ -343,13 +349,49 @@ impl App {
webview.kick_stylsheets();
}
}
if hr_msg.jump_table.is_some()
&& hr_msg.for_build_id == Some(dioxus_cli_config::build_id())
{
self.send_toast_to_all(
"Hot-patch success!",
&format!("App successfully patched in {} ms", hr_msg.ms_elapsed),
"success",
TOAST_TIMEOUT,
false,
);
}
}
DevserverMsg::FullReloadCommand
| DevserverMsg::FullReloadStart
| DevserverMsg::FullReloadFailed => {
// usually only web gets this message - what are we supposed to do?
// Maybe we could just binary patch ourselves in place without losing window state?
DevserverMsg::FullReloadCommand => {
self.send_toast_to_all(
"Successfully rebuilt.",
"Your app was rebuilt successfully and without error.",
"success",
TOAST_TIMEOUT,
true,
);
}
DevserverMsg::FullReloadStart => self.send_toast_to_all(
"Your app is being rebuilt.",
"A non-hot-reloadable change occurred and we must rebuild.",
"info",
TOAST_TIMEOUT_LONG,
false,
),
DevserverMsg::FullReloadFailed => self.send_toast_to_all(
"Oops! The build failed.",
"We tried to rebuild your app, but something went wrong.",
"error",
TOAST_TIMEOUT_LONG,
false,
),
DevserverMsg::HotPatchStart => self.send_toast_to_all(
"Hot-patching app...",
"Hot-patching modified Rust code.",
"info",
TOAST_TIMEOUT_LONG,
false,
),
DevserverMsg::Shutdown => {
self.control_flow = ControlFlow::Exit;
}
@@ -357,6 +399,20 @@ impl App {
}
}
#[cfg(all(feature = "devtools", debug_assertions))]
fn send_toast_to_all(
&self,
header_text: &str,
message: &str,
level: &str,
duration: Duration,
after_reload: bool,
) {
for webview in self.webviews.values() {
webview.show_toast(header_text, message, level, duration, after_reload);
}
}
pub fn handle_file_dialog_msg(&mut self, msg: IpcMessage, window: WindowId) {
let Ok(file_dialog) = serde_json::from_value::<FileDialogRequest>(msg.params()) else {
return;
@@ -462,6 +518,9 @@ impl App {
#[cfg(debug_assertions)]
fn persist_window_state(&self) {
use dioxus_core::ScopeId;
use dioxus_history::History;
if let Some(webview) = self.webviews.values().next() {
let window = &webview.desktop_context.window;
@@ -491,12 +550,20 @@ impl App {
return;
};
let url = webview.dom.in_runtime(|| {
ScopeId::ROOT
.consume_context::<Rc<dyn History>>()
.unwrap()
.current_route()
});
let state = PreservedWindowState {
x,
y,
width: size.width.max(200),
height: size.height.saturating_sub(adjustment).max(200),
monitor: monitor_name.to_string(),
url: Some(url),
};
// Yes... I know... we're loading a file that might not be ours... but it's a debug feature
@@ -540,6 +607,16 @@ impl App {
if explicit_inner_size.is_none() {
window.set_inner_size(tao::dpi::PhysicalSize::new(size.0, size.1));
}
// Set the url if it exists
webview.dom.in_runtime(|| {
if let Some(url) = state.url {
ScopeId::ROOT
.consume_context::<Rc<dyn History>>()
.unwrap()
.replace(url);
}
})
}
}
}
@@ -563,7 +640,7 @@ impl App {
}
// give it a moment for the event to be processed
std::thread::sleep(std::time::Duration::from_secs(1));
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
});
@@ -578,6 +655,7 @@ struct PreservedWindowState {
width: u32,
height: u32,
monitor: String,
url: Option<String>,
}
/// Hide the last window when using LastWindowHides.
+184
View File
@@ -0,0 +1,184 @@
<!DOCTYPE html>
<html>
<head>
<title>Dioxus app</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<style>
/* Inter Font */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
#dx-toast-template {
display: none;
visibility: hidden;
}
.dx-toast {
position: absolute;
top: 10px;
right: 0;
padding-right: 10px;
user-select: none;
/* transition: transform 0.2s ease; */
z-index: 2147483647;
}
.dx-toast .dx-toast-inner {
/* transition: right 0.2s ease-out; */
position: fixed;
background-color: #181B20;
color: #ffffff;
font-family: "Inter", sans-serif;
display: grid;
grid-template-columns: auto auto;
max-width: 400px;
min-height: 56px;
border-radius: 5px;
}
.dx-toast .dx-toast-inner {
cursor: pointer;
margin-right: 10px;
}
.dx-toast .dx-toast-level-bar-container {
height: 100%;
width: 6px;
}
.dx-toast .dx-toast-level-bar-container .dx-toast-level-bar {
width: 100%;
height: 100%;
border-radius: 5px 0px 0px 5px;
}
.dx-toast .dx-toast-content {
padding: 8px;
}
.dx-toast .dx-toast-header {
display: flex;
flex-direction: row;
justify-content: start;
align-items: end;
margin-bottom: 10px;
}
.dx-toast .dx-toast-header>svg {
height: 18px;
margin-right: 5px;
}
.dx-toast .dx-toast-header .dx-toast-header-text {
font-size: 14px;
font-weight: 700;
padding: 0;
margin: 0;
}
.dx-toast .dx-toast-msg {
font-size: 11px;
font-weight: 400;
padding: 0;
margin: 0;
}
.dx-toast-level-bar.info {
background-color: #428EFF;
}
.dx-toast-level-bar.success {
background-color: #42FF65;
}
.dx-toast-level-bar.error {
background-color: #FF4242;
}
</style>
<script>
const STORAGE_KEY = "SCHEDULED-DX-TOAST";
let currentTimeout = null;
let currentToastId = 0;
// Show a toast, removing the previous one.
function showDXToast(headerText, message, progressLevel, durationMs) {
document.getElementById("__dx-toast-decor").className = `dx-toast-level-bar ${progressLevel}`;
document.getElementById("__dx-toast-text").innerText = headerText;
document.getElementById("__dx-toast-msg").innerText = message;
document.getElementById("__dx-toast-inner").style.right = "0";
document.getElementById("__dx-toast").addEventListener("click", closeDXToast);
// Wait a bit of time so animation plays correctly.
setTimeout(
() => {
let ourToastId = currentToastId;
currentTimeout = setTimeout(() => {
if (ourToastId == currentToastId) {
closeDXToast();
}
}, durationMs);
},
100
);
currentToastId += 1;
}
// Schedule a toast to be displayed after reload.
function scheduleDXToast(headerText, message, level, durationMs) {
let data = {
headerText,
message,
level,
durationMs,
};
let jsonData = JSON.stringify(data);
sessionStorage.setItem(STORAGE_KEY, jsonData);
}
// Close the current toast.
function closeDXToast() {
document.getElementById("__dx-toast-inner").style.right = "-1000px";
clearTimeout(currentTimeout);
}
// Handle any scheduled toasts after reload.
let potentialData = sessionStorage.getItem(STORAGE_KEY);
if (potentialData) {
sessionStorage.removeItem(STORAGE_KEY);
let data = JSON.parse(potentialData);
showDXToast(data.headerText, data.message, data.level, data.durationMs);
}
window.scheduleDXToast = scheduleDXToast;
window.showDXToast = showDXToast;
window.closeDXToast = closeDXToast;
</script>
<!-- CUSTOM HEAD -->
</head>
<body>
<div id="__dx-toast" class="dx-toast">
<div id="__dx-toast-inner" class="dx-toast-inner" style="right:-1000px;">
<div class="dx-toast-level-bar-container">
<div id="__dx-toast-decor" class="dx-toast-level-bar __info"></div>
</div>
<div class="dx-toast-content">
<div class="dx-toast-header">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" preserveAspectRatio="none">
<path d="M22.158 1.783c0 3.077-.851 5.482-2.215 7.377s-3.32 3.557-5.447 5.33-4.425 3.657-6.252 6.195-3.102 5.515-3.102 9.532h4.699c0-3.077.853-5.377 2.217-7.272s3.32-3.557 5.447-5.33 4.425-3.657 6.252-6.195 3.102-5.62 3.102-9.637z" fill="#e96020"/>
<path d="M9.531 25.927c-.635 0-1.021.515-1.02 1.15s.385 1.151 1.02 1.15H22.47a1.151 1.151 0 1 0 0-2.301zm1.361-4.076c-.608 0-.954.558-.953 1.166s.346 1.035.953 1.035h10.217a1.101 1.101 0 1 0 0-2.201zm0-13.594a1.101 1.101 0 1 0 0 2.201h10.217c.607 0 .953-.598.953-1.205s-.345-.996-.953-.996zM9.531 4.021A1.15 1.15 0 0 0 8.38 5.17a1.15 1.15 0 0 0 1.15 1.15h12.94c.635 0 1.021-.498 1.02-1.133s-.386-1.166-1.02-1.166z" fill="#2d323b"/>
<path d="M5.142 1.783c0 4.016 1.275 7.099 3.102 9.637s4.125 4.422 6.252 6.195 4.083 3.656 5.447 5.551 2.215 3.974 2.215 7.051h4.701c0-4.016-1.275-7.038-3.102-9.576s-4.125-4.422-6.252-6.195-4.083-3.435-5.447-5.33S9.841 4.86 9.841 1.783z" fill="#00a8d6"/>
</svg>
<h3 id="__dx-toast-text" class="dx-toast-header-text">Your app is being rebuilt.</h3>
</div>
<p id="__dx-toast-msg" class="dx-toast-msg">A non-hot-reloadable change occurred and we must rebuild.</p>
</div>
</div>
</div>
<div id="main"></div>
<!-- MODULE LOADER -->
</body>
</html>
File renamed without changes.
+105 -34
View File
@@ -8,6 +8,52 @@ use dioxus_document::eval;
use std::any::Any;
use tao::event::{Event, StartCause, WindowEvent};
/// Launches the WebView and runs the event loop
pub fn launch(root: fn() -> Element) {
launch_cfg(root, vec![], vec![]);
}
/// Launches the WebView and runs the event loop, with configuration and root props.
pub fn launch_cfg(
root: fn() -> Element,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
platform_config: Vec<Box<dyn Any>>,
) {
let mut virtual_dom = VirtualDom::new(root);
for context in contexts {
virtual_dom.insert_any_root_context(context());
}
let platform_config = *platform_config
.into_iter()
.find_map(|cfg| cfg.downcast::<Config>().ok())
.unwrap_or_default();
launch_virtual_dom(virtual_dom, platform_config)
}
/// Launches the WebView and runs the event loop, with configuration and root props.
pub fn launch_virtual_dom(virtual_dom: VirtualDom, desktop_config: Config) -> ! {
#[cfg(feature = "tokio_runtime")]
{
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(tokio::task::unconstrained(async move {
launch_virtual_dom_blocking(virtual_dom, desktop_config)
}));
unreachable!("The desktop launch function will never exit")
}
#[cfg(not(feature = "tokio_runtime"))]
{
launch_virtual_dom_blocking(virtual_dom, desktop_config);
}
}
/// Launch the WebView and run the event loop, with configuration and root props.
///
/// This will block the main thread, and *must* be spawned on the main thread. This function does not assume any runtime
@@ -111,43 +157,68 @@ pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, mut desktop_config:
})
}
/// Launches the WebView and runs the event loop, with configuration and root props.
pub fn launch_virtual_dom(virtual_dom: VirtualDom, desktop_config: Config) -> ! {
#[cfg(feature = "tokio_runtime")]
{
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(tokio::task::unconstrained(async move {
launch_virtual_dom_blocking(virtual_dom, desktop_config)
}));
unreachable!("The desktop launch function will never exit")
}
#[cfg(not(feature = "tokio_runtime"))]
{
launch_virtual_dom_blocking(virtual_dom, desktop_config);
}
/// Expose the `Java_dev_dioxus_main_WryActivity_create` function to the JNI layer.
/// We hardcode these to have a single trampoline for host Java code to call into.
///
/// This saves us from having to plumb the top-level package name all the way down into
/// this file. This is better for modularity (ie just call dioxus' main to run the app) as
/// well as cache thrashing since this crate doesn't rely on external env vars.
///
/// The CLI is expecting to find `dev.dioxus.main` in the final library. If you find a need to
/// change this, you'll need to change the CLI as well.
#[cfg(target_os = "android")]
#[no_mangle]
#[inline(never)]
pub extern "C" fn start_app() {
wry::android_binding!(dev_dioxus, main, wry);
tao::android_binding!(
dev_dioxus,
main,
WryActivity,
wry::android_setup,
dioxus_main_root_fn,
tao
);
}
/// Launches the WebView and runs the event loop, with configuration and root props.
pub fn launch(
root: fn() -> Element,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
platform_config: Vec<Box<dyn Any>>,
) -> ! {
let mut virtual_dom = VirtualDom::new(root);
#[cfg(target_os = "android")]
#[doc(hidden)]
pub fn dioxus_main_root_fn() {
// we're going to find the `main` symbol using dlsym directly and call it
unsafe {
let mut main_fn_ptr = libc::dlsym(libc::RTLD_DEFAULT, b"main\0".as_ptr() as _);
for context in contexts {
virtual_dom.insert_any_root_context(context());
}
if main_fn_ptr.is_null() {
main_fn_ptr = libc::dlsym(libc::RTLD_DEFAULT, b"_main\0".as_ptr() as _);
}
let platform_config = *platform_config
.into_iter()
.find_map(|cfg| cfg.downcast::<Config>().ok())
.unwrap_or_default();
if main_fn_ptr.is_null() {
panic!("Failed to find main symbol");
}
launch_virtual_dom(virtual_dom, platform_config)
// Set the env vars that rust code might expect, passed off to us by the android app
// Doing this before main emulates the behavior of a regular executable
if cfg!(target_os = "android") && cfg!(debug_assertions) {
load_env_file_from_session_cache();
}
let main_fn: extern "C" fn() = std::mem::transmute(main_fn_ptr);
main_fn();
};
}
/// Load the env file from the session cache if we're in debug mode and on android
///
/// This is a slightly hacky way of being able to use std::env::var code in android apps without
/// going through their custom java-based system.
#[cfg(target_os = "android")]
fn load_env_file_from_session_cache() {
let env_file = dioxus_cli_config::android_session_cache_dir().join(".env");
if let Some(env_file) = std::fs::read_to_string(&env_file).ok() {
for line in env_file.lines() {
if let Some((key, value)) = line.trim().split_once('=') {
std::env::set_var(key, value);
}
}
}
}
+5 -1
View File
@@ -19,7 +19,11 @@ const EVENTS_PATH: &str = "http://dioxus.index.html/__events";
#[cfg(not(any(target_os = "android", target_os = "windows")))]
const EVENTS_PATH: &str = "dioxus://index.html/__events";
static DEFAULT_INDEX: &str = include_str!("./index.html");
#[cfg(debug_assertions)]
static DEFAULT_INDEX: &str = include_str!("./assets/dev.index.html");
#[cfg(not(debug_assertions))]
static DEFAULT_INDEX: &str = include_str!("./assets/prod.index.html");
#[allow(clippy::too_many_arguments)] // just for now, should fix this eventually
/// Handle a request from the webview
+26 -1
View File
@@ -19,8 +19,8 @@ use dioxus_history::{History, MemoryHistory};
use dioxus_hooks::to_owned;
use dioxus_html::{HasFileData, HtmlEvent, PlatformEventData};
use futures_util::{pin_mut, FutureExt};
use std::cell::OnceCell;
use std::sync::Arc;
use std::{cell::OnceCell, time::Duration};
use std::{rc::Rc, task::Waker};
use wry::{DragDropEvent, RequestAsyncResponder, WebContext, WebViewBuilder};
@@ -480,6 +480,31 @@ impl WebviewInstance {
.webview
.evaluate_script("window.interpreter.kickAllStylesheetsOnPage()");
}
/// Displays a toast to the developer.
pub(crate) fn show_toast(
&self,
header_text: &str,
message: &str,
level: &str,
duration: Duration,
after_reload: bool,
) {
let as_ms = duration.as_millis();
let js_fn_name = match after_reload {
true => "scheduleDXToast",
false => "showDXToast",
};
_ = self.desktop_context.webview.evaluate_script(&format!(
r#"
if (typeof {js_fn_name} !== "undefined") {{
window.{js_fn_name}("{header_text}", "{message}", "{level}", {as_ms});
}}
"#,
));
}
}
/// A synchronous response to a browser event which may prevent the default browser's action