mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-03 16:41:26 +01:00
feat(app,android,ios): swipe gestures, touch lock and sleep timer
On Android and iOS a swipe up or down on the left half of the picture sets the brightness and on the right half the volume; a sideways swipe seeks, showing where it will land and seeking only when the finger lifts. Swipes that start at the top or bottom edge are left to the system. Android turns the device's media volume; iOS lets no app do that, so there the swipe turns the player's own, as its slider does. Brightness set by a swipe is handed back when the player goes. Lock touch, in the player's menu, makes the picture ignore touches and the back gesture; a tap brings up Unlock for a few seconds. The sleep timer pauses after 30 or 60 minutes, or stops at the end of the episode playing instead of going on to the next, with no up-next card. On the desktop it is a panel like the others, behind a moon button that shows what is left; on Android and iOS it lives with the player host, so it keeps running with the screen off.
This commit is contained in:
1 parent
cf8ee58f6b
commit
973a8f2749
11 files changed
+827
-25
No files matched your search
@@ -283,6 +283,8 @@ pub enum Action {
|
||||
},
|
||||
/// Open or close one of the panels beside the picture.
|
||||
TogglePanel(ui::player::PlayerPanel),
|
||||
/// Pause when this runs out, or turn the timer off.
|
||||
SetSleep(Option<ui::player::SleepChoice>),
|
||||
/// Put watch states back as they were — what Undo does.
|
||||
RestoreWatch(Vec<(String, String, pstr_core::catalog::WatchState)>),
|
||||
ClearStreamCache,
|
||||
@@ -735,6 +737,8 @@ pub struct App {
|
||||
subtitle_style_unsaved: bool,
|
||||
/// Which panel is out beside the picture on the player page.
|
||||
pub player_panel: Option<ui::player::PlayerPanel>,
|
||||
/// The sleep timer, when one is set.
|
||||
sleep: Option<Sleep>,
|
||||
/// Set while the window is fullscreen, so `F` can toggle rather than only
|
||||
/// ever entering. egui has no way to ask the platform.
|
||||
pub fullscreen: bool,
|
||||
@@ -845,6 +849,7 @@ impl App {
|
||||
subtitle_style_unsaved: false,
|
||||
fullscreen: false,
|
||||
player_panel: None,
|
||||
sleep: None,
|
||||
opening: None,
|
||||
connecting: true,
|
||||
crawling: false,
|
||||
@@ -1376,7 +1381,7 @@ impl App {
|
||||
actions: &mut Vec<Action>,
|
||||
) -> Option<UpNextCard> {
|
||||
let has_next = self.neighbours().next;
|
||||
let autoplay = self.engine.playback_prefs().autoplay_next;
|
||||
let autoplay = self.engine.playback_prefs().autoplay_next && !self.stops_after_this();
|
||||
let next = self.adjacent(Adjacent::Next);
|
||||
|
||||
let Some(playback) = &self.playback else {
|
||||
@@ -1462,6 +1467,34 @@ impl App {
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether the sleep timer is set to stop at the end of what is playing.
|
||||
fn stops_after_this(&self) -> bool {
|
||||
matches!(
|
||||
(self.sleep, &self.playback),
|
||||
(Some(Sleep::EndOfEpisode(id)), Some(playback)) if playback.id == id
|
||||
)
|
||||
}
|
||||
|
||||
/// Pause once the sleep timer runs out. A timer for the end of an episode
|
||||
/// that is no longer playing — the viewer moved on to another — is
|
||||
/// dropped: it was about that episode.
|
||||
fn tick_sleep(&mut self, ctx: &egui::Context) {
|
||||
match (self.sleep, &self.playback) {
|
||||
(Some(Sleep::At(at)), Some(playback)) if std::time::Instant::now() >= at => {
|
||||
if !playback.paused {
|
||||
playback.send(crate::playback::Command::TogglePause);
|
||||
}
|
||||
self.sleep = None;
|
||||
self.note(ctx, "Sleep timer: paused", false);
|
||||
}
|
||||
(Some(Sleep::EndOfEpisode(id)), Some(playback)) if playback.id != id => {
|
||||
self.sleep = None;
|
||||
}
|
||||
(Some(_), None) => self.sleep = None,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Once a file has loaded with no chapters naming its opening or ending,
|
||||
/// ask AniSkip, when the episode's provider entry has a MyAnimeList id.
|
||||
fn ask_skip_times(&mut self) {
|
||||
@@ -1555,6 +1588,11 @@ impl App {
|
||||
/// end too, and auto-advancing through a broken share one episode at a
|
||||
/// time is how a player earns being turned off.
|
||||
fn autoplay_next(&mut self, ctx: &egui::Context) {
|
||||
if self.stops_after_this() {
|
||||
// The timer has done its job.
|
||||
self.sleep = None;
|
||||
return;
|
||||
}
|
||||
if !self.engine.playback_prefs().autoplay_next {
|
||||
return;
|
||||
}
|
||||
@@ -2078,6 +2116,29 @@ impl App {
|
||||
Action::RemoveFromHistory { share_id, link_id } => {
|
||||
self.forget_watch(ctx, share_id, link_id, "history");
|
||||
}
|
||||
Action::SetSleep(choice) => {
|
||||
self.sleep = match choice {
|
||||
None => None,
|
||||
Some(ui::player::SleepChoice::EndOfEpisode) => self
|
||||
.playback
|
||||
.as_ref()
|
||||
.map(|playback| Sleep::EndOfEpisode(playback.id)),
|
||||
Some(ui::player::SleepChoice::Minutes(minutes)) => Some(Sleep::At(
|
||||
std::time::Instant::now()
|
||||
+ std::time::Duration::from_secs(u64::from(minutes) * 60),
|
||||
)),
|
||||
};
|
||||
let text = match choice {
|
||||
None => "Sleep timer off".to_owned(),
|
||||
Some(ui::player::SleepChoice::EndOfEpisode) => {
|
||||
"Stopping after this episode".to_owned()
|
||||
}
|
||||
Some(ui::player::SleepChoice::Minutes(minutes)) => {
|
||||
format!("Pausing in {minutes} min")
|
||||
}
|
||||
};
|
||||
self.flash(ctx, egui_phosphor::regular::MOON, text);
|
||||
}
|
||||
Action::TogglePanel(panel) => {
|
||||
self.player_panel = (self.player_panel != Some(panel)).then_some(panel);
|
||||
}
|
||||
@@ -2345,6 +2406,7 @@ impl eframe::App for App {
|
||||
}
|
||||
self.warm_next();
|
||||
self.ask_skip_times();
|
||||
self.tick_sleep(ctx);
|
||||
self.auto_skip(ctx);
|
||||
self.save_subtitle_style(ctx);
|
||||
self.window.observe(ctx);
|
||||
@@ -2404,6 +2466,7 @@ impl eframe::App for App {
|
||||
metadata,
|
||||
episodes,
|
||||
player_panel,
|
||||
sleep,
|
||||
..
|
||||
} = self;
|
||||
// Before the list borrows the art caches for the rest of the frame.
|
||||
@@ -2498,6 +2561,7 @@ impl eframe::App for App {
|
||||
episodes: list,
|
||||
panel: *player_panel,
|
||||
about,
|
||||
sleep: sleep.map(Sleep::choice),
|
||||
},
|
||||
&mut actions,
|
||||
);
|
||||
@@ -3165,6 +3229,30 @@ struct PlayerKeys {
|
||||
skip_to: Option<f64>,
|
||||
}
|
||||
|
||||
/// A sleep timer that is running.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Sleep {
|
||||
/// Stop when player `id`'s file ends, rather than going on to the next.
|
||||
EndOfEpisode(u64),
|
||||
/// Pause at this moment.
|
||||
At(std::time::Instant),
|
||||
}
|
||||
|
||||
impl Sleep {
|
||||
/// What the panel shows as chosen.
|
||||
fn choice(self) -> ui::player::SleepChoice {
|
||||
match self {
|
||||
Self::EndOfEpisode(_) => ui::player::SleepChoice::EndOfEpisode,
|
||||
Self::At(at) => ui::player::SleepChoice::Minutes(
|
||||
(at.saturating_duration_since(std::time::Instant::now())
|
||||
.as_secs()
|
||||
/ 60) as u32
|
||||
+ 1,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The speeds `[` and `]` step through. Fixed steps rather than a nudge of a
|
||||
/// tenth, because 1.0 has to be somewhere a viewer can land on again.
|
||||
use crate::ui::transport::SPEEDS;
|
||||
|
||||
@@ -89,8 +89,22 @@ pub enum PlayerPanel {
|
||||
Tracks,
|
||||
Speed,
|
||||
Chapters,
|
||||
Sleep,
|
||||
}
|
||||
|
||||
/// A sleep timer, as chosen or as it stands.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SleepChoice {
|
||||
/// Stop at the end of the episode playing, rather than go on to the next.
|
||||
EndOfEpisode,
|
||||
/// Pause after this many minutes. As it stands, the minutes left, rounded
|
||||
/// up.
|
||||
Minutes(u32),
|
||||
}
|
||||
|
||||
/// The timers on offer, besides the end of the episode.
|
||||
const SLEEP_MINUTES: [u32; 2] = [30, 60];
|
||||
|
||||
impl PlayerPanel {
|
||||
fn heading(self) -> &'static str {
|
||||
match self {
|
||||
@@ -98,6 +112,7 @@ impl PlayerPanel {
|
||||
Self::Tracks => "Audio and subtitles",
|
||||
Self::Speed => "Playback speed",
|
||||
Self::Chapters => "Chapters",
|
||||
Self::Sleep => "Sleep timer",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,6 +122,7 @@ impl PlayerPanel {
|
||||
struct Panels {
|
||||
episodes: bool,
|
||||
open: Option<PlayerPanel>,
|
||||
sleep: Option<SleepChoice>,
|
||||
}
|
||||
|
||||
/// What the pause screen says about what is playing.
|
||||
@@ -172,6 +188,8 @@ pub struct Chrome<'a> {
|
||||
pub panel: Option<PlayerPanel>,
|
||||
/// What the pause screen shows, while the picture is paused.
|
||||
pub about: Option<About>,
|
||||
/// The sleep timer, when one is running.
|
||||
pub sleep: Option<SleepChoice>,
|
||||
}
|
||||
|
||||
/// The playing title's episodes, for the list beside the picture.
|
||||
@@ -293,6 +311,7 @@ pub fn show(
|
||||
mut episodes,
|
||||
panel,
|
||||
about,
|
||||
sleep,
|
||||
} = chrome_state;
|
||||
let ctx = ui.ctx().clone();
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
@@ -383,6 +402,7 @@ pub fn show(
|
||||
Panels {
|
||||
episodes: episodes.is_some(),
|
||||
open: panel,
|
||||
sleep,
|
||||
},
|
||||
rect,
|
||||
overlay.chrome_height,
|
||||
@@ -403,6 +423,7 @@ pub fn show(
|
||||
(PlayerPanel::Tracks, _) => track_list(ui, playback, actions),
|
||||
(PlayerPanel::Speed, _) => speed_list(ui, playback, actions),
|
||||
(PlayerPanel::Chapters, _) => chapter_list(ui, playback, actions),
|
||||
(PlayerPanel::Sleep, _) => sleep_list(ui, sleep, actions),
|
||||
(PlayerPanel::Episodes, None) => {}
|
||||
},
|
||||
);
|
||||
@@ -902,7 +923,7 @@ fn chrome(
|
||||
.max_rect(content)
|
||||
.layout(egui::Layout::top_down(egui::Align::Min)),
|
||||
|ui| {
|
||||
ui::transport::full(ui, playback, neighbours, panels.open, actions);
|
||||
ui::transport::full(ui, playback, neighbours, panels.open, panels.sleep, actions);
|
||||
ui.min_rect().height()
|
||||
},
|
||||
)
|
||||
@@ -1112,6 +1133,30 @@ fn speed_list(ui: &mut egui::Ui, playback: &Playback, actions: &mut Vec<Action>)
|
||||
}
|
||||
}
|
||||
|
||||
/// The sleep timers, and what the running one has left.
|
||||
fn sleep_list(ui: &mut egui::Ui, sleep: Option<SleepChoice>, actions: &mut Vec<Action>) {
|
||||
if let Some(SleepChoice::Minutes(left)) = sleep {
|
||||
ui.label(
|
||||
theme::Role::Label
|
||||
.rich(format!("Pausing in {left} min"))
|
||||
.color(theme::muted()),
|
||||
);
|
||||
ui.add_space(theme::space::S);
|
||||
}
|
||||
if option_row(ui, sleep.is_none(), "Off", None).clicked() {
|
||||
actions.push(Action::SetSleep(None));
|
||||
}
|
||||
let end = sleep == Some(SleepChoice::EndOfEpisode);
|
||||
if option_row(ui, end, "End of this episode", None).clicked() {
|
||||
actions.push(Action::SetSleep(Some(SleepChoice::EndOfEpisode)));
|
||||
}
|
||||
for minutes in SLEEP_MINUTES {
|
||||
if option_row(ui, false, &format!("{minutes} minutes"), None).clicked() {
|
||||
actions.push(Action::SetSleep(Some(SleepChoice::Minutes(minutes))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Every chapter, with where it starts, the one playing marked.
|
||||
fn chapter_list(ui: &mut egui::Ui, playback: &Playback, actions: &mut Vec<Action>) {
|
||||
let current = playback.chapter().map(|chapter| chapter.index);
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::app::{Action, Adjacent, Page};
|
||||
use crate::playback::{Command, Playback};
|
||||
use crate::theme;
|
||||
use crate::ui;
|
||||
use crate::ui::player::PlayerPanel;
|
||||
use crate::ui::player::{PlayerPanel, SleepChoice};
|
||||
use crate::ui::widgets::Named;
|
||||
|
||||
/// Height of the clickable seek bar. Comfortably larger than the few pixels it
|
||||
@@ -59,6 +59,7 @@ pub fn full(
|
||||
playback: &Playback,
|
||||
neighbours: Neighbours,
|
||||
panel: Option<PlayerPanel>,
|
||||
sleep: Option<SleepChoice>,
|
||||
actions: &mut Vec<Action>,
|
||||
) {
|
||||
seek_bar(ui, playback, actions, 6.0);
|
||||
@@ -108,7 +109,7 @@ pub fn full(
|
||||
// opens its panel down the right of the picture rather than a menu
|
||||
// under the pointer: a list of twelve chapters is read, not aimed at.
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
panel_buttons(ui, playback, panel, actions);
|
||||
panel_buttons(ui, playback, panel, sleep, actions);
|
||||
ui.add_space(theme::space::M);
|
||||
volume(ui, playback, actions);
|
||||
});
|
||||
@@ -551,12 +552,13 @@ fn track_options(ui: &mut egui::Ui, tracks: &[&Track], kind: TrackKind, actions:
|
||||
}
|
||||
|
||||
/// The buttons that open the player's panels, right to left: tracks, speed,
|
||||
/// chapters. Each shows only when there is a choice to make, and says what is
|
||||
/// chosen when that is worth noticing at a glance.
|
||||
/// chapters, sleep. Each shows only when there is a choice to make, and says
|
||||
/// what is chosen when that is worth noticing at a glance.
|
||||
fn panel_buttons(
|
||||
ui: &mut egui::Ui,
|
||||
playback: &Playback,
|
||||
open: Option<PlayerPanel>,
|
||||
sleep: Option<SleepChoice>,
|
||||
actions: &mut Vec<Action>,
|
||||
) {
|
||||
let mut button = |ui: &mut egui::Ui, panel: PlayerPanel, text: String, hint: &str| {
|
||||
@@ -609,6 +611,13 @@ fn panel_buttons(
|
||||
"Chapters",
|
||||
);
|
||||
}
|
||||
let moon = egui_phosphor::regular::MOON;
|
||||
let sleep = match sleep {
|
||||
None => moon.to_owned(),
|
||||
Some(SleepChoice::EndOfEpisode) => format!("{moon} End"),
|
||||
Some(SleepChoice::Minutes(left)) => format!("{moon} {left} min"),
|
||||
};
|
||||
button(ui, PlayerPanel::Sleep, sleep, "Sleep timer");
|
||||
}
|
||||
|
||||
/// A track in as few words as fit on a button: what distinguishes it from the
|
||||
|
||||
Reference in new issue
Block a user