Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions crates/rkvm-web/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,28 @@ pub fn App() -> impl IntoView {

let toggle_mouse = move |_| {
let next = !mouse_relative.get();
mouse_relative.set(next);
mouse_mode.get_value().set(next);
if !next {
mouse_relative.set(false);
mouse_mode.get_value().set(false);
return;
}
// Relative reports go to a separate USB function; without it the host
// receives nothing, so only switch when the device has it enabled.
let Some(client) = rpc_store.get_value() else { return };
spawn_local(async move {
match client.call("getUsbDevices", Value::Null).await {
Ok(v) if v.get("relative_mouse").and_then(Value::as_bool).unwrap_or(false) => {
error.set(None);
mouse_relative.set(true);
mouse_mode.get_value().set(true);
}
Ok(_) => error.set(Some(
"Relative mouse is disabled: enable it in ⚙ Settings → USB devices first"
.to_string(),
)),
Err(e) => error.set(Some(e.to_string())),
}
});
};

let do_reboot = move |_| {
Expand Down
64 changes: 48 additions & 16 deletions crates/rkvm-web/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ use crate::net::{rest, upload};
use crate::rpc::RpcClient;
use crate::util::{Rpc, as_string, fire, json_str};

/// Values are the codes `getKeyboardLayout` returns and `setKeyboardLayout`
/// accepts on the device.
const KEYBOARD_LAYOUTS: &[(&str, &str)] = &[
("en_US", "English (US)"),
("en_UK", "English (UK)"),
("de_DE", "German"),
("fr_FR", "French"),
("es_ES", "Spanish"),
("it_IT", "Italian"),
("ja_JP", "Japanese"),
("us", "English (US)"),
("uk", "English (UK)"),
("de", "German"),
("fr", "French"),
("es", "Spanish"),
("it", "Italian"),
("jp", "Japanese"),
("kr", "Korean"),
];

#[component]
Expand Down Expand Up @@ -109,7 +112,11 @@ pub fn Settings(
if let Ok(v) = client.call("getLocalVersion", Value::Null).await {
let app = json_str(&v, "app_version");
let sys = json_str(&v, "system_version");
version.set(format!("app {app} · system {sys}"));
version.set(if sys.is_empty() {
format!("app {app}")
} else {
format!("app {app} · system {sys}")
});
}
}),
Box::pin(async move {
Expand All @@ -119,13 +126,7 @@ pub fn Settings(
}),
Box::pin(async move {
if let Ok(v) = client.call("getNetworkState", Value::Null).await {
let ip = json_str(&v, "ip");
let online = v.get("online").and_then(Value::as_bool).unwrap_or(false);
network.set(format!(
"{} ({})",
if ip.is_empty() { "—" } else { &ip },
if online { "online" } else { "offline" }
));
network.set(describe_network(&v));
}
}),
Box::pin(async move {
Expand Down Expand Up @@ -262,7 +263,7 @@ pub fn Settings(
spawn_local(async move {
let _ = client.call("renewDHCPLease", Value::Null).await;
if let Ok(v) = client.call("getNetworkState", Value::Null).await {
network.set(json_str(&v, "ip"));
network.set(describe_network(&v));
}
});
};
Expand Down Expand Up @@ -779,6 +780,37 @@ impl UsbDevices {
}
}

/// One line for the Network section: address, link state, gateway and DNS.
/// The device can have several interfaces, so when this page was loaded from a
/// different address than the one reported, say so instead of looking wrong.
fn describe_network(v: &Value) -> String {
let ip = json_str(v, "ip");
let online = v.get("online").and_then(Value::as_bool).unwrap_or(false);
let mut line = format!(
"{} ({})",
if ip.is_empty() { "—" } else { &ip },
if online { "online" } else { "offline" }
);
let gateway = json_str(v, "gateway");
if !gateway.is_empty() {
line.push_str(&format!(" · gateway {gateway}"));
}
let dns: Vec<&str> = v
.get("dns")
.and_then(Value::as_array)
.map(|a| a.iter().filter_map(Value::as_str).collect())
.unwrap_or_default();
if !dns.is_empty() {
line.push_str(&format!(" · DNS {}", dns.join(", ")));
}
let page_host =
web_sys::window().and_then(|w| w.location().hostname().ok()).unwrap_or_default();
if !ip.is_empty() && !page_host.is_empty() && page_host != ip {
line.push_str(&format!(" · this page via {page_host}"));
}
line
}

fn describe_mount(v: &Value) -> String {
if v.is_null() {
return "none".to_string();
Expand Down
80 changes: 59 additions & 21 deletions crates/rkvm-web/src/settings_advanced.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ use crate::util::{Rpc, as_string, fire, json_str, json_string_list};
#[component]
pub fn AdvancedSettings(open: RwSignal<bool>, rpc: Rpc) -> impl IntoView {
let dc_enabled = RwSignal::new(false);
let dc_supported = RwSignal::new(false);
let dc_restore = RwSignal::new(String::new());
let dc_reading = RwSignal::new(String::new());
let atx_state = RwSignal::new(String::new());
// "dc-power", "atx-power" or "" (no power extension mounted).
let power_ext = RwSignal::new(String::new());
let cloud_state = RwSignal::new(String::new());
let cloud_api = RwSignal::new(String::new());
let cloud_app = RwSignal::new(String::new());
Expand All @@ -33,22 +36,52 @@ pub fn AdvancedSettings(open: RwSignal<bool>, rpc: Rpc) -> impl IntoView {
}
let Some(c) = rpc.get_value() else { return };
spawn_local(async move {
if let Ok(v) = c.call("getDCPowerState", Value::Null).await {
dc_enabled.set(v.get("enabled").and_then(Value::as_bool).unwrap_or(false));
dc_supported.set(v.get("supported").and_then(Value::as_bool).unwrap_or(false));
}
if let Ok(v) = c.call("getATXState", Value::Null).await {
atx_state.set(compact_json(&v));
// Power controls depend on which extension is mounted. Asking for
// ATX state while it is not mounted only logs an error on the device.
let ext = match c.call("getActiveExtension", Value::Null).await {
Ok(v) => as_string(&v),
Err(_) => String::new(),
};
match ext.as_str() {
"dc-power" => {
if let Ok(v) = c.call("getDCPowerState", Value::Null).await {
dc_enabled.set(v.get("isOn").and_then(Value::as_bool).unwrap_or(false));
dc_restore.set(
v.get("restoreState")
.and_then(Value::as_i64)
.map(|s| s.to_string())
.unwrap_or_default(),
);
let num = |k: &str| v.get(k).and_then(Value::as_f64).unwrap_or(0.0);
dc_reading.set(format!(
"{:.2} V · {:.2} A · {:.2} W",
num("voltage"),
num("current"),
num("power")
));
}
}
"atx-power" => match c.call("getATXState", Value::Null).await {
Ok(v) => {
let on = |k: &str| v.get(k).and_then(Value::as_bool).unwrap_or(false);
atx_state.set(format!(
"power LED {} · HDD LED {}",
if on("power") { "on" } else { "off" },
if on("hdd") { "on" } else { "off" }
));
}
Err(e) => atx_state.set(e.to_string()),
},
_ => {}
}
active_ext.set(ext.clone());
power_ext.set(ext);
if let Ok(v) = c.call("getCloudState", Value::Null).await {
cloud_state.set(compact_json(&v));
}
if let Ok(v) = c.call("getTailscaleControlURL", Value::Null).await {
ts_url.set(as_string(&v));
}
if let Ok(v) = c.call("getActiveExtension", Value::Null).await {
active_ext.set(as_string(&v));
}
if let Ok(v) = c.call("getDevModeState", Value::Null).await {
dev_mode.set(v.get("enabled").and_then(Value::as_bool).unwrap_or(false));
}
Expand Down Expand Up @@ -173,26 +206,31 @@ pub fn AdvancedSettings(open: RwSignal<bool>, rpc: Rpc) -> impl IntoView {
view! {
<section>
<h3>"Power"</h3>
<Show
when=move || dc_supported.get()
fallback=|| view! { <div class="muted">"DC power not supported"</div> }
>
<Show when=move || power_ext.get() == "dc-power">
<label class="toggle">
<input type="checkbox" prop:checked=move || dc_enabled.get() on:change=toggle_dc />
"DC power"
</label>
<select on:change=set_restore>
<div class="muted">{move || dc_reading.get()}</div>
<select on:change=set_restore prop:value=move || dc_restore.get()>
<option value="0">"Restore: off"</option>
<option value="1">"Restore: on"</option>
<option value="2">"Restore: last"</option>
</select>
</Show>
<div class="muted">"ATX: "{move || atx_state.get()}</div>
<div class="row">
<button on:click=move |_| atx("power-short")>"Power"</button>
<button on:click=move |_| atx("power-long")>"Force off"</button>
<button on:click=move |_| atx("reset")>"Reset"</button>
</div>
<Show when=move || power_ext.get() == "atx-power">
<div class="muted">"ATX: "{move || atx_state.get()}</div>
<div class="row">
<button on:click=move |_| atx("power-short")>"Power"</button>
<button on:click=move |_| atx("power-long")>"Force off"</button>
<button on:click=move |_| atx("reset")>"Reset"</button>
</div>
</Show>
<Show when=move || power_ext.get().is_empty()>
<div class="muted">
"No power extension active. Choose atx-power or dc-power under Extension to control host power."
</div>
</Show>
</section>

<section>
Expand Down
Loading