Skip to main content

mimick/library/
upload_picker.rs

1//! Manual upload entry point.
2//!
3//! Opens a multi-file picker, hashes each selection on a blocking worker, and
4//! enqueues a `FileTask` on the shared `QueueManager`. The Upload header-bar
5//! button in the library window invokes [`pick_and_upload`]; the same flow is
6//! used for both library and album scopes — the caller resolves the album.
7
8use std::path::PathBuf;
9use std::sync::Arc;
10
11use gtk::gio;
12use gtk::prelude::*;
13
14use crate::app_context::AppContext;
15use crate::monitor::compute_sha1_chunked;
16use crate::queue_manager::FileTask;
17
18/// Open the OS file picker and enqueue every chosen file for upload.
19///
20/// `album` carries the target album id and display name when the caller is in
21/// a selected-album view; otherwise pass `None` and files go to the library.
22pub fn pick_and_upload(
23    parent: &libadwaita::ApplicationWindow,
24    ctx: Arc<AppContext>,
25    album: Option<(String, String)>,
26) {
27    let dialog = gtk::FileDialog::builder()
28        .title(if album.is_some() {
29            "Upload to album"
30        } else {
31            "Upload to library"
32        })
33        .modal(true)
34        .build();
35
36    let parent = parent.clone();
37    dialog.open_multiple(Some(&parent), gio::Cancellable::NONE, move |result| {
38        let files = match result {
39            Ok(files) => files,
40            Err(err) => {
41                if !err.matches(gtk::DialogError::Dismissed) {
42                    log::warn!("Upload picker failed: {}", err);
43                }
44                return;
45            }
46        };
47        let paths: Vec<PathBuf> = (0..files.n_items())
48            .filter_map(|i| files.item(i))
49            .filter_map(|obj| obj.downcast::<gio::File>().ok())
50            .filter_map(|file| file.path())
51            .collect();
52        if paths.is_empty() {
53            return;
54        }
55        spawn_enqueue(ctx.clone(), album.clone(), paths);
56    });
57}
58
59pub(crate) fn spawn_enqueue(
60    ctx: Arc<AppContext>,
61    album: Option<(String, String)>,
62    paths: Vec<PathBuf>,
63) {
64    spawn_enqueue_with_callback(ctx, album, paths, |_, _| {});
65}
66
67/// Enqueue files for upload and invoke `on_complete(queued, skipped)` on the
68/// main context when all files have been hashed and enqueued.
69pub(crate) fn spawn_enqueue_with_callback<F>(
70    ctx: Arc<AppContext>,
71    album: Option<(String, String)>,
72    paths: Vec<PathBuf>,
73    on_complete: F,
74) where
75    F: FnOnce(usize, usize) + 'static,
76{
77    glib::MainContext::default().spawn_local(async move {
78        let total = paths.len();
79        let mut queued = 0usize;
80        for path in paths {
81            if let Some(task) = build_file_task(&ctx, &album, &path).await
82                && ctx.queue_manager.add_to_queue(task).await
83            {
84                queued += 1;
85            }
86        }
87        let skipped = total - queued;
88        log::info!(
89            "Manual upload: queued {} file(s), skipped {}",
90            queued,
91            skipped
92        );
93        on_complete(queued, skipped);
94    });
95}
96
97/// Build a `FileTask` for a single path, computing checksum and resolving sidecar.
98async fn build_file_task(
99    ctx: &AppContext,
100    album: &Option<(String, String)>,
101    path: &std::path::Path,
102) -> Option<FileTask> {
103    let path_str = path.to_str().map(str::to_owned)?;
104    let watch_path = path
105        .parent()
106        .and_then(|p| p.to_str())
107        .map(str::to_owned)
108        .unwrap_or_default();
109    let hash_target = path_str.clone();
110    let checksum =
111        match tokio::task::spawn_blocking(move || compute_sha1_chunked(&hash_target)).await {
112            Ok(Ok(c)) => c,
113            Ok(Err(err)) => {
114                log::warn!("Could not checksum upload '{}': {}", path_str, err);
115                return None;
116            }
117            Err(err) => {
118                log::warn!("Checksum task failed for '{}': {}", path_str, err);
119                return None;
120            }
121        };
122    let sidecar_path = if ctx.config.read().data.upload_xmp_sidecars {
123        crate::sidecar::find_sidecar(path).map(|p| p.to_string_lossy().into_owned())
124    } else {
125        None
126    };
127    Some(FileTask {
128        path: path_str,
129        watch_path,
130        checksum,
131        album_id: album.as_ref().map(|(id, _)| id.clone()),
132        album_name: album.as_ref().map(|(_, name)| name.clone()),
133        reassociate_only: false,
134        skip_album: album.is_none(),
135        sidecar_path,
136    })
137}