feat(clients): unified full-screen connect/wake takeover + iOS/tvOS Wake-on-LAN
ci / web (push) Successful in 54s
ci / docs-site (push) Successful in 1m8s
decky / build-publish (push) Successful in 20s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
android / android (push) Successful in 12m23s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
ci / bench (push) Successful in 6m3s
arch / build-publish (push) Successful in 12m57s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9m40s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7m4s
docker / deploy-docs (push) Successful in 11s
flatpak / build-publish (push) Successful in 5m57s
deb / build-publish (push) Successful in 13m26s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m50s
apple / swift (push) Successful in 4m31s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m8s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m44s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m7s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m9s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m45s
release / apple (push) Successful in 21m27s
ci / rust (push) Failing after 6m47s
apple / screenshots (push) Successful in 20m18s
ci / web (push) Successful in 54s
ci / docs-site (push) Successful in 1m8s
decky / build-publish (push) Successful in 20s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
android / android (push) Successful in 12m23s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
ci / bench (push) Successful in 6m3s
arch / build-publish (push) Successful in 12m57s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9m40s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7m4s
docker / deploy-docs (push) Successful in 11s
flatpak / build-publish (push) Successful in 5m57s
deb / build-publish (push) Successful in 13m26s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m50s
apple / swift (push) Successful in 4m31s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m8s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m44s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m7s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m9s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m45s
release / apple (push) Successful in 21m27s
ci / rust (push) Failing after 6m47s
apple / screenshots (push) Successful in 20m18s
Give instant feedback the moment a host is picked, and make the wake wait a full-screen takeover instead of a modal card — unified into one ConnectOverlay across every client: - android: new ConnectOverlay (aurora backdrop; Connecting / Waking / timed-out phases) replaces the tiny inline "Connecting…" row and the WakeOverlay card. The dial phase is now cancelable and hands off to the wake wait in one frame. - console (pf-console-ui): the connect/wake overlays become a full-screen aurora takeover (draw_takeover) instead of a centered card over a dim scrim; the Waking → Connecting handoff no longer blinks. - apple: new ConnectOverlay mirrors it (macOS / iOS / tvOS), replacing the per-tile connecting spinner + the WakeOverlay card; instant "Connecting…" from model.phase, and the carousel is gated inactive during the dial. Also enable Wake-on-LAN on iOS/tvOS now that the multicast entitlement is approved: enable com.apple.developer.networking.multicast and flip wakeOnLANAvailable to true on every platform (MACs were already learned from mDNS, so wake works immediately). Verified: Android compileDebugKotlin + screenshot renders; console clippy + 36 tests + rendered phases on Linux; Apple swift build + 121 tests + rendered phases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+158
-130
@@ -19,7 +19,9 @@ use anyhow::{anyhow, Result};
|
||||
use pf_client_core::gamepad::{MenuDir, MenuEvent, MenuPulse, PadInfo};
|
||||
use pf_client_core::trust;
|
||||
use pf_presenter::overlay::OverlayAction;
|
||||
use skia_safe::{Canvas, Color4f, Data, Paint, Rect, RuntimeEffect};
|
||||
use skia_safe::{
|
||||
gradient_shader, Canvas, Color4f, Data, Paint, Point, Rect, RuntimeEffect, TileMode,
|
||||
};
|
||||
use std::collections::VecDeque;
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -240,6 +242,11 @@ impl Shell {
|
||||
self.wake = None;
|
||||
if let Some(Some(intent)) = intent {
|
||||
self.start_connect(intent);
|
||||
// The wake takeover was already full-screen; skip the connect fade-in so the
|
||||
// Waking → Connecting handoff is seamless (no flash of the home behind).
|
||||
if let Some(c) = &mut self.connecting {
|
||||
c.appear = 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -607,77 +614,68 @@ impl Shell {
|
||||
t: f64,
|
||||
fonts: &Fonts,
|
||||
) {
|
||||
if let Some(c) = &mut self.connecting {
|
||||
c.appear = approach(c.appear, 1.0, dt, 0.07);
|
||||
let a = c.appear;
|
||||
canvas.draw_rect(
|
||||
Rect::from_wh(w as f32, h as f32),
|
||||
&Paint::new(Color4f::new(0.0, 0.0, 0.0, (0.45 * a) as f32), None),
|
||||
);
|
||||
let title = if c.canceling {
|
||||
"Canceling…".to_string()
|
||||
// Resolve the connect/wake takeover — the two phases of reaching a host — into one
|
||||
// full-screen shape (spinner, title, one detail line, its own hints). Connecting flows
|
||||
// straight out of a wake (see `sync`) so they share the same backdrop and never blink
|
||||
// between them. Mirrors the Android client's unified `ConnectOverlay`.
|
||||
let takeover: Option<(f64, bool, String, String, Vec<Hint>)> =
|
||||
if let Some(c) = &mut self.connecting {
|
||||
c.appear = approach(c.appear, 1.0, dt, 0.07);
|
||||
if c.canceling {
|
||||
Some((
|
||||
c.appear,
|
||||
true,
|
||||
"Canceling…".to_string(),
|
||||
String::new(),
|
||||
vec![],
|
||||
))
|
||||
} else {
|
||||
Some((
|
||||
c.appear,
|
||||
true,
|
||||
format!("Connecting to {}…", c.title),
|
||||
"Starting the stream in this window.".to_string(),
|
||||
vec![Hint::new(HintKey::Back, "Cancel")],
|
||||
))
|
||||
}
|
||||
} else if let Some(wk) = &self.wake {
|
||||
// Service-driven, so it appears settled (no fade-in).
|
||||
if wk.timed_out {
|
||||
Some((
|
||||
1.0,
|
||||
false,
|
||||
format!("{} didn't wake", wk.name),
|
||||
"Check its power settings, or wake it manually and try again.".to_string(),
|
||||
vec![
|
||||
Hint::new(HintKey::Confirm, "Try Again"),
|
||||
Hint::new(HintKey::Back, "Cancel"),
|
||||
],
|
||||
))
|
||||
} else {
|
||||
Some((
|
||||
1.0,
|
||||
true,
|
||||
format!("Waking {}…", wk.name),
|
||||
format!("Waiting for it to come online · {} s", wk.seconds),
|
||||
// A wake-only wait (no dial after) offers "Stop Waiting"; a wake-&-connect
|
||||
// is a plain "Cancel".
|
||||
vec![Hint::new(
|
||||
HintKey::Back,
|
||||
if wk.then_connect {
|
||||
"Cancel"
|
||||
} else {
|
||||
"Stop Waiting"
|
||||
},
|
||||
)],
|
||||
))
|
||||
}
|
||||
} else {
|
||||
format!("Connecting to {}…", c.title)
|
||||
None
|
||||
};
|
||||
let hints = if c.canceling {
|
||||
vec![]
|
||||
} else {
|
||||
vec![Hint::new(HintKey::Back, "Cancel")]
|
||||
};
|
||||
card(
|
||||
canvas,
|
||||
fonts,
|
||||
w,
|
||||
h,
|
||||
k,
|
||||
a,
|
||||
t,
|
||||
self.glyphs,
|
||||
true,
|
||||
&title,
|
||||
"Starting the stream in this window.",
|
||||
&hints,
|
||||
if let Some((appear, spinner, title, body, hints)) = takeover {
|
||||
self.draw_takeover(
|
||||
canvas, w, h, k, appear, t, fonts, spinner, &title, &body, &hints,
|
||||
);
|
||||
} else if let Some(wk) = &self.wake {
|
||||
let a = 1.0; // the wake card is service-driven; it appears settled
|
||||
canvas.draw_rect(
|
||||
Rect::from_wh(w as f32, h as f32),
|
||||
&Paint::new(Color4f::new(0.0, 0.0, 0.0, 0.45), None),
|
||||
);
|
||||
if wk.timed_out {
|
||||
card(
|
||||
canvas,
|
||||
fonts,
|
||||
w,
|
||||
h,
|
||||
k,
|
||||
a,
|
||||
t,
|
||||
self.glyphs,
|
||||
false,
|
||||
&format!("{} didn't wake", wk.name),
|
||||
"Check its power settings, or wake it manually and try again.",
|
||||
&[
|
||||
Hint::new(HintKey::Confirm, "Try Again"),
|
||||
Hint::new(HintKey::Back, "Cancel"),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
card(
|
||||
canvas,
|
||||
fonts,
|
||||
w,
|
||||
h,
|
||||
k,
|
||||
a,
|
||||
t,
|
||||
self.glyphs,
|
||||
true,
|
||||
&format!("Waking {}…", wk.name),
|
||||
&format!("Waiting for it to come online · {} s", wk.seconds),
|
||||
&[Hint::new(HintKey::Back, "Cancel")],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// The toast: a transient pill above the hint bar; slides in, fades out.
|
||||
@@ -799,70 +797,91 @@ impl LayerEnv<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A centered modal card: spinner (or not), a title, one detail line, and its own hint
|
||||
/// row — the connect/wake overlays share this one shape.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn card(
|
||||
canvas: &Canvas,
|
||||
fonts: &Fonts,
|
||||
w: f64,
|
||||
h: f64,
|
||||
k: f64,
|
||||
appear: f64,
|
||||
t: f64,
|
||||
glyphs: GlyphStyle,
|
||||
spinner: bool,
|
||||
title: &str,
|
||||
body: &str,
|
||||
hints: &[Hint],
|
||||
) {
|
||||
let cw = (440.0 * k).min(w * 0.86);
|
||||
let ch = 190.0 * k;
|
||||
let cx = w / 2.0;
|
||||
let top = h / 2.0 - ch / 2.0 + (1.0 - appear) * 14.0 * k;
|
||||
canvas.save_layer_alpha_f(None, appear as f32);
|
||||
let rect = Rect::from_xywh((cx - cw / 2.0) as f32, top as f32, cw as f32, ch as f32);
|
||||
crate::theme::drop_shadow(canvas, rect, 22.0, k as f32, 0.5);
|
||||
crate::theme::panel(
|
||||
canvas,
|
||||
rect,
|
||||
22.0,
|
||||
Some(Color4f::new(0.07, 0.06, 0.12, 0.85)),
|
||||
PanelStroke::Plain(0.14),
|
||||
k as f32,
|
||||
);
|
||||
let mut y = top + 44.0 * k;
|
||||
if spinner {
|
||||
crate::theme::spinner(canvas, cx, y, 14.0 * k, t);
|
||||
y += 34.0 * k;
|
||||
} else {
|
||||
y += 6.0 * k;
|
||||
}
|
||||
fonts.centered(canvas, title, W::SemiBold, 19.0 * k, WHITE, cx, y, cw * 0.9);
|
||||
fonts.centered(
|
||||
canvas,
|
||||
body,
|
||||
W::Regular,
|
||||
13.0 * k,
|
||||
DIM,
|
||||
cx,
|
||||
y + 30.0 * k,
|
||||
cw * 0.86,
|
||||
);
|
||||
if !hints.is_empty() {
|
||||
// Centered inside the card's bottom band.
|
||||
let probe = hint_bar(canvas, fonts, hints, glyphs, -10_000.0, -10_000.0, k);
|
||||
hint_bar(
|
||||
impl Shell {
|
||||
/// A full-screen connect/wake takeover: a fresh aurora over everything (so the carousel and
|
||||
/// chrome fall away), a centered spinner (or none, when a wake has timed out), a title, one
|
||||
/// detail line, and its own bottom hint row. `appear` fades the whole thing in over the home;
|
||||
/// a wake that hands off to a connect passes 1.0 so the two never blink between them. The
|
||||
/// console counterpart of the Android/Apple `ConnectOverlay` — one full-screen shape, not a
|
||||
/// centered modal card.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_takeover(
|
||||
&self,
|
||||
canvas: &Canvas,
|
||||
w: f64,
|
||||
h: f64,
|
||||
k: f64,
|
||||
appear: f64,
|
||||
t: f64,
|
||||
fonts: &Fonts,
|
||||
spinner: bool,
|
||||
title: &str,
|
||||
body: &str,
|
||||
hints: &[Hint],
|
||||
) {
|
||||
let cx = w / 2.0;
|
||||
canvas.save_layer_alpha_f(None, appear as f32);
|
||||
// Opaque aurora — the same living backdrop the home wears, so the takeover reads as the
|
||||
// console taking over rather than a card popping up.
|
||||
self.draw_aurora(canvas, w, h, t);
|
||||
// A soft pool of shade under the centre seats the white text against a bright aurora.
|
||||
let mut vignette = Paint::default();
|
||||
vignette.set_shader(gradient_shader::radial(
|
||||
Point::new(cx as f32, (h / 2.0) as f32),
|
||||
(w.max(h) * 0.42) as f32,
|
||||
gradient_shader::GradientShaderColors::Colors(&[
|
||||
Color4f::new(0.0, 0.0, 0.0, 0.5).to_color(),
|
||||
Color4f::new(0.0, 0.0, 0.0, 0.0).to_color(),
|
||||
]),
|
||||
None,
|
||||
TileMode::Clamp,
|
||||
None,
|
||||
None,
|
||||
));
|
||||
canvas.draw_rect(Rect::from_wh(w as f32, h as f32), &vignette);
|
||||
|
||||
// Centre the spinner + title + detail as a group around the middle of the screen.
|
||||
let title_y = h / 2.0 + if spinner { 14.0 * k } else { 0.0 };
|
||||
if spinner {
|
||||
crate::theme::spinner(canvas, cx, title_y - 52.0 * k, 22.0 * k, t);
|
||||
}
|
||||
fonts.centered(
|
||||
canvas,
|
||||
fonts,
|
||||
hints,
|
||||
glyphs,
|
||||
cx - probe.0 / 2.0,
|
||||
top + ch - 16.0 * k,
|
||||
k,
|
||||
title,
|
||||
W::SemiBold,
|
||||
23.0 * k,
|
||||
WHITE,
|
||||
cx,
|
||||
title_y,
|
||||
w * 0.82,
|
||||
);
|
||||
if !body.is_empty() {
|
||||
fonts.centered(
|
||||
canvas,
|
||||
body,
|
||||
W::Regular,
|
||||
14.0 * k,
|
||||
DIM,
|
||||
cx,
|
||||
title_y + 32.0 * k,
|
||||
w * 0.66,
|
||||
);
|
||||
}
|
||||
if !hints.is_empty() {
|
||||
// Centered near the bottom, where every console screen's legend sits.
|
||||
let probe = hint_bar(canvas, fonts, hints, self.glyphs, -10_000.0, -10_000.0, k);
|
||||
hint_bar(
|
||||
canvas,
|
||||
fonts,
|
||||
hints,
|
||||
self.glyphs,
|
||||
cx - probe.0 / 2.0,
|
||||
h - 34.0 * k,
|
||||
k,
|
||||
);
|
||||
}
|
||||
canvas.restore();
|
||||
}
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1154,6 +1173,15 @@ mod tests {
|
||||
then_connect: true,
|
||||
}));
|
||||
dump(&mut s, 10, 8, "08-waking", true);
|
||||
console.set_wake(Some(WakeStatus {
|
||||
key: "bb22".into(),
|
||||
name: "Office Tower".into(),
|
||||
seconds: 90,
|
||||
timed_out: true,
|
||||
online: false,
|
||||
then_connect: true,
|
||||
}));
|
||||
dump(&mut s, 10, 8, "08b-wake-timed-out", true);
|
||||
console.set_wake(None);
|
||||
s.set_connecting(Some("Elden Ring".into()));
|
||||
dump(&mut s, 10, 8, "09-connecting", true);
|
||||
|
||||
Reference in New Issue
Block a user