mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-03 16:41:26 +01:00
feat(core,app,android,ios): finish adding a first share where it pays off
- A link with a password of its own, added without one, now fails as "This link has a password of its own" instead of the handshake's refusal. The link's public flags are only asked for after a refusal. The desktop form turns its password toggle on. - After the first share is added, all three clients go to the library. While that share is being listed, the empty library says so instead of offering to add a share. - The desktop library offers posters and synopses once there is a library to put them on. The offer says what is sent, links to Settings, never turns anything on, and "Not now" is remembered.
This commit is contained in:
1 parent
0f96cc4514
commit
89e55aac12
11 files changed
+150
-10
No files matched your search
@@ -144,6 +144,8 @@ pub enum Action {
|
||||
PasteShare(String),
|
||||
/// Go to the add form, caret in the link field.
|
||||
OpenShareForm,
|
||||
/// Stop offering posters and synopses on the library page.
|
||||
DismissMetadataOffer,
|
||||
/// Store a new link, and password, for a share already in the library.
|
||||
RepairShare {
|
||||
id: String,
|
||||
@@ -813,12 +815,25 @@ impl App {
|
||||
Event::ShareAdded(name) => {
|
||||
self.form = ShareForm::default();
|
||||
self.note(ctx, format!("added {name}"), false);
|
||||
// The first share is the library: watch it fill rather than
|
||||
// stay on a form that has done its job. A second one is
|
||||
// more likely one of several being added in a row.
|
||||
if self.library.is_empty() && self.page == Page::Shares {
|
||||
self.page = Page::Library;
|
||||
}
|
||||
}
|
||||
Event::ShareRejected(error) => {
|
||||
Event::ShareRejected {
|
||||
error,
|
||||
needs_password,
|
||||
} => {
|
||||
// Nothing was crawled, so nothing will say it finished.
|
||||
self.crawling = false;
|
||||
self.form.sending = false;
|
||||
self.form.error = Some(error);
|
||||
// The field the refusal asks for, opened and ready.
|
||||
if needs_password {
|
||||
self.form.has_password = true;
|
||||
}
|
||||
}
|
||||
Event::Connected { failures } => {
|
||||
self.connecting = false;
|
||||
@@ -1645,6 +1660,11 @@ impl App {
|
||||
self.page = Page::Shares;
|
||||
self.form.focus = true;
|
||||
}
|
||||
Action::DismissMetadataOffer => {
|
||||
let mut prefs = self.engine.desktop_prefs();
|
||||
prefs.metadata_offer_dismissed = true;
|
||||
self.engine.set_desktop_prefs(prefs);
|
||||
}
|
||||
Action::AddShare {
|
||||
name,
|
||||
url,
|
||||
@@ -2221,6 +2241,7 @@ impl eframe::App for App {
|
||||
},
|
||||
);
|
||||
|
||||
let crawl = self.crawling.then(|| crawl_label(&self.crawl_progress));
|
||||
let App {
|
||||
engine,
|
||||
page,
|
||||
@@ -2320,6 +2341,9 @@ impl eframe::App for App {
|
||||
loaded: *loaded,
|
||||
search,
|
||||
tiles: engine.appearance().tiles,
|
||||
crawl: crawl.as_deref(),
|
||||
offer_metadata: !settings.enabled
|
||||
&& !engine.desktop_prefs().metadata_offer_dismissed,
|
||||
},
|
||||
&mut actions,
|
||||
),
|
||||
|
||||
@@ -25,6 +25,8 @@ pub struct DesktopPrefs {
|
||||
pub filter: crate::app::Filter,
|
||||
pub sort: pstr_core::browse::Sort,
|
||||
pub grouped: bool,
|
||||
/// The library's offer of posters and synopses was turned down.
|
||||
pub metadata_offer_dismissed: bool,
|
||||
}
|
||||
|
||||
impl Default for DesktopPrefs {
|
||||
@@ -37,6 +39,7 @@ impl Default for DesktopPrefs {
|
||||
// On: a share holding a series and its three films is four tiles
|
||||
// of the same picture otherwise.
|
||||
grouped: true,
|
||||
metadata_offer_dismissed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,11 @@ pub enum Event {
|
||||
/// The share the form asked for is stored; its crawl is starting.
|
||||
ShareAdded(String),
|
||||
/// The share the form asked for was refused, with why. Nothing was crawled.
|
||||
ShareRejected(String),
|
||||
ShareRejected {
|
||||
error: String,
|
||||
/// The link has a password of its own and none was given.
|
||||
needs_password: bool,
|
||||
},
|
||||
/// Shares opened; playback is possible from here.
|
||||
Connected {
|
||||
/// Shares that did not open, with why.
|
||||
@@ -305,6 +309,16 @@ pub enum Event {
|
||||
BrowseFailed(String),
|
||||
}
|
||||
|
||||
impl Event {
|
||||
/// The add form's refusal of `error`.
|
||||
fn rejected(error: &pstr_core::Error) -> Self {
|
||||
Self::ShareRejected {
|
||||
error: error.to_string(),
|
||||
needs_password: matches!(error, pstr_core::Error::PasswordNeeded),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the Proton account stands.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub enum AccountStatus {
|
||||
@@ -671,7 +685,7 @@ impl Engine {
|
||||
.await
|
||||
{
|
||||
Ok((share, _)) => share,
|
||||
Err(error) => return engine.emit(Event::ShareRejected(error.to_string())),
|
||||
Err(error) => return engine.emit(Event::rejected(&error)),
|
||||
};
|
||||
engine.emit(Event::ShareAdded(share.name.clone()));
|
||||
engine.load_shares();
|
||||
@@ -720,7 +734,7 @@ impl Engine {
|
||||
};
|
||||
let share = match engine.store.add_folder(&name, folder) {
|
||||
Ok(share) => share,
|
||||
Err(error) => return engine.emit(Event::ShareRejected(error.to_string())),
|
||||
Err(error) => return engine.emit(Event::rejected(&error)),
|
||||
};
|
||||
engine.emit(Event::ShareAdded(share.name.clone()));
|
||||
engine.load_shares();
|
||||
|
||||
@@ -19,6 +19,11 @@ pub struct Shelves<'a> {
|
||||
pub search: &'a str,
|
||||
/// The grid's shape.
|
||||
pub tiles: Tiles,
|
||||
/// What a crawl in progress has found, if one is.
|
||||
pub crawl: Option<&'a str>,
|
||||
/// Whether to offer posters and synopses: they are off, and the offer has
|
||||
/// not been turned down.
|
||||
pub offer_metadata: bool,
|
||||
}
|
||||
|
||||
pub fn show(ui: &mut egui::Ui, art: &mut Art<'_>, shelves: Shelves<'_>, actions: &mut Vec<Action>) {
|
||||
@@ -28,12 +33,14 @@ pub fn show(ui: &mut egui::Ui, art: &mut Art<'_>, shelves: Shelves<'_>, actions:
|
||||
loaded,
|
||||
search,
|
||||
tiles,
|
||||
crawl,
|
||||
offer_metadata,
|
||||
} = shelves;
|
||||
if !loaded {
|
||||
return placeholder(ui);
|
||||
}
|
||||
if library.is_empty() {
|
||||
return empty_state(ui, actions);
|
||||
return empty_state(ui, crawl, actions);
|
||||
}
|
||||
|
||||
let pick = |indices: &[usize]| -> Vec<&Title> {
|
||||
@@ -48,6 +55,10 @@ pub fn show(ui: &mut egui::Ui, art: &mut Art<'_>, shelves: Shelves<'_>, actions:
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false, false])
|
||||
.show(ui, |ui| {
|
||||
if !searching && offer_metadata {
|
||||
metadata_offer(ui, actions);
|
||||
ui.add_space(theme::space::XL);
|
||||
}
|
||||
if !searching {
|
||||
if let Some(title) = featured(library, view, art.metadata, today()) {
|
||||
hero(ui, art, title, actions);
|
||||
@@ -635,9 +646,54 @@ fn placeholder(ui: &mut egui::Ui) {
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_state(ui: &mut egui::Ui, actions: &mut Vec<Action>) {
|
||||
/// Posters and synopses, offered once there is a library to put them on and
|
||||
/// never turned on unasked: switching them on sends title names out.
|
||||
fn metadata_offer(ui: &mut egui::Ui, actions: &mut Vec<Action>) {
|
||||
egui::Frame::new()
|
||||
.fill(theme::card())
|
||||
.corner_radius(egui::CornerRadius::same(theme::radius::MD))
|
||||
.inner_margin(egui::Margin::symmetric(14, 12))
|
||||
.show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
ui.horizontal(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.label(egui::RichText::new("Add posters and synopses?").strong());
|
||||
ui.label(ui::muted(
|
||||
"Turning it on in Settings sends the titles in your library — not your \
|
||||
files, and nothing about what you have watched — to AniList or TMDB.",
|
||||
));
|
||||
});
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui.button("Not now").clicked() {
|
||||
actions.push(Action::DismissMetadataOffer);
|
||||
}
|
||||
if ui::accent_button(ui, "Open settings").clicked() {
|
||||
actions.push(Action::Goto(Page::Settings));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn empty_state(ui: &mut egui::Ui, crawl: Option<&str>, actions: &mut Vec<Action>) {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_space(120.0);
|
||||
// A share just added: say it is being looked through, rather than
|
||||
// offer to add the one that is already filling the page.
|
||||
if let Some(crawl) = crawl {
|
||||
ui.add(egui::Spinner::new().size(28.0));
|
||||
ui.add_space(theme::space::L);
|
||||
ui.label(
|
||||
theme::Role::Title
|
||||
.rich("Looking through the share")
|
||||
.strong(),
|
||||
);
|
||||
ui.add_space(theme::space::S);
|
||||
ui.label(ui::muted(format!(
|
||||
"Its titles appear here once it has been listed ({crawl})."
|
||||
)));
|
||||
return;
|
||||
}
|
||||
ui.label(
|
||||
theme::Role::Title
|
||||
.rich("Nothing in the library yet")
|
||||
|
||||
@@ -46,6 +46,11 @@ pub enum Error {
|
||||
/// mailbox password.
|
||||
#[error("{0}")]
|
||||
SignIn(String),
|
||||
|
||||
/// A public link with a password of its own was opened without one. The
|
||||
/// handshake refusal says nothing a viewer can act on; this does.
|
||||
#[error("This link has a password of its own. Enter it to add the link.")]
|
||||
PasswordNeeded,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
|
||||
@@ -443,7 +443,22 @@ impl ShareStore {
|
||||
));
|
||||
}
|
||||
let custom_password = custom_password.filter(|password| !password.is_empty());
|
||||
let client = open_link(url, custom_password).await?;
|
||||
let client = match open_link(url, custom_password).await {
|
||||
Ok(client) => client,
|
||||
// Asked only after a refusal: the link's public flags cost a round
|
||||
// trip, and most links open without one.
|
||||
Err(error) if custom_password.is_none() => {
|
||||
let protected = ProtonDrivePublicLinkClient::info(client_configuration(), url)
|
||||
.await
|
||||
.is_ok_and(|info| info.is_custom_password_protected);
|
||||
return Err(if protected {
|
||||
Error::PasswordNeeded
|
||||
} else {
|
||||
error
|
||||
});
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
// A name is the viewer's to give, not theirs to invent before they
|
||||
// can see anything: without one, the shared folder's own name.
|
||||
let name = match name.trim() {
|
||||
|
||||
Reference in new issue
Block a user