mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-04 16:57:00 +01:00
feat(app,android,ios): ask before forgetting matches or downloading a whole show
- Turning metadata off or switching provider forgets every stored answer, including matches chosen by hand. The desktop now asks first and says how many hand-picked matches go. The phones' settings dialog says so above Save. - Forgetting a hand-picked match on the desktop, and any match on the phones, is asked about first. - Downloading a season, a show, or everything from a library tile asks first, with the episode count and their size. - Android confirmations that delete something use an error-toned text button rather than the filled accent, which read as the safe choice.
This commit is contained in:
1 parent
89e55aac12
commit
a79e41aa8a
14 files changed
+349
-41
No files matched your search
+116
-4
@@ -662,6 +662,12 @@ pub struct App {
|
||||
/// Finished offline copies the viewer asked to delete, and what they are
|
||||
/// of, waiting on their say-so: gigabytes are one misclick away otherwise.
|
||||
pub confirm_offline_delete: Option<(Vec<DownloadKey>, String)>,
|
||||
/// Metadata settings that would forget every stored answer, waiting on a
|
||||
/// yes.
|
||||
pub confirm_metadata: Option<MetadataConfig>,
|
||||
/// Several episodes asked for offline at once, waiting on a yes, with how
|
||||
/// many are not offline yet and their size.
|
||||
pub confirm_download: Option<(Vec<PlaybackTarget>, usize, i64)>,
|
||||
/// A share the viewer asked to remove, waiting on their say-so.
|
||||
pub confirm_remove_share: Option<String>,
|
||||
/// Messages in the corner. See [`ui::toast`].
|
||||
@@ -748,6 +754,8 @@ impl App {
|
||||
offline_files: std::collections::HashSet::new(),
|
||||
confirm_partial_delete: None,
|
||||
confirm_offline_delete: None,
|
||||
confirm_metadata: None,
|
||||
confirm_download: None,
|
||||
confirm_remove_share: None,
|
||||
toasts: ui::toast::Toasts::default(),
|
||||
pacer: Pacer::new(),
|
||||
@@ -1500,8 +1508,34 @@ impl App {
|
||||
self.engine.play(target);
|
||||
}
|
||||
Action::MakeOffline(targets) => {
|
||||
self.note(ctx, "downloading for offline use…", false);
|
||||
self.engine.make_offline(targets);
|
||||
// A season or a show is asked about, with its size: forty
|
||||
// episodes is tens of gigabytes, and one click should not
|
||||
// fill a disk.
|
||||
let missing: Vec<_> = targets
|
||||
.iter()
|
||||
.filter(|target| !self.offline_files.contains(&DownloadKey::from(*target)))
|
||||
.collect();
|
||||
if missing.len() > 1 {
|
||||
let bytes = missing
|
||||
.iter()
|
||||
.filter_map(|target| {
|
||||
self.library
|
||||
.get(&target.title_key)?
|
||||
.episodes()
|
||||
.find(|episode| {
|
||||
episode.node.share_id == target.share_id
|
||||
&& episode.node.link_id == target.link_id
|
||||
})?
|
||||
.node
|
||||
.size
|
||||
})
|
||||
.sum();
|
||||
let count = missing.len();
|
||||
self.confirm_download = Some((targets, count, bytes));
|
||||
} else {
|
||||
self.note(ctx, "downloading for offline use…", false);
|
||||
self.engine.make_offline(targets);
|
||||
}
|
||||
}
|
||||
Action::PauseDownload(key) => self.engine.pause_download(&key),
|
||||
Action::ResumeDownload(key) => self.engine.resume_download(&key),
|
||||
@@ -1519,8 +1553,18 @@ impl App {
|
||||
}
|
||||
}
|
||||
Action::SetMetadataConfig(config) => {
|
||||
self.settings = config.clone();
|
||||
self.engine.set_metadata_config(config);
|
||||
// Turning it off or changing provider forgets every answer,
|
||||
// hand-picked matches included: asked, when there are some.
|
||||
let previous = self.engine.metadata_config();
|
||||
let forgets =
|
||||
previous.enabled && (!config.enabled || config.provider != previous.provider);
|
||||
if forgets && !self.metadata.is_empty() {
|
||||
self.settings = previous;
|
||||
self.confirm_metadata = Some(config);
|
||||
} else {
|
||||
self.settings = config.clone();
|
||||
self.engine.set_metadata_config(config);
|
||||
}
|
||||
}
|
||||
Action::SetApiKey { provider, key } => self.engine.set_api_key(provider, key),
|
||||
Action::MatchTitles { force } => {
|
||||
@@ -2174,6 +2218,8 @@ impl eframe::App for App {
|
||||
let dialog = self.matcher.is_some()
|
||||
|| self.confirm_partial_delete.is_some()
|
||||
|| self.confirm_offline_delete.is_some()
|
||||
|| self.confirm_metadata.is_some()
|
||||
|| self.confirm_download.is_some()
|
||||
|| self.confirm_remove_share.is_some()
|
||||
|| self.shortcuts_open;
|
||||
if !dialog {
|
||||
@@ -2266,6 +2312,8 @@ impl eframe::App for App {
|
||||
offline_files,
|
||||
confirm_partial_delete,
|
||||
confirm_offline_delete,
|
||||
confirm_metadata,
|
||||
confirm_download,
|
||||
confirm_remove_share,
|
||||
shown_page,
|
||||
..
|
||||
@@ -2440,6 +2488,70 @@ impl eframe::App for App {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((targets, count, bytes)) = confirm_download.clone() {
|
||||
let what = targets
|
||||
.first()
|
||||
.map_or("these episodes", |target| target.title_name.as_str());
|
||||
let size = if bytes > 0 {
|
||||
format!(" — about {}", ui::format_size(bytes))
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
match ui::confirm_with(
|
||||
ctx,
|
||||
"download",
|
||||
&format!("Download {count} episodes of {what}?"),
|
||||
&format!(
|
||||
"They are kept on this computer for watching without a connection{size}. \
|
||||
Delete them from the Downloads page."
|
||||
),
|
||||
"Download",
|
||||
false,
|
||||
) {
|
||||
ui::Answer::Confirmed => {
|
||||
engine.make_offline(targets);
|
||||
*confirm_download = None;
|
||||
}
|
||||
ui::Answer::Declined => *confirm_download = None,
|
||||
ui::Answer::Pending => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(config) = confirm_metadata.clone() {
|
||||
let picked = metadata.values().filter(|record| record.manual).count();
|
||||
let (heading, verb) = if config.enabled {
|
||||
(format!("Switch to {}?", config.provider.label()), "Switch")
|
||||
} else {
|
||||
("Turn off posters and descriptions?".to_owned(), "Turn off")
|
||||
};
|
||||
let mut body = "Every poster, description and episode name stored now is \
|
||||
forgotten"
|
||||
.to_owned();
|
||||
if picked > 0 {
|
||||
body.push_str(&format!(
|
||||
", and so {} you chose by hand",
|
||||
match picked {
|
||||
1 => "is the 1 match".to_owned(),
|
||||
count => format!("are the {count} matches"),
|
||||
}
|
||||
));
|
||||
}
|
||||
body.push_str(if config.enabled {
|
||||
". The library is looked up again with the new provider."
|
||||
} else {
|
||||
". Nothing more is sent anywhere."
|
||||
});
|
||||
match ui::confirm(ctx, "metadata", &heading, &body, verb) {
|
||||
ui::Answer::Confirmed => {
|
||||
*settings = config.clone();
|
||||
engine.set_metadata_config(config);
|
||||
*confirm_metadata = None;
|
||||
}
|
||||
ui::Answer::Declined => *confirm_metadata = None,
|
||||
ui::Answer::Pending => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(id) = confirm_remove_share.clone() {
|
||||
let name = shares
|
||||
.iter()
|
||||
|
||||
@@ -130,12 +130,29 @@ fn current_row(
|
||||
)));
|
||||
// Only worth offering when there is something to undo: with no
|
||||
// row stored, "clear" would delete nothing.
|
||||
if ui
|
||||
//
|
||||
// A match chosen by hand is asked about first: it is work the
|
||||
// next match run does not redo.
|
||||
let asking = ui.id().with(("forget", &matcher.title_key));
|
||||
if ui.data(|data| data.get_temp::<bool>(asking).unwrap_or(false)) {
|
||||
ui.label(ui::muted("Forget the match you chose?"));
|
||||
if ui::danger_button(ui, "Forget").clicked() {
|
||||
ui.data_mut(|data| data.remove::<bool>(asking));
|
||||
actions.push(Action::ForgetMatch(matcher.title_key.clone()));
|
||||
}
|
||||
if ui.button("Keep").clicked() {
|
||||
ui.data_mut(|data| data.remove::<bool>(asking));
|
||||
}
|
||||
} else if ui
|
||||
.button("Clear")
|
||||
.on_hover_text("Forget this match and let the next match run decide again")
|
||||
.clicked()
|
||||
{
|
||||
actions.push(Action::ForgetMatch(matcher.title_key.clone()));
|
||||
if *manual {
|
||||
ui.data_mut(|data| data.insert_temp(asking, true));
|
||||
} else {
|
||||
actions.push(Action::ForgetMatch(matcher.title_key.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some((None, _)) => {
|
||||
|
||||
@@ -387,6 +387,19 @@ pub enum Answer {
|
||||
/// inside the app — stored secrets, downloaded bytes. Everything else acts on
|
||||
/// the click.
|
||||
pub fn confirm(ctx: &egui::Context, id: &str, heading: &str, body: &str, verb: &str) -> Answer {
|
||||
confirm_with(ctx, id, heading, body, verb, true)
|
||||
}
|
||||
|
||||
/// [`confirm`], for a question whose yes loses nothing — a large download —
|
||||
/// and so is the accent rather than the danger colour.
|
||||
pub fn confirm_with(
|
||||
ctx: &egui::Context,
|
||||
id: &str,
|
||||
heading: &str,
|
||||
body: &str,
|
||||
verb: &str,
|
||||
destroys: bool,
|
||||
) -> Answer {
|
||||
let mut answer = Answer::Pending;
|
||||
let modal = egui::Modal::new(egui::Id::new(("confirm", id)))
|
||||
.frame(
|
||||
@@ -407,7 +420,12 @@ pub fn confirm(ctx: &egui::Context, id: &str, heading: &str, body: &str, verb: &
|
||||
ui.label(theme::Role::Body.rich(body).color(theme::muted()));
|
||||
ui.add_space(theme::space::XL);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if danger_button(ui, verb).clicked() {
|
||||
let yes = if destroys {
|
||||
danger_button(ui, verb)
|
||||
} else {
|
||||
accent_button(ui, verb)
|
||||
};
|
||||
if yes.clicked() {
|
||||
answer = Answer::Confirmed;
|
||||
}
|
||||
if ui.button("Cancel").clicked() {
|
||||
|
||||
Reference in new issue
Block a user