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: >k::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}