feat(client/windows): console UI in the header, one Edit dialog per host
ci / web (push) Successful in 48s
ci / docs-site (push) Successful in 57s
apple / swift (push) Successful in 1m20s
decky / build-publish (push) Successful in 23s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 11s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Failing after 4m57s
ci / bench (push) Successful in 6m31s
ci / rust (push) Failing after 6m36s
apple / screenshots (push) Successful in 6m17s
deb / build-publish (push) Successful in 10m38s
flatpak / build-publish (push) Failing after 8m17s
docker / deploy-docs (push) Successful in 26s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Failing after 6m15s
deb / build-publish-host (push) Successful in 10m24s
arch / build-publish (push) Successful in 12m43s
android / android (push) Successful in 14m5s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 6m1s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m15s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m40s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 8m56s
ci / web (push) Successful in 48s
ci / docs-site (push) Successful in 57s
apple / swift (push) Successful in 1m20s
decky / build-publish (push) Successful in 23s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 11s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Failing after 4m57s
ci / bench (push) Successful in 6m31s
ci / rust (push) Failing after 6m36s
apple / screenshots (push) Successful in 6m17s
deb / build-publish (push) Successful in 10m38s
flatpak / build-publish (push) Failing after 8m17s
docker / deploy-docs (push) Successful in 26s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Failing after 6m15s
deb / build-publish-host (push) Successful in 10m24s
arch / build-publish (push) Successful in 12m43s
android / android (push) Successful in 14m5s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 6m1s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m15s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m40s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 8m56s
Three related bits of the same cleanup: - The console UI moves from a card in the page body to a button in the header row, beside Add host / Shortcuts / Settings. It also opens the console's OWN host view now (bare --browse) rather than diving straight into one host's library, which is what the card did -- the couch counterpart of this page, not a shortcut past it. - spawn_browse / open_console take an Option target to express that. - The per-host overflow menu collapses: rename and the two clipboard labels become one Edit dialog carrying every per-host property, mirroring the Apple client's add/edit sheet (name, address, port, Wake-on-LAN MAC, share clipboard). A menu item per field read as clutter and buried the entries that matter. Cleared fields mean leave-as-is rather than erase, except the MAC, which is legitimately clearable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -328,9 +328,12 @@ fn connect_spawn(
|
|||||||
/// PAIRED host in the session window. The shell yields exactly like a stream — hidden on
|
/// PAIRED host in the session window. The shell yields exactly like a stream — hidden on
|
||||||
/// the library window's `ready`, restored when the child exits (launched titles stream
|
/// the library window's `ready`, restored when the child exits (launched titles stream
|
||||||
/// in that same window, so the whole couch round-trip happens without the shell).
|
/// in that same window, so the whole couch round-trip happens without the shell).
|
||||||
|
/// `target = None` opens the console's own host view (discovery, pairing, settings) — the
|
||||||
|
/// couch entry point that isn't tied to one host; `Some` opens straight into that host's
|
||||||
|
/// library.
|
||||||
pub(crate) fn open_console(
|
pub(crate) fn open_console(
|
||||||
ctx: &Arc<AppCtx>,
|
ctx: &Arc<AppCtx>,
|
||||||
target: Target,
|
target: Option<Target>,
|
||||||
set_screen: &AsyncSetState<Screen>,
|
set_screen: &AsyncSetState<Screen>,
|
||||||
set_status: &AsyncSetState<String>,
|
set_status: &AsyncSetState<String>,
|
||||||
) {
|
) {
|
||||||
@@ -338,15 +341,21 @@ pub(crate) fn open_console(
|
|||||||
*ctx.shared.session.lock().unwrap() = child.clone();
|
*ctx.shared.session.lock().unwrap() = child.clone();
|
||||||
ctx.shared.stats_line.lock().unwrap().clear();
|
ctx.shared.stats_line.lock().unwrap().clear();
|
||||||
ctx.shared.browse.store(true, Ordering::SeqCst);
|
ctx.shared.browse.store(true, Ordering::SeqCst);
|
||||||
*ctx.shared.target.lock().unwrap() = target.clone();
|
if let Some(t) = target.clone() {
|
||||||
|
*ctx.shared.target.lock().unwrap() = t;
|
||||||
|
}
|
||||||
let fullscreen = ctx.settings.lock().unwrap().fullscreen_on_stream;
|
let fullscreen = ctx.settings.lock().unwrap().fullscreen_on_stream;
|
||||||
set_status.call(String::new());
|
set_status.call(String::new());
|
||||||
set_screen.call(Screen::Connecting);
|
set_screen.call(Screen::Connecting);
|
||||||
|
|
||||||
let shared = ctx.shared.clone();
|
let shared = ctx.shared.clone();
|
||||||
let (ss, st) = (set_screen.clone(), set_status.clone());
|
let (ss, st) = (set_screen.clone(), set_status.clone());
|
||||||
let spawned =
|
let addr_port = target.as_ref().map(|t| (t.addr.clone(), t.port));
|
||||||
crate::spawn::spawn_browse(&target.addr, target.port, fullscreen, child, move |event| {
|
let spawned = crate::spawn::spawn_browse(
|
||||||
|
addr_port.as_ref().map(|(a, p)| (a.as_str(), *p)),
|
||||||
|
fullscreen,
|
||||||
|
child,
|
||||||
|
move |event| {
|
||||||
use crate::spawn::SpawnEvent;
|
use crate::spawn::SpawnEvent;
|
||||||
match event {
|
match event {
|
||||||
SpawnEvent::Ready => {
|
SpawnEvent::Ready => {
|
||||||
@@ -364,7 +373,8 @@ pub(crate) fn open_console(
|
|||||||
ss.call(Screen::Hosts);
|
ss.call(Screen::Hosts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
if let Err(e) = spawned {
|
if let Err(e) = spawned {
|
||||||
set_status.call(e);
|
set_status.call(e);
|
||||||
set_screen.call(Screen::Hosts);
|
set_screen.call(Screen::Hosts);
|
||||||
|
|||||||
+183
-169
@@ -17,12 +17,10 @@ const MENU_LIBRARY: &str = "Browse library\u{2026}";
|
|||||||
const MENU_CONSOLE: &str = "Open console UI";
|
const MENU_CONSOLE: &str = "Open console UI";
|
||||||
const MENU_SPEED: &str = "Test network speed\u{2026}";
|
const MENU_SPEED: &str = "Test network speed\u{2026}";
|
||||||
const MENU_WAKE: &str = "Wake host";
|
const MENU_WAKE: &str = "Wake host";
|
||||||
/// The per-host clipboard opt-in (design/clipboard-and-file-transfer.md §5.3 — the Apple
|
/// One entry for every per-host property (name, address, MAC, clipboard sharing) — the
|
||||||
/// client's "Share clipboard with this host"). Two labels rather than a checkable item: the
|
/// Apple client's add/edit sheet. A menu item per field read as clutter and buried the ones
|
||||||
/// flyout reports the clicked item BY TEXT, so the item has to say which way it's going.
|
/// that matter.
|
||||||
const MENU_CLIP_ON: &str = "Share clipboard with this host";
|
const MENU_EDIT: &str = "Edit\u{2026}";
|
||||||
const MENU_CLIP_OFF: &str = "Stop sharing clipboard";
|
|
||||||
const MENU_RENAME: &str = "Rename\u{2026}";
|
|
||||||
const MENU_FORGET: &str = "Forget\u{2026}";
|
const MENU_FORGET: &str = "Forget\u{2026}";
|
||||||
|
|
||||||
/// Whether the console (gamepad) UI is available in this build: the session binary ships
|
/// Whether the console (gamepad) UI is available in this build: the session binary ships
|
||||||
@@ -192,43 +190,114 @@ fn status_row(online: Option<bool>, badge: &str, kind: Pill) -> Element {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The in-tile rename editor (ContentDialog can't hold a text field): name box + save/cancel.
|
/// The in-tile host editor (a ContentDialog can't hold text fields): every per-host
|
||||||
/// No tap-to-connect while editing — a click into the box would bubble `Tapped` to the region.
|
/// property in one place, mirroring the Apple client's add/edit sheet — name, address,
|
||||||
/// `initial` seeds the text box's displayed value and is CONSTANT for the life of the edit — the
|
/// port, Wake-on-LAN MAC, and whether this machine shares its clipboard with the host.
|
||||||
/// field is uncontrolled, its live value kept in `live` (read at Save). Driving a *controlled* box
|
/// Replaced a menu-item-per-property, which buried the useful entries in noise.
|
||||||
/// from an always-deferred `AsyncSetState` round-trip fights the caret on fast typing and can drop
|
///
|
||||||
/// the last char if Save is clicked before the write lands; an uncontrolled box + a ref sidesteps
|
/// Drafts live in refs owned by the page and are read at Save time; the root `edit` state
|
||||||
/// both (and skips a full-page re-render per keystroke). See the seed block in `hosts_page`.
|
/// carries only the target's fingerprint + initial name, so typing doesn't round-trip
|
||||||
fn rename_editor(
|
/// through a re-render.
|
||||||
initial: &str,
|
#[allow(clippy::too_many_arguments)]
|
||||||
fp: String,
|
fn edit_editor(
|
||||||
live: HookRef<String>,
|
fp: &str,
|
||||||
set_rename: AsyncSetState<Option<(String, String)>>,
|
initial_name: &str,
|
||||||
|
name_draft: HookRef<String>,
|
||||||
|
addr_draft: HookRef<String>,
|
||||||
|
port_draft: HookRef<String>,
|
||||||
|
mac_draft: HookRef<String>,
|
||||||
|
clip_draft: HookRef<bool>,
|
||||||
|
set_edit: AsyncSetState<Option<(String, String)>>,
|
||||||
) -> Element {
|
) -> Element {
|
||||||
let commit = {
|
let commit = {
|
||||||
let (fp, live, sr) = (fp.clone(), live.clone(), set_rename.clone());
|
let (fp, se) = (fp.to_string(), set_edit.clone());
|
||||||
|
let (name_draft, addr_draft, port_draft, mac_draft, clip_draft) = (
|
||||||
|
name_draft.clone(),
|
||||||
|
addr_draft.clone(),
|
||||||
|
port_draft.clone(),
|
||||||
|
mac_draft.clone(),
|
||||||
|
clip_draft.clone(),
|
||||||
|
);
|
||||||
move || {
|
move || {
|
||||||
let draft = live.borrow();
|
|
||||||
let name = draft.trim();
|
|
||||||
if !name.is_empty() {
|
|
||||||
let mut known = KnownHosts::load();
|
let mut known = KnownHosts::load();
|
||||||
if let Some(h) = known.hosts.iter_mut().find(|h| h.fp_hex == fp) {
|
if let Some(h) = known.hosts.iter_mut().find(|h| h.fp_hex == fp) {
|
||||||
h.name = name.to_string();
|
// Each field falls back to what was stored: a cleared box means "leave it",
|
||||||
|
// never "erase it" — except the MAC, which is legitimately clearable.
|
||||||
|
let name = name_draft.borrow().trim().to_string();
|
||||||
|
if !name.is_empty() {
|
||||||
|
h.name = name;
|
||||||
|
}
|
||||||
|
let addr = addr_draft.borrow().trim().to_string();
|
||||||
|
if !addr.is_empty() {
|
||||||
|
h.addr = addr;
|
||||||
|
}
|
||||||
|
if let Ok(p) = port_draft.borrow().trim().parse::<u16>() {
|
||||||
|
if p != 0 {
|
||||||
|
h.port = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mac = mac_draft.borrow().trim().to_string();
|
||||||
|
h.mac = if mac.is_empty() {
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
mac.split(&[',', ' '][..])
|
||||||
|
.filter(|m| !m.trim().is_empty())
|
||||||
|
.map(|m| m.trim().to_string())
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
h.clipboard_sync = *clip_draft.borrow();
|
||||||
}
|
}
|
||||||
let _ = known.save();
|
let _ = known.save();
|
||||||
}
|
se.call(None);
|
||||||
sr.call(None);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let on_changed = {
|
let field = |label: &str, value: String, placeholder: &str, draft: HookRef<String>| {
|
||||||
let live = live.clone();
|
vstack((
|
||||||
move |s: String| live.set(s)
|
text_block(label)
|
||||||
|
.font_size(12.0)
|
||||||
|
.foreground(ThemeRef::SecondaryText)
|
||||||
|
.horizontal_alignment(HorizontalAlignment::Left),
|
||||||
|
text_box(&value)
|
||||||
|
.placeholder_text(placeholder)
|
||||||
|
.on_text_changed(move |t: String| draft.set(t)),
|
||||||
|
))
|
||||||
|
.spacing(2.0)
|
||||||
};
|
};
|
||||||
|
let (name0, addr0, port0, mac0, clip0) = (
|
||||||
|
name_draft.borrow().clone(),
|
||||||
|
addr_draft.borrow().clone(),
|
||||||
|
port_draft.borrow().clone(),
|
||||||
|
mac_draft.borrow().clone(),
|
||||||
|
*clip_draft.borrow(),
|
||||||
|
);
|
||||||
|
let _ = initial_name;
|
||||||
card(
|
card(
|
||||||
vstack((
|
vstack((
|
||||||
text_box(initial)
|
field("Name", name0, "e.g. Living Room", name_draft),
|
||||||
.placeholder_text("Host name")
|
field("Address", addr0, "IP or hostname", addr_draft),
|
||||||
.on_text_changed(on_changed),
|
field("Port", port0, "9777", port_draft),
|
||||||
|
field(
|
||||||
|
"MAC (Wake-on-LAN)",
|
||||||
|
mac0,
|
||||||
|
"auto-filled when known",
|
||||||
|
mac_draft,
|
||||||
|
),
|
||||||
|
vstack((
|
||||||
|
ToggleSwitch::new(clip0)
|
||||||
|
.header("Share clipboard with this host")
|
||||||
|
.on_content("On")
|
||||||
|
.off_content("Off")
|
||||||
|
.on_toggled(move |v: bool| clip_draft.set(v)),
|
||||||
|
text_block(
|
||||||
|
"Copy on one machine, paste on the other. Off for every host until you \
|
||||||
|
turn it on here; the host must allow it too.",
|
||||||
|
)
|
||||||
|
.font_size(12.0)
|
||||||
|
.foreground(ThemeRef::SecondaryText)
|
||||||
|
.wrap()
|
||||||
|
.horizontal_alignment(HorizontalAlignment::Left),
|
||||||
|
))
|
||||||
|
.spacing(4.0),
|
||||||
hstack((
|
hstack((
|
||||||
button("Save")
|
button("Save")
|
||||||
.accent()
|
.accent()
|
||||||
@@ -236,7 +305,7 @@ fn rename_editor(
|
|||||||
.on_click(commit),
|
.on_click(commit),
|
||||||
button("Cancel")
|
button("Cancel")
|
||||||
.subtle()
|
.subtle()
|
||||||
.on_click(move || set_rename.call(None)),
|
.on_click(move || set_edit.call(None)),
|
||||||
))
|
))
|
||||||
.spacing(4.0),
|
.spacing(4.0),
|
||||||
))
|
))
|
||||||
@@ -269,16 +338,41 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
let rename = props.rename.clone();
|
let rename = props.rename.clone();
|
||||||
let set_forget = &props.set_forget;
|
let set_forget = &props.set_forget;
|
||||||
let set_rename = &props.set_rename;
|
let set_rename = &props.set_rename;
|
||||||
// The live rename draft, read at Save time (see `rename_editor`). Root `rename` carries only the
|
// The live edit drafts, read at Save time (see `edit_editor`). Root `rename` carries only
|
||||||
// INITIAL name, so it no longer round-trips per keystroke. Seed the draft each time the rename
|
// the target's fingerprint + initial name, so typing never round-trips through a
|
||||||
// TARGET changes (start, cancel, or a switch to another host).
|
// re-render. Every draft is re-seeded from the STORED host whenever the edit target
|
||||||
let rename_draft = cx.use_ref(String::new());
|
// changes (open, cancel, or switching to another host).
|
||||||
let rename_seed = cx.use_ref(Option::<String>::None);
|
let name_draft = cx.use_ref(String::new());
|
||||||
|
let addr_draft = cx.use_ref(String::new());
|
||||||
|
let port_draft = cx.use_ref(String::new());
|
||||||
|
let mac_draft = cx.use_ref(String::new());
|
||||||
|
let clip_draft = cx.use_ref(false);
|
||||||
|
let edit_seed = cx.use_ref(Option::<String>::None);
|
||||||
{
|
{
|
||||||
let active = rename.as_ref().map(|(fp, _)| fp.clone());
|
let active = rename.as_ref().map(|(fp, _)| fp.clone());
|
||||||
if *rename_seed.borrow() != active {
|
if *edit_seed.borrow() != active {
|
||||||
rename_draft.set(rename.as_ref().map(|(_, n)| n.clone()).unwrap_or_default());
|
let stored = active.as_ref().and_then(|fp| {
|
||||||
rename_seed.set(active);
|
KnownHosts::load()
|
||||||
|
.hosts
|
||||||
|
.into_iter()
|
||||||
|
.find(|h| &h.fp_hex == fp)
|
||||||
|
});
|
||||||
|
name_draft.set(stored.as_ref().map(|h| h.name.clone()).unwrap_or_default());
|
||||||
|
addr_draft.set(stored.as_ref().map(|h| h.addr.clone()).unwrap_or_default());
|
||||||
|
port_draft.set(
|
||||||
|
stored
|
||||||
|
.as_ref()
|
||||||
|
.map(|h| h.port.to_string())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
);
|
||||||
|
mac_draft.set(
|
||||||
|
stored
|
||||||
|
.as_ref()
|
||||||
|
.map(|h| h.mac.join(", "))
|
||||||
|
.unwrap_or_default(),
|
||||||
|
);
|
||||||
|
clip_draft.set(stored.as_ref().is_some_and(|h| h.clipboard_sync));
|
||||||
|
edit_seed.set(active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let hover = Hover {
|
let hover = Hover {
|
||||||
@@ -319,20 +413,51 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
.spacing(2.0)
|
.spacing(2.0)
|
||||||
.grid_column(0)
|
.grid_column(0)
|
||||||
.vertical_alignment(VerticalAlignment::Center),
|
.vertical_alignment(VerticalAlignment::Center),
|
||||||
hstack((
|
hstack({
|
||||||
header_btn("Add host", Symbol::Add).accent().on_click({
|
let mut actions: Vec<Element> = vec![header_btn("Add host", Symbol::Add)
|
||||||
|
.accent()
|
||||||
|
.on_click({
|
||||||
let sa = set_show_add.clone();
|
let sa = set_show_add.clone();
|
||||||
move || sa.call(true)
|
move || sa.call(true)
|
||||||
}),
|
})
|
||||||
header_btn("Shortcuts", Symbol::Keyboard).on_click({
|
.into()];
|
||||||
|
// The couch UI's front door, beside the other page actions. Absent on ARM64,
|
||||||
|
// where the session binary ships without its Skia console.
|
||||||
|
if CONSOLE_UI_AVAILABLE {
|
||||||
|
actions.push(
|
||||||
|
header_btn("Console UI", Symbol::Play)
|
||||||
|
.tooltip(
|
||||||
|
"The controller-driven couch interface \u{2014} host list, \
|
||||||
|
pairing and libraries, launching streams in the same window.",
|
||||||
|
)
|
||||||
|
.on_click({
|
||||||
|
let (c, ss, st) =
|
||||||
|
(ctx.clone(), set_screen.clone(), set_status.clone());
|
||||||
|
// No target: the console opens its OWN host view rather than
|
||||||
|
// one host's library — the couch counterpart of this page.
|
||||||
|
move || open_console(&c, None, &ss, &st)
|
||||||
|
})
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
actions.push(
|
||||||
|
header_btn("Shortcuts", Symbol::Keyboard)
|
||||||
|
.on_click({
|
||||||
let ss = set_screen.clone();
|
let ss = set_screen.clone();
|
||||||
move || ss.call(Screen::Help)
|
move || ss.call(Screen::Help)
|
||||||
}),
|
})
|
||||||
header_btn("Settings", Symbol::Setting).on_click({
|
.into(),
|
||||||
|
);
|
||||||
|
actions.push(
|
||||||
|
header_btn("Settings", Symbol::Setting)
|
||||||
|
.on_click({
|
||||||
let ss = set_screen.clone();
|
let ss = set_screen.clone();
|
||||||
move || ss.call(Screen::Settings)
|
move || ss.call(Screen::Settings)
|
||||||
}),
|
})
|
||||||
))
|
.into(),
|
||||||
|
);
|
||||||
|
actions
|
||||||
|
})
|
||||||
.spacing(8.0)
|
.spacing(8.0)
|
||||||
.grid_column(1)
|
.grid_column(1)
|
||||||
.vertical_alignment(VerticalAlignment::Center),
|
.vertical_alignment(VerticalAlignment::Center),
|
||||||
@@ -352,104 +477,23 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The couch entry point, for the most recently used paired + REACHABLE host (an offline
|
|
||||||
// one would just open the console onto an error scene).
|
|
||||||
//
|
|
||||||
// Deliberately NOT gated on a controller being connected any more: it used to be, which
|
|
||||||
// meant that with no pad plugged in the gamepad UI had no visible entry point on this
|
|
||||||
// page at all — its only other door is the per-host overflow menu, behind a "…" nobody
|
|
||||||
// opens. The card now always shows (the copy below adapts), so the feature is findable
|
|
||||||
// before you own the hardware for it.
|
|
||||||
if CONSOLE_UI_AVAILABLE {
|
|
||||||
let reachable = |k: &&crate::trust::KnownHost| {
|
|
||||||
hosts
|
|
||||||
.iter()
|
|
||||||
.any(|h| h.fp_hex == k.fp_hex || (h.addr == k.addr && h.port == k.port))
|
|
||||||
|| props.probed.get(&k.fp_hex).copied().unwrap_or(false)
|
|
||||||
};
|
|
||||||
if let Some(k) = known
|
|
||||||
.hosts
|
|
||||||
.iter()
|
|
||||||
.filter(|h| h.paired)
|
|
||||||
.filter(reachable)
|
|
||||||
.max_by_key(|h| h.last_used.unwrap_or(0))
|
|
||||||
{
|
|
||||||
let target = Target {
|
|
||||||
name: k.name.clone(),
|
|
||||||
addr: k.addr.clone(),
|
|
||||||
port: k.port,
|
|
||||||
fp_hex: Some(k.fp_hex.clone()),
|
|
||||||
pair_optional: false,
|
|
||||||
mac: k.mac.clone(),
|
|
||||||
};
|
|
||||||
let svc = props.svc.clone();
|
|
||||||
body.push(
|
|
||||||
card(
|
|
||||||
grid((
|
|
||||||
vstack((
|
|
||||||
text_block(if props.pads > 0 {
|
|
||||||
"Controller detected"
|
|
||||||
} else {
|
|
||||||
"Console UI"
|
|
||||||
})
|
|
||||||
.font_size(14.0)
|
|
||||||
.semibold(),
|
|
||||||
text_block(if props.pads > 0 {
|
|
||||||
format!(
|
|
||||||
"Browse {}\u{2019}s game library with the gamepad \u{2014} \
|
|
||||||
launches stream in the same window.",
|
|
||||||
k.name
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
format!(
|
|
||||||
"The couch interface for {} \u{2014} a controller- and \
|
|
||||||
remote-friendly library that launches streams in the same \
|
|
||||||
window. Works with keyboard arrows too.",
|
|
||||||
k.name
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.font_size(12.0)
|
|
||||||
.wrap()
|
|
||||||
.foreground(ThemeRef::SecondaryText),
|
|
||||||
))
|
|
||||||
.spacing(2.0)
|
|
||||||
.grid_column(0)
|
|
||||||
.vertical_alignment(VerticalAlignment::Center),
|
|
||||||
button("Open console UI")
|
|
||||||
.accent()
|
|
||||||
.icon(Symbol::Play)
|
|
||||||
.on_click(move || {
|
|
||||||
open_console(
|
|
||||||
&svc.ctx,
|
|
||||||
target.clone(),
|
|
||||||
&svc.set_screen,
|
|
||||||
&svc.set_status,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.grid_column(1)
|
|
||||||
.vertical_alignment(VerticalAlignment::Center)
|
|
||||||
.margin(edges(12.0, 0.0, 0.0, 0.0)),
|
|
||||||
))
|
|
||||||
.columns([GridLength::Star(1.0), GridLength::Auto]),
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Saved (trusted/paired) hosts — reachable even when mDNS isn't. A saved host that's also
|
// Saved (trusted/paired) hosts — reachable even when mDNS isn't. A saved host that's also
|
||||||
// being advertised right now shows as Online (and is deduped out of the discovery section).
|
// being advertised right now shows as Online (and is deduped out of the discovery section).
|
||||||
if !known.hosts.is_empty() {
|
if !known.hosts.is_empty() {
|
||||||
body.push(section("SAVED HOSTS"));
|
body.push(section("SAVED HOSTS"));
|
||||||
let mut tiles: Vec<Element> = Vec::new();
|
let mut tiles: Vec<Element> = Vec::new();
|
||||||
for k in &known.hosts {
|
for k in &known.hosts {
|
||||||
// Rust 2021 (no let-chains): match the "this tile is being renamed" case explicitly.
|
// Rust 2021 (no let-chains): match the "this tile is being edited" case explicitly.
|
||||||
if matches!(&rename, Some((fp, _)) if fp == &k.fp_hex) {
|
if matches!(&rename, Some((fp, _)) if fp == &k.fp_hex) {
|
||||||
let (fp, initial) = rename.clone().unwrap();
|
let (fp, initial) = rename.clone().unwrap();
|
||||||
tiles.push(rename_editor(
|
tiles.push(edit_editor(
|
||||||
|
&fp,
|
||||||
&initial,
|
&initial,
|
||||||
fp,
|
name_draft.clone(),
|
||||||
rename_draft.clone(),
|
addr_draft.clone(),
|
||||||
|
port_draft.clone(),
|
||||||
|
mac_draft.clone(),
|
||||||
|
clip_draft.clone(),
|
||||||
set_rename.clone(),
|
set_rename.clone(),
|
||||||
));
|
));
|
||||||
continue;
|
continue;
|
||||||
@@ -481,10 +525,6 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
let menu = {
|
let menu = {
|
||||||
let (svc, target) = (props.svc.clone(), target.clone());
|
let (svc, target) = (props.svc.clone(), target.clone());
|
||||||
let (sf, sr) = (set_forget.clone(), set_rename.clone());
|
let (sf, sr) = (set_forget.clone(), set_rename.clone());
|
||||||
// Re-render lever for the clipboard toggle: `hover` is a value field, so
|
|
||||||
// clearing it flips the menu label to its opposite immediately (and dropping
|
|
||||||
// the hover highlight after a menu action is right regardless).
|
|
||||||
let sh = props.set_hover.clone();
|
|
||||||
let (fp, name) = (k.fp_hex.clone(), k.name.clone());
|
let (fp, name) = (k.fp_hex.clone(), k.name.clone());
|
||||||
button("")
|
button("")
|
||||||
.icon(Symbol::More)
|
.icon(Symbol::More)
|
||||||
@@ -500,22 +540,12 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
if library_enabled && k.paired {
|
if library_enabled && k.paired {
|
||||||
items.push(menu_item(MENU_LIBRARY));
|
items.push(menu_item(MENU_LIBRARY));
|
||||||
}
|
}
|
||||||
if CONSOLE_UI_AVAILABLE && k.paired {
|
|
||||||
items.push(menu_item(MENU_CONSOLE));
|
|
||||||
}
|
|
||||||
if k.paired {
|
|
||||||
items.push(menu_item(if k.clipboard_sync {
|
|
||||||
MENU_CLIP_OFF
|
|
||||||
} else {
|
|
||||||
MENU_CLIP_ON
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
items.push(menu_item(MENU_SPEED));
|
items.push(menu_item(MENU_SPEED));
|
||||||
// Offer an explicit wake only when the host is offline and we have a MAC.
|
// Offer an explicit wake only when the host is offline and we have a MAC.
|
||||||
if can_wake {
|
if can_wake {
|
||||||
items.push(menu_item(MENU_WAKE));
|
items.push(menu_item(MENU_WAKE));
|
||||||
}
|
}
|
||||||
items.push(menu_item(MENU_RENAME));
|
items.push(menu_item(MENU_EDIT));
|
||||||
items.push(menu_separator());
|
items.push(menu_separator());
|
||||||
items.push(menu_item(MENU_FORGET));
|
items.push(menu_item(MENU_FORGET));
|
||||||
items
|
items
|
||||||
@@ -529,23 +559,7 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
super::library::start_fetch(&svc.ctx, &svc.set_library);
|
super::library::start_fetch(&svc.ctx, &svc.set_library);
|
||||||
svc.set_screen.call(Screen::Library);
|
svc.set_screen.call(Screen::Library);
|
||||||
}
|
}
|
||||||
MENU_CONSOLE => {
|
|
||||||
open_console(&svc.ctx, target.clone(), &svc.set_screen, &svc.set_status)
|
|
||||||
}
|
|
||||||
MENU_WAKE => crate::wol::wake(&target.mac, target.addr.parse().ok()),
|
MENU_WAKE => crate::wol::wake(&target.mac, target.addr.parse().ok()),
|
||||||
MENU_CLIP_ON | MENU_CLIP_OFF => {
|
|
||||||
let mut known = KnownHosts::load();
|
|
||||||
if let Some(h) = known.hosts.iter_mut().find(|h| h.fp_hex == fp) {
|
|
||||||
h.clipboard_sync = !h.clipboard_sync;
|
|
||||||
tracing::info!(
|
|
||||||
host = %h.name,
|
|
||||||
on = h.clipboard_sync,
|
|
||||||
"clipboard sharing toggled for host"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
known.save();
|
|
||||||
sh.call(None);
|
|
||||||
}
|
|
||||||
MENU_SPEED => {
|
MENU_SPEED => {
|
||||||
*svc.ctx.shared.target.lock().unwrap() = target.clone();
|
*svc.ctx.shared.target.lock().unwrap() = target.clone();
|
||||||
// New run: invalidate any still-in-flight probe, reset the screen.
|
// New run: invalidate any still-in-flight probe, reset the screen.
|
||||||
@@ -556,7 +570,7 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
|
|||||||
svc.set_speed.call(SpeedState::Running);
|
svc.set_speed.call(SpeedState::Running);
|
||||||
svc.set_screen.call(Screen::SpeedTest);
|
svc.set_screen.call(Screen::SpeedTest);
|
||||||
}
|
}
|
||||||
MENU_RENAME => sr.call(Some((fp.clone(), name.clone()))),
|
MENU_EDIT => sr.call(Some((fp.clone(), name.clone()))),
|
||||||
MENU_FORGET => sf.call(Some((fp.clone(), name.clone()))),
|
MENU_FORGET => sf.call(Some((fp.clone(), name.clone()))),
|
||||||
_ => {}
|
_ => {}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -126,21 +126,26 @@ pub(crate) fn spawn_session(
|
|||||||
/// The same stdout contract as a connect (`--json-status`): `ready` when the library
|
/// The same stdout contract as a connect (`--json-status`): `ready` when the library
|
||||||
/// window presents, `error` on a failed start, EOF on quit.
|
/// window presents, `error` on a failed start, EOF on quit.
|
||||||
pub(crate) fn spawn_browse(
|
pub(crate) fn spawn_browse(
|
||||||
addr: &str,
|
target: Option<(&str, u16)>,
|
||||||
port: u16,
|
|
||||||
fullscreen: bool,
|
fullscreen: bool,
|
||||||
slot: SessionChild,
|
slot: SessionChild,
|
||||||
on_event: impl FnMut(SpawnEvent) + Send + 'static,
|
on_event: impl FnMut(SpawnEvent) + Send + 'static,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let mut cmd = Command::new(session_binary());
|
let mut cmd = Command::new(session_binary());
|
||||||
cmd.arg("--browse")
|
cmd.arg("--browse");
|
||||||
.arg(format!("{addr}:{port}"))
|
// A target opens straight into that host's library; bare `--browse` opens the console's
|
||||||
.arg("--json-status");
|
// OWN host view (discovery, pairing, settings, Wake-on-LAN) — the couch equivalent of
|
||||||
|
// the shell's hosts page.
|
||||||
|
if let Some((addr, port)) = target {
|
||||||
|
cmd.arg(format!("{addr}:{port}"));
|
||||||
|
}
|
||||||
|
cmd.arg("--json-status");
|
||||||
if fullscreen {
|
if fullscreen {
|
||||||
cmd.arg("--fullscreen");
|
cmd.arg("--fullscreen");
|
||||||
}
|
}
|
||||||
add_window_pos(&mut cmd);
|
add_window_pos(&mut cmd);
|
||||||
spawn_with(cmd, &format!("{addr}:{port}"), slot, on_event)
|
let label = target.map_or_else(|| "console".to_string(), |(a, p)| format!("{a}:{p}"));
|
||||||
|
spawn_with(cmd, &label, slot, on_event)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hand the shell window's position to the child (`--window-pos`) so the session window
|
/// Hand the shell window's position to the child (`--window-pos`) so the session window
|
||||||
|
|||||||
Reference in New Issue
Block a user