mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-03 16:41:26 +01:00
feat(app): go to the player page as soon as an episode is asked for
The click used to leave the viewer on the title page with a small line in the bottom bar until the file had opened, which over a public link can be seconds. The player page now comes up at once with the title's art dimmed behind the episode's name and a spinner, and Back still works. A file that fails to open goes back to the page it was asked from.
This commit is contained in:
1 parent
9d72acce88
commit
0f96cc4514
2 files changed
+123
-20
No files matched your search
+69
-10
@@ -78,6 +78,16 @@ impl Drop for FrameTimer {
|
||||
/// sitting through it is not a decision.
|
||||
const UP_NEXT_SECONDS: f64 = 10.0;
|
||||
|
||||
/// A file asked for whose player does not exist yet.
|
||||
pub struct Opening {
|
||||
/// What the bar and the player page call it while it opens.
|
||||
pub name: String,
|
||||
pub title_key: String,
|
||||
/// The page the viewer asked from, to go back to if it never opens. The
|
||||
/// player page itself when one episode follows another.
|
||||
pub from: Page,
|
||||
}
|
||||
|
||||
/// Which page is showing.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Page {
|
||||
@@ -627,7 +637,7 @@ pub struct App {
|
||||
/// ever entering. egui has no way to ask the platform.
|
||||
pub fullscreen: bool,
|
||||
/// The file being opened, if a click is still waiting on the network.
|
||||
pub opening: Option<String>,
|
||||
pub opening: Option<Opening>,
|
||||
pub connecting: bool,
|
||||
pub crawling: bool,
|
||||
/// Which share the crawl is on, and how far it has got.
|
||||
@@ -957,7 +967,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
Event::PlaybackReady { target, stream } => {
|
||||
self.opening = None;
|
||||
let opening = self.opening.take();
|
||||
// Dropped before the new one is built: two mpv cores would
|
||||
// fight over the ring and the bandwidth, and the old one's
|
||||
// render context has to be freed on this thread anyway.
|
||||
@@ -971,6 +981,7 @@ impl App {
|
||||
self.playback = None;
|
||||
let torn_down = swap.elapsed();
|
||||
let gl = frame.gl().cloned();
|
||||
let target_key = target.title_key.clone();
|
||||
let started = Playback::start(&self.engine, *target, stream, gl.as_ref());
|
||||
if swap.elapsed() >= STALL {
|
||||
tracing::warn!(
|
||||
@@ -986,7 +997,15 @@ impl App {
|
||||
self.page = Page::Player;
|
||||
self.playback = Some(playback);
|
||||
}
|
||||
Err(error) => self.note(ctx, error, true),
|
||||
Err(error) => {
|
||||
if self.page == Page::Player {
|
||||
self.page = match opening.map(|opening| opening.from) {
|
||||
Some(Page::Player) | None => Page::Title(target_key),
|
||||
Some(from) => from,
|
||||
};
|
||||
}
|
||||
self.note(ctx, error, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Only from the player currently on the bar: one being replaced
|
||||
@@ -1052,7 +1071,17 @@ impl App {
|
||||
}
|
||||
}
|
||||
Event::Error(text) => {
|
||||
self.opening = None;
|
||||
// The open failed: back to where it was asked from, with
|
||||
// the reason, rather than a player page with nothing on it.
|
||||
if let Some(opening) = self.opening.take()
|
||||
&& self.page == Page::Player
|
||||
&& self.playback.is_none()
|
||||
{
|
||||
self.page = match opening.from {
|
||||
Page::Player => Page::Title(opening.title_key),
|
||||
from => from,
|
||||
};
|
||||
}
|
||||
self.note(ctx, text, true);
|
||||
}
|
||||
Event::Status(text) => self.note(ctx, text, false),
|
||||
@@ -1444,7 +1473,15 @@ impl App {
|
||||
.engine
|
||||
.title_track_prefs(&target.title_key)
|
||||
.map(Box::new);
|
||||
self.opening = Some(target.name.clone());
|
||||
// To the player page now, not when the file has opened: over a
|
||||
// public link that can take seconds, and a click that seems to
|
||||
// do nothing gets clicked again.
|
||||
self.opening = Some(Opening {
|
||||
name: target.name.clone(),
|
||||
title_key: target.title_key.clone(),
|
||||
from: self.page.clone(),
|
||||
});
|
||||
self.page = Page::Player;
|
||||
self.engine.play(target);
|
||||
}
|
||||
Action::MakeOffline(targets) => {
|
||||
@@ -1549,9 +1586,10 @@ impl App {
|
||||
self.fullscreen = false;
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(false));
|
||||
}
|
||||
self.page = match &self.playback {
|
||||
Some(playback) => Page::Title(playback.target.title_key.clone()),
|
||||
None => Page::Library,
|
||||
self.page = match (&self.playback, &self.opening) {
|
||||
(Some(playback), _) => Page::Title(playback.target.title_key.clone()),
|
||||
(None, Some(opening)) => Page::Title(opening.title_key.clone()),
|
||||
(None, None) => Page::Library,
|
||||
};
|
||||
}
|
||||
Action::Crawl(share) => {
|
||||
@@ -2040,6 +2078,27 @@ impl eframe::App for App {
|
||||
episode_drawer,
|
||||
..
|
||||
} = self;
|
||||
// Before the list borrows the art caches for the rest of the frame.
|
||||
let waiting = opening
|
||||
.as_ref()
|
||||
.filter(|_| playback.is_none())
|
||||
.map(|opening| {
|
||||
let title = library.get(&opening.title_key);
|
||||
let mut art = ui::Art {
|
||||
engine,
|
||||
thumbs,
|
||||
posters,
|
||||
metadata,
|
||||
episodes,
|
||||
names: engine.appearance().names,
|
||||
};
|
||||
ui::player::Waiting {
|
||||
name: &opening.name,
|
||||
title: title.map(|title| art.name(title).to_owned()),
|
||||
art: title.and_then(|title| art.wide(title)).map(|(art, _)| art),
|
||||
next: opening.from == Page::Player,
|
||||
}
|
||||
});
|
||||
let list = playback
|
||||
.as_ref()
|
||||
.and_then(|playback| library.get(&playback.target.title_key))
|
||||
@@ -2065,7 +2124,7 @@ impl eframe::App for App {
|
||||
ui,
|
||||
frame,
|
||||
playback.as_mut(),
|
||||
opening.as_deref(),
|
||||
waiting,
|
||||
ui::player::Chrome {
|
||||
overlay,
|
||||
up_next,
|
||||
@@ -2221,7 +2280,7 @@ impl eframe::App for App {
|
||||
ui,
|
||||
frame,
|
||||
playback.as_mut(),
|
||||
opening.as_deref(),
|
||||
opening.as_ref().map(|opening| opening.name.as_str()),
|
||||
neighbours,
|
||||
&mut actions,
|
||||
);
|
||||
|
||||
@@ -188,16 +188,28 @@ impl Overlay {
|
||||
}
|
||||
}
|
||||
|
||||
/// What the player page shows while a file opens and there is no picture yet.
|
||||
pub struct Waiting<'a> {
|
||||
/// The episode being opened.
|
||||
pub name: &'a str,
|
||||
/// The title it belongs to, as the library names it.
|
||||
pub title: Option<String>,
|
||||
/// The title's widest art, dimmed behind the words.
|
||||
pub art: Option<egui::TextureHandle>,
|
||||
/// Whether one episode is following another, rather than a first click.
|
||||
pub next: bool,
|
||||
}
|
||||
|
||||
/// Draw the picture and its controls.
|
||||
///
|
||||
/// `playback` is `None` in the gap between one episode ending and the next
|
||||
/// having opened — the page stays, because that is where the viewer means to
|
||||
/// be, and `opening` is what it says while they wait.
|
||||
/// `playback` is `None` until the file has opened — on a first click, and in
|
||||
/// the gap between one episode ending and the next — and `opening` is what the
|
||||
/// page says meanwhile.
|
||||
pub fn show(
|
||||
ui: &mut egui::Ui,
|
||||
frame: &mut eframe::Frame,
|
||||
playback: Option<&mut Playback>,
|
||||
opening: Option<&str>,
|
||||
opening: Option<Waiting<'_>>,
|
||||
chrome_state: Chrome<'_>,
|
||||
actions: &mut Vec<Action>,
|
||||
) {
|
||||
@@ -375,26 +387,60 @@ fn stalled(ui: &mut egui::Ui, rect: Rect) {
|
||||
);
|
||||
}
|
||||
|
||||
/// The screen between two episodes: one has ended and the next is opening.
|
||||
/// The screen while a file opens: after a click, or between one episode and
|
||||
/// the next.
|
||||
///
|
||||
/// It keeps a way out, because opening a file over a public link can take
|
||||
/// seconds and a black screen with no controls is indistinguishable from a
|
||||
/// hang.
|
||||
fn between(ui: &mut egui::Ui, opening: Option<&str>, rect: Rect, actions: &mut Vec<Action>) {
|
||||
fn between(ui: &mut egui::Ui, waiting: Option<Waiting<'_>>, rect: Rect, actions: &mut Vec<Action>) {
|
||||
let Waiting {
|
||||
name,
|
||||
title,
|
||||
art,
|
||||
next,
|
||||
} = waiting.unwrap_or(Waiting {
|
||||
name: "opening…",
|
||||
title: None,
|
||||
art: None,
|
||||
next: true,
|
||||
});
|
||||
if let Some(art) = &art {
|
||||
// Cover rather than fit: bars around a dimmed backdrop look like a
|
||||
// mistake, and its edges are not what anyone is looking at.
|
||||
let size = art.size_vec2();
|
||||
let scale = (rect.width() / size.x).max(rect.height() / size.y);
|
||||
let shown = size * scale;
|
||||
let uv = Rect::from_center_size(
|
||||
egui::pos2(0.5, 0.5),
|
||||
Vec2::new(rect.width() / shown.x, rect.height() / shown.y),
|
||||
);
|
||||
ui.painter()
|
||||
.image(art.id(), rect, uv, Color32::from_white_alpha(70));
|
||||
}
|
||||
ui.painter().text(
|
||||
rect.center() - Vec2::new(0.0, 16.0),
|
||||
Align2::CENTER_CENTER,
|
||||
"Up next",
|
||||
match (&title, next) {
|
||||
(_, true) => "Up next",
|
||||
(Some(title), false) => title.as_str(),
|
||||
(None, false) => "Opening",
|
||||
},
|
||||
theme::Role::Body.font(),
|
||||
theme::muted(),
|
||||
);
|
||||
ui.painter().text(
|
||||
rect.center() + Vec2::new(0.0, 12.0),
|
||||
Align2::CENTER_CENTER,
|
||||
opening.unwrap_or("opening…"),
|
||||
name,
|
||||
theme::Role::Title.font(),
|
||||
theme::text(),
|
||||
);
|
||||
let spinner = Rect::from_center_size(rect.center() + Vec2::new(0.0, 56.0), Vec2::splat(24.0));
|
||||
ui.put(
|
||||
spinner,
|
||||
egui::Spinner::new().size(24.0).color(Color32::WHITE),
|
||||
);
|
||||
|
||||
ui.scope_builder(
|
||||
egui::UiBuilder::new()
|
||||
@@ -407,8 +453,6 @@ fn between(ui: &mut egui::Ui, opening: Option<&str>, rect: Rect, actions: &mut V
|
||||
if ui.button("Back").clicked() {
|
||||
actions.push(Action::LeavePlayer);
|
||||
}
|
||||
ui.add_space(theme::space::M);
|
||||
ui.add(egui::Spinner::new().size(14.0));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user