Skip to main content

mimick/library/
sharing_view.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3
4use gtk::prelude::*;
5use libadwaita::prelude::*;
6
7use crate::api_client::{PartnerDirection, UserSummary};
8
9use super::LibraryWindowUi;
10use super::album_share_dialog::{addable_users, display_name};
11use super::context_menu::show_alert_dialog;
12use super::dialogs::confirm;
13
14pub(super) struct SharingView {
15    pub root: gtk::ScrolledWindow,
16    shared_by: libadwaita::PreferencesGroup,
17    shared_with: libadwaita::PreferencesGroup,
18    add: gtk::Button,
19    rows: RefCell<Vec<(libadwaita::PreferencesGroup, gtk::Widget)>>,
20    partners: RefCell<Vec<UserSummary>>,
21}
22
23pub(super) fn build_sharing_view() -> SharingView {
24    let add = gtk::Button::builder()
25        .icon_name("list-add-symbolic")
26        .tooltip_text("Add partner")
27        .valign(gtk::Align::Center)
28        .css_classes(["flat"])
29        .build();
30    let shared_by = libadwaita::PreferencesGroup::builder()
31        .title("You Share With")
32        .description("Partners can see all your photos and videos.")
33        .header_suffix(&add)
34        .build();
35    let shared_with = libadwaita::PreferencesGroup::builder()
36        .title("Sharing With You")
37        .description("People who added you as a partner.")
38        .build();
39    let content = gtk::Box::builder()
40        .orientation(gtk::Orientation::Vertical)
41        .spacing(24)
42        .margin_top(16)
43        .margin_bottom(16)
44        .margin_start(12)
45        .margin_end(12)
46        .build();
47    content.append(&shared_by);
48    content.append(&shared_with);
49    let clamp = libadwaita::Clamp::builder()
50        .maximum_size(820)
51        .child(&content)
52        .build();
53    let root = gtk::ScrolledWindow::builder()
54        .hscrollbar_policy(gtk::PolicyType::Never)
55        .vexpand(true)
56        .child(&clamp)
57        .build();
58    SharingView {
59        root,
60        shared_by,
61        shared_with,
62        add,
63        rows: RefCell::default(),
64        partners: RefCell::default(),
65    }
66}
67
68pub(super) fn connect_sharing_view(ui: Rc<LibraryWindowUi>) {
69    let ui_add = ui.clone();
70    ui.sharing
71        .add
72        .connect_clicked(move |_| present_add_dialog(&ui_add));
73}
74
75pub(super) fn show(ui: &Rc<LibraryWindowUi>) {
76    ui.content_stack.set_visible_child_name("sharing");
77    reload(ui.clone());
78}
79
80fn reload(ui: Rc<LibraryWindowUi>) {
81    glib::MainContext::default().spawn_local(async move {
82        let api = &ui.ctx.api_client;
83        let shared_by = api.fetch_partners(PartnerDirection::SharedBy).await;
84        let shared_with = api.fetch_partners(PartnerDirection::SharedWith).await;
85        match (shared_by, shared_with) {
86            (Ok(shared_by), Ok(shared_with)) => render(&ui, shared_by, &shared_with),
87            (Err(err), _) | (_, Err(err)) => show_alert_dialog(
88                &ui,
89                "Could Not Load Partners",
90                &format!("{err}\n\nYour API key needs the 'partner.read' permission."),
91            ),
92        }
93    });
94}
95
96fn render(ui: &Rc<LibraryWindowUi>, shared_by: Vec<UserSummary>, shared_with: &[UserSummary]) {
97    let view = &ui.sharing;
98    for (group, row) in view.rows.borrow_mut().drain(..) {
99        group.remove(&row);
100    }
101    let mut rows = view.rows.borrow_mut();
102    for partner in &shared_by {
103        let row = partner_row(partner);
104        row.add_suffix(&remove_button(ui, partner));
105        view.shared_by.add(&row);
106        rows.push((view.shared_by.clone(), row.upcast()));
107    }
108    if shared_by.is_empty() {
109        let row = placeholder_row("Use + to share your library with someone.");
110        view.shared_by.add(&row);
111        rows.push((view.shared_by.clone(), row.upcast()));
112    }
113    for partner in shared_with {
114        let row = partner_row(partner);
115        view.shared_with.add(&row);
116        rows.push((view.shared_with.clone(), row.upcast()));
117    }
118    if shared_with.is_empty() {
119        let row = placeholder_row("No one is sharing with you.");
120        view.shared_with.add(&row);
121        rows.push((view.shared_with.clone(), row.upcast()));
122    }
123    *view.partners.borrow_mut() = shared_by;
124}
125
126fn partner_row(user: &UserSummary) -> libadwaita::ActionRow {
127    let name = display_name(&user.name, &user.email);
128    let row = libadwaita::ActionRow::builder()
129        .title(&name)
130        .subtitle(&user.email)
131        .use_markup(false)
132        .title_lines(1)
133        .subtitle_lines(1)
134        .build();
135    row.add_prefix(&libadwaita::Avatar::new(32, Some(&name), true));
136    row
137}
138
139fn placeholder_row(text: &str) -> libadwaita::ActionRow {
140    libadwaita::ActionRow::builder()
141        .title(text)
142        .use_markup(false)
143        .css_classes(["dim-label"])
144        .build()
145}
146
147fn remove_button(ui: &Rc<LibraryWindowUi>, partner: &UserSummary) -> gtk::Button {
148    let button = gtk::Button::builder()
149        .icon_name("user-trash-symbolic")
150        .tooltip_text("Stop sharing")
151        .valign(gtk::Align::Center)
152        .css_classes(["flat"])
153        .build();
154    let (ui, partner) = (ui.clone(), partner.clone());
155    button.connect_clicked(move |_| {
156        let name = display_name(&partner.name, &partner.email);
157        let (ui_remove, id) = (ui.clone(), partner.id.clone());
158        confirm(
159            &ui,
160            &format!("Stop sharing with {name}?"),
161            "They'll no longer see your photos and videos.",
162            "Stop Sharing",
163            move || change(&ui_remove, "Could Not Remove Partner", false, id.clone()),
164        );
165    });
166    button
167}
168
169fn change(ui: &Rc<LibraryWindowUi>, heading: &'static str, add: bool, user_id: String) {
170    let ui = ui.clone();
171    glib::MainContext::default().spawn_local(async move {
172        let api = &ui.ctx.api_client;
173        let result = if add {
174            api.add_partner(&user_id).await
175        } else {
176            api.remove_partner(&user_id).await
177        };
178        match result {
179            Ok(()) => reload(ui),
180            Err(err) => show_alert_dialog(&ui, heading, &err),
181        }
182    });
183}
184
185#[derive(Clone)]
186struct AddPartnerDialog {
187    ui: Rc<LibraryWindowUi>,
188    dialog: libadwaita::Dialog,
189    search: gtk::SearchEntry,
190    list: gtk::ListBox,
191    status: gtk::Label,
192    users: Rc<RefCell<Vec<UserSummary>>>,
193    excluded: Rc<RefCell<Vec<String>>>,
194}
195
196fn present_add_dialog(ui: &Rc<LibraryWindowUi>) {
197    let add = AddPartnerDialog::build(ui);
198    let add_for_search = add.clone();
199    add.search
200        .connect_search_changed(move |_| add_for_search.fill());
201    add.load();
202    add.dialog.present(Some(&ui.window));
203}
204
205impl AddPartnerDialog {
206    fn build(ui: &Rc<LibraryWindowUi>) -> Self {
207        let search = gtk::SearchEntry::builder()
208            .placeholder_text("Find people by name or email")
209            .build();
210        let list = gtk::ListBox::builder()
211            .selection_mode(gtk::SelectionMode::None)
212            .css_classes(["boxed-list"])
213            .valign(gtk::Align::Start)
214            .build();
215        let status = gtk::Label::builder()
216            .label("Loading people…")
217            .wrap(true)
218            .css_classes(["dim-label"])
219            .build();
220        let body = gtk::Box::builder()
221            .orientation(gtk::Orientation::Vertical)
222            .spacing(12)
223            .margin_top(6)
224            .margin_bottom(12)
225            .margin_start(12)
226            .margin_end(12)
227            .build();
228        body.append(&search);
229        body.append(&status);
230        body.append(&list);
231        let excluded = ui
232            .sharing
233            .partners
234            .borrow()
235            .iter()
236            .map(|p| p.id.clone())
237            .collect();
238        Self {
239            ui: ui.clone(),
240            dialog: dialog_with_body(&body),
241            search,
242            list,
243            status,
244            users: Rc::default(),
245            excluded: Rc::new(RefCell::new(excluded)),
246        }
247    }
248
249    fn fill(&self) {
250        self.list.remove_all();
251        let users = self.users.borrow();
252        let candidates = addable_users(&users, &self.excluded.borrow(), &self.search.text());
253        for user in &candidates {
254            self.list.append(&add_row(&self.ui, &self.dialog, user));
255        }
256        self.status.set_visible(candidates.is_empty());
257        self.status.set_label(if users.is_empty() {
258            "No other people found. Listing people needs the 'user.read' permission."
259        } else {
260            "No one matches your search."
261        });
262        self.list.set_visible(!candidates.is_empty());
263    }
264
265    fn load(&self) {
266        let add = self.clone();
267        glib::MainContext::default().spawn_local(async move {
268            if let Some(me) = super::ensure_current_user_id(&add.ui.ctx).await {
269                add.excluded.borrow_mut().push(me);
270            }
271            match add.ui.ctx.api_client.list_users().await {
272                Ok(list) => *add.users.borrow_mut() = list,
273                Err(err) => log::warn!("Could not list users for partners: {}", err),
274            }
275            add.fill();
276        });
277    }
278}
279
280fn dialog_with_body(body: &gtk::Box) -> libadwaita::Dialog {
281    let scroller = gtk::ScrolledWindow::builder()
282        .hscrollbar_policy(gtk::PolicyType::Never)
283        .propagate_natural_height(true)
284        .vexpand(true)
285        .child(body)
286        .build();
287    let toolbar = libadwaita::ToolbarView::new();
288    toolbar.add_top_bar(&libadwaita::HeaderBar::new());
289    toolbar.set_content(Some(&scroller));
290    libadwaita::Dialog::builder()
291        .title("Add Partner")
292        .content_width(420)
293        .content_height(520)
294        .child(&toolbar)
295        .build()
296}
297
298fn add_row(
299    ui: &Rc<LibraryWindowUi>,
300    dialog: &libadwaita::Dialog,
301    user: &UserSummary,
302) -> libadwaita::ActionRow {
303    let row = partner_row(user);
304    let add = gtk::Button::builder()
305        .label("Add")
306        .valign(gtk::Align::Center)
307        .build();
308    row.add_suffix(&add);
309    let (ui, dialog, user) = (ui.clone(), dialog.clone(), user.clone());
310    add.connect_clicked(move |_| {
311        let name = display_name(&user.name, &user.email);
312        let (ui_add, dialog, id) = (ui.clone(), dialog.clone(), user.id.clone());
313        let share = libadwaita::AlertDialog::builder()
314            .heading(format!("Share your library with {name}?"))
315            .body("They'll be able to see all your photos and videos. You can stop sharing at any time.")
316            .build();
317        share.add_response("cancel", "Cancel");
318        share.add_response("share", "Share");
319        share.set_response_appearance("share", libadwaita::ResponseAppearance::Suggested);
320        share.set_default_response(Some("share"));
321        share.set_close_response("cancel");
322        share.connect_response(None, move |_, response| {
323            if response == "share" {
324                dialog.close();
325                change(&ui_add, "Could Not Add Partner", true, id.clone());
326            }
327        });
328        share.present(Some(&ui.window));
329    });
330    row
331}